1/** 2 * ============================================================================ 3 * SCRIPT.JS â ÐбÑий ÑкÑÐ¸Ð¿Ñ ÑÑÑаниÑÑ Predanie.ru 4 * ============================================================================ 5 * 6 * ÐÑÐ¾Ñ Ñайл подклÑÑаеÑÑÑ Ð½Ð° вÑÐµÑ ÑÑÑаниÑÐ°Ñ Ð¿ÑоекÑа. 7 * СодеÑÐ¶Ð¸Ñ Ð¾Ð±Ñие UI-ÑлÑÑÑÐµÐ½Ð¸Ñ Ð¸ поведенÑеÑкие ÑкÑипÑÑ: 8 * 9 * ФУÐÐЦÐÐÐÐÐ: 10 * 1. ÐÑÐµÐ»Ð¾Ð°Ð´ÐµÑ â плавное иÑÑезновение пÑи загÑÑзке 11 * 2. ÐобилÑное Ð¼ÐµÐ½Ñ â оÑкÑÑÑие/закÑÑÑие ÑайдбаÑа 12 * 3. РаÑкÑÑваÑÑиеÑÑ Ð³ÑÑÐ¿Ð¿Ñ Ð¼ÐµÐ½Ñ Ð² ÑайдбаÑе 13 * 4. ÐеÑеклÑÑение ÑÐµÐ¼Ñ (light/dark) + ÑÐ¾Ñ Ñанение в localStorage 14 * 5. Bootstrap-Ð¿Ð¾Ð´Ð¾Ð±Ð½Ð°Ñ Ð²Ð°Ð»Ð¸Ð´Ð°ÑÐ¸Ñ ÑоÑм 15 * 6. СвоÑаÑивание блоков пÑавого ÑайдбаÑа 16 * 17 * ÐÐ ÐÐЦÐÐЫ: 18 * - СкÑÐ¸Ð¿Ñ Ð½Ðµ должен падаÑÑ Ð½Ð° ÑÑÑаниÑÐ°Ñ , где ÑаÑÑи ÑазмеÑки оÑÑÑÑÑÑвÑÑÑ 19 * - Ðезде еÑÑÑ Ð¿ÑовеÑки налиÑÐ¸Ñ ÑлеменÑов 20 * - ÐаÑÑÑойки полÑзоваÑÐµÐ»Ñ ÑÐ¾Ñ ÑанÑÑÑÑÑ Ð² localStorage 21 * 22 * ÐÐÐÐСÐÐÐСТÐ: 23 * - Bootstrap 5 (bootstrap.bundle.min.js) â опÑионалÑно 24 */ 25 26 // ------------------------------ 27 // 3.x) СвоÑаÑивание/ÑазвоÑаÑивание блоков пÑавого ÑайдбаÑа 28 // ÐÐ ÐÐÐÐÐÐ ÐТÐÐЮЧÐÐÐ 29 // ------------------------------ 30 /* 31 function initSidebarBannerCollapse() { 32 const storageKey = 'sidebarBannerStates'; 33 const readStoredState = () => { 34 try { 35 const raw = localStorage.getItem(storageKey); 36 if (!raw) return {}; 37 const parsed = JSON.parse(raw); 38 if (!parsed || typeof parsed !== 'object') return {}; 39 return parsed; 40 } catch { 41 return {}; 42 } 43 }; 44 const writeStoredState = state => { 45 try { 46 localStorage.setItem(storageKey, JSON.stringify(state)); 47 } catch {} 48 }; 49 50 const banners = document.querySelectorAll('.sidebar-banner[data-sidebar-banner-id]'); 51 if (!banners.length) return; 52 53 const state = readStoredState(); 54 55 banners.forEach(banner => { 56 const id = banner.getAttribute('data-sidebar-banner-id'); 57 if (!id) return; 58 const collapseBtn = banner.querySelector('.sidebar-banner__collapse'); 59 const content = Array.from(banner.children).find( 60 el => el.classList && el.classList.contains('sidebar-banner__content') 61 ); 62 // ÐÑименÑем ÑÐ¾Ñ ÑанÑнное ÑоÑÑоÑние 63 const isCollapsed = state[id] === false ? false : state[id] === true; 64 if (isCollapsed) { 65 banner.classList.add('is-collapsed'); 66 if (content) content.style.display = 'none'; 67 if (collapseBtn) collapseBtn.querySelector('span').classList.remove('fa-chevron-up'); 68 if (collapseBtn) collapseBtn.querySelector('span').classList.add('fa-chevron-down'); 69 } else { 70 banner.classList.remove('is-collapsed'); 71 if (content) content.style.display = ''; 72 if (collapseBtn) collapseBtn.querySelector('span').classList.remove('fa-chevron-down'); 73 if (collapseBtn) collapseBtn.querySelector('span').classList.add('fa-chevron-up'); 74 } 75 if (collapseBtn) { 76 collapseBtn.addEventListener('click', () => { 77 const nowCollapsed = !banner.classList.contains('is-collapsed'); 78 banner.classList.toggle('is-collapsed', nowCollapsed); 79 if (content) content.style.display = nowCollapsed ? 'none' : ''; 80 if (collapseBtn.querySelector('span')) { 81 collapseBtn.querySelector('span').classList.toggle('fa-chevron-up', !nowCollapsed); 82 collapseBtn.querySelector('span').classList.toggle('fa-chevron-down', nowCollapsed); 83 } 84 state[id] = nowCollapsed; 85 writeStoredState(state); 86 }); 87 } 88 }); 89 // ÐÑклÑÑаем preinit-Ñежим (CSS пеÑеÑÑаÑÑ Ð¾ÑиенÑиÑоваÑÑÑÑ Ð½Ð° аÑÑибÑÑÑ Ð½Ð° <html>) 90 document.documentElement.classList.add('sidebar-banners-init'); 91 } 92 93 initSidebarBannerCollapse(); 94 */ 95 // ÐÐÐÐЦ ÐÐ ÐÐÐÐÐÐ ÐТÐÐЮЧÐÐÐÐÐÐ ÐÐÐÐ 96/** 97 * ÐÑÐ¾Ñ Ñайл â обÑий «ÑкÑÐ¸Ð¿Ñ ÑÑÑаниÑÑ». 98 * 99 * ÐдеÑÑ ÑобÑÐ°Ð½Ñ Ð½ÐµÐ±Ð¾Ð»ÑÑие поведенÑеÑкие ÑлÑÑÑениÑ, коÑоÑÑе иÑполÑзÑÑÑÑÑ 100 * на Ð¼Ð½Ð¾Ð³Ð¸Ñ ÑÑÑаниÑÐ°Ñ Ð¿ÑоекÑа: 101 * - пÑÐµÐ»Ð¾Ð°Ð´ÐµÑ (еÑли пÑиÑÑÑÑÑвÑÐµÑ ÑазмеÑка) 102 * - мобилÑное оÑкÑÑÑие/закÑÑÑие ÑайдбаÑа (ÑÑÑеÑÑвÑÑÑий toggle) 103 * - ÑаÑкÑÑваÑÑиеÑÑ Ð³ÑÑÐ¿Ð¿Ñ Ð¼ÐµÐ½Ñ Ð² ÑайдбаÑе (Фонд/ÐедиаÑека) 104 * - пеÑеклÑÑение ÑÐµÐ¼Ñ (light/dark) + ÑÐ¾Ñ Ñанение в localStorage 105 * - Bootstrap-Ð¿Ð¾Ð´Ð¾Ð±Ð½Ð°Ñ Ð²Ð°Ð»Ð¸Ð´Ð°ÑÐ¸Ñ ÑоÑм 106 * 107 * Ðажно: 108 * - СкÑÐ¸Ð¿Ñ Ð½Ðµ должен падаÑÑ Ð½Ð° ÑÑÑаниÑÐ°Ñ , где ÑаÑÑи ÑазмеÑки оÑÑÑÑÑÑвÑÑÑ. 109 * ÐоÑÑÐ¾Ð¼Ñ Ð¿Ð¾ÑÑи везде еÑÑÑ Ð¿ÑовеÑки налиÑÐ¸Ñ ÑлеменÑов. 110 * ÐÑÑоÑник дизайна иÑÑ Ð¾Ð´Ð½Ð¾Ð¹ ÑемÑ: 111 * WEBSITE: https://themefisher.com 112 * TWITTER: https://twitter.com/themefisher 113 * FACEBOOK: https://facebook.com/themefisher 114 * GITHUB: https://github.com/themefisher/ 115 */ 116 117'use strict'; 118 119// ------------------------------ 120// 1) ÐÑÐµÐ»Ð¾Ð°Ð´ÐµÑ (еÑли еÑÑÑ) 121// ------------------------------ 122// ÐниÑиализиÑÑеÑÑÑ Ð½Ð° ÑобÑÑии window.load (когда загÑÑзилиÑÑ ÑеÑÑÑÑÑ). 123// ÐонÑÑÐ°ÐºÑ ÑазмеÑки: 124// - body полÑÑÐ°ÐµÑ ÐºÐ»Ð°ÑÑ "loaded" 125// - .preloader-wrapper (еÑли ÑÑÑеÑÑвÑеÑ) Ñоже полÑÑÐ°ÐµÑ ÐºÐ»Ð°ÑÑ "loaded" 126// ÐÑо позволÑÐµÑ CSS'ом ÑкÑÑÑÑ Ð¿ÑÐµÐ»Ð¾Ð°Ð´ÐµÑ Ð¿Ð¾Ñле полной загÑÑзки. 127 128window.addEventListener('load', function () { 129 function preLoader() { 130 document.body.classList.add('loaded'); 131 const preloaderWrapper = document.querySelector('.preloader-wrapper'); 132 if (preloaderWrapper) { 133 preloaderWrapper.classList.add('loaded'); 134 } 135 } 136 preLoader(); 137}); 138 139document.addEventListener('DOMContentLoaded', function () { 140 // ------------------------------ 141 // 2) ÐобилÑнÑй toggle ÑайдбаÑа 142 // ------------------------------ 143 // ÐÑо ÑÑÑеÑÑвÑÑÑÐ°Ñ Ð»Ð¾Ð³Ð¸ÐºÐ° ÑемÑ. 144 // Ðажно: в далÑнейÑÐ¸Ñ Ð¿ÑÐ°Ð²ÐºÐ°Ñ Ð¼Ñ ÐµÑ ÐÐ ÑÑогаем, 145 // ÑÑÐ¾Ð±Ñ Ð¼Ð¾Ð±Ð¸Ð»Ñное поведение оÑÑавалоÑÑ ÑÑабилÑнÑм. 146 // 147 // ÐонÑÑÐ°ÐºÑ ÑазмеÑки: 148 // - кнопка: .nav-toggle 149 // - ÑайдбаÑ: .sidenav (в мобилÑной веÑÑÑке пÑÑÑеÑÑÑ Ð·Ð° ÑкÑаном) 150 // - иконка внÑÑÑи кнопки: .nav-toggle span (FontAwesome) 151 // - заÑемнение/овеÑлей: .nav-toggle-overlay 152 153 // ÐбÑие Ñ ÐµÐ»Ð¿ÐµÑÑ Ð´Ð»Ñ Â«Ð²Ð·Ð°Ð¸Ð¼Ð¾Ð¸ÑклÑÑаÑÑÐ¸Ñ Â» ÑайдбаÑов: 154 // в моб/планÑеÑной веÑÑÑке Ñ Ð¾Ñим, ÑÑÐ¾Ð±Ñ Ð¾Ð´Ð½Ð¾Ð²Ñеменно бÑл оÑкÑÑÑ 155 // макÑимÑм один ÑайдбаÑ. 156 function initSidebarMutualExclusion() { 157 const sidenav = document.querySelector('.sidenav'); 158 const navToggleSpan = document.querySelector('.nav-toggle span'); 159 const navToggleOverlay = document.querySelector('.nav-toggle-overlay'); 160 161 const rightSidebar = document.querySelector('.right-sidebar'); 162 const rightNavToggleSpans = document.querySelectorAll('.right-nav-toggle span'); 163 const rightNavToggleOverlay = document.querySelector( 164 '.right-nav-toggle-overlay', 165 ); 166
167 const setLeftClosedIcon = () => { 168 if (!navToggleSpan) return; 169 navToggleSpan.classList.remove('fa-times'); 170 navToggleSpan.classList.add('fa-bars'); 171 }; 172 173 const setRightClosedIcon = () => { 174 rightNavToggleSpans.forEach(span => { 175 span.classList.remove('fa-times'); 176 span.classList.add('fa-indent'); 177 span.classList.add('fa-flip-horizontal'); 178 }); 179 }; 180 181 const closeLeftSidebar = () => { 182 if (sidenav) sidenav.classList.remove('show'); 183 setLeftClosedIcon(); 184 if (navToggleOverlay) navToggleOverlay.classList.remove('show'); 185 }; 186 187 const closeRightSidebar = () => { 188 const rightSidebarContainer = document.querySelector( 189 '.right-sidebar-container', 190 ); 191 if (rightSidebar) rightSidebar.classList.remove('show'); 192 if (rightSidebarContainer) rightSidebarContainer.classList.remove('show'); 193 setRightClosedIcon(); 194 if (rightNavToggleOverlay) rightNavToggleOverlay.classList.remove('show'); 195 document.body.classList.remove('right-sidebar-open'); 196 }; 197 198 return { 199 closeLeftSidebar, 200 closeRightSidebar, 201 setLeftClosedIcon, 202 setRightClosedIcon, 203 }; 204 } 205 206 const sidebarMutex = initSidebarMutualExclusion(); 207 208 function navbarToggle() { 209 const navToggle = document.querySelector('.nav-toggle'); 210 const sidenav = document.querySelector('.sidenav'); 211 const navToggleSpan = document.querySelector('.nav-toggle span'); 212 const navToggleOverlay = document.querySelector('.nav-toggle-overlay'); 213 214 if (navToggle) { 215 navToggle.addEventListener('click', function () { 216 // ÐÑли оÑкÑÑваем левÑй â закÑÑваем пÑавÑй. 217 const rightSidebarContainer = document.querySelector( 218 '.right-sidebar-container', 219 ); 220 if ( 221 rightSidebarContainer?.classList.contains('show') || 222 document.querySelector('.right-sidebar')?.classList.contains('show') 223 ) { 224 sidebarMutex?.closeRightSidebar?.(); 225 } 226 if (sidenav) { 227 sidenav.classList.toggle('show'); 228 } 229 if (navToggleSpan) { 230 navToggleSpan.classList.toggle('fa-times'); 231 navToggleSpan.classList.toggle('fa-bars'); 232 } 233 if (navToggleOverlay) { 234 navToggleOverlay.classList.toggle('show'); 235 } 236 }); 237 } 238 239 if (navToggleOverlay) { 240 navToggleOverlay.addEventListener('click', function () { 241 if (sidenav) { 242 sidenav.classList.remove('show'); 243 } 244 if (navToggleSpan) { 245 navToggleSpan.classList.remove('fa-times'); 246 navToggleSpan.classList.add('fa-bars'); 247 } 248 if (navToggleOverlay) { 249 navToggleOverlay.classList.remove('show'); 250 } 251 }); 252 } 253 } 254 navbarToggle(); 255 256 // ------------------------------ 257 // 2.0.1) ÐодÑказка пÑокÑÑÑки ÑайдбаÑа (мобилÑнÑе) 258 // ------------------------------ 259 function initSidebarScrollHint() { 260 const sidenav = document.querySelector('.sidenav'); 261 const scrollHint = document.querySelector('.js-sidebar-scroll-hint'); 262 if (!sidenav || !scrollHint) return; 263 264 // СкÑÑваем подÑÐºÐ°Ð·ÐºÑ Ð¿Ð¾Ñле неболÑÑого ÑкÑолла 265 const handleScroll = () => { 266 if (sidenav.scrollTop > 50) { 267 scrollHint.classList.add('hidden'); 268 } else { 269 scrollHint.classList.remove('hidden'); 270 } 271 }; 272 273 sidenav.addEventListener('scroll', handleScroll, { passive: true }); 274 275 // Также ÑкÑÑваем, еÑли ÑÐ°Ð¹Ð´Ð±Ð°Ñ Ñже пÑокÑÑÑен пÑи загÑÑзке 276 handleScroll(); 277 } 278 initSidebarScrollHint(); 279 280 // ------------------------------ 281 // 2.1) ÐобилÑнÑй toggle ÐÐ ÐÐÐÐÐ ÑайдбаÑа 282 // ------------------------------ 283 // ÐонÑÑÐ°ÐºÑ ÑазмеÑки: 284 // - кнопка: .right-nav-toggle 285 // - конÑÐµÐ¹Ð½ÐµÑ Ð¿Ñавого ÑайдбаÑа: .right-sidebar-container (на мобилÑном fixed за ÑкÑаном) 286 // - пÑавÑй ÑайдбаÑ: .right-sidebar (внÑÑÑи конÑейнеÑа) 287 // - иконка внÑÑÑи кнопки: .right-nav-toggle span (FontAwesome) 288 // - заÑемнение/овеÑлей: .right-nav-toggle-overlay 289 function rightSidebarToggle() { 290 const rightNavToggles = document.querySelectorAll('.right-nav-toggle'); 291 const rightSidebarContainer = document.querySelector( 292 '.right-sidebar-container', 293 ); 294 const rightSidebar = document.querySelector('.right-sidebar'); 295 const rightNavToggleSpans = document.querySelectorAll('.right-nav-toggle span'); 296 const rightNavToggleOverlay = document.querySelector( 297 '.right-nav-toggle-overlay', 298 ); 299 300 const setClosedIcon = () => { 301 rightNavToggleSpans.forEach(span => { 302 span.classList.remove('fa-times'); 303 span.classList.add('fa-indent'); 304 span.classList.add('fa-flip-horizontal'); 305 }); 306 }; 307 308 const setOpenIcon = () => { 309 rightNavToggleSpans.forEach(span => { 310 span.classList.add('fa-times'); 311 span.classList.remove('fa-indent'); 312 span.classList.remove('fa-flip-horizontal'); 313 }); 314 }; 315
316 rightNavToggles.forEach(btn => { 317 btn.addEventListener('click', function () { 318 // ÐÑли оÑкÑÑваем пÑавÑй â закÑÑваем левÑй. 319 if (document.querySelector('.sidenav')?.classList.contains('show')) { 320 sidebarMutex?.closeLeftSidebar?.(); 321 } 322 // ÐеÑеклÑÑаем show на конÑейнеÑе 323 const isNowOpen = rightSidebarContainer 324 ? rightSidebarContainer.classList.toggle('show') 325 : false; 326 // Также на Ñамом ÑайдбаÑе Ð´Ð»Ñ ÑовмеÑÑимоÑÑи 327 if (rightSidebar) { 328 rightSidebar.classList.toggle('show', isNowOpen); 329 } 330 331 if (isNowOpen) { 332 setOpenIcon(); 333 document.body.classList.add('right-sidebar-open'); 334 } else { 335 setClosedIcon(); 336 document.body.classList.remove('right-sidebar-open'); 337 } 338 339 if (rightNavToggleOverlay) { 340 rightNavToggleOverlay.classList.toggle('show'); 341 } 342 }); 343 }); 344 345 if (rightNavToggleOverlay) { 346 rightNavToggleOverlay.addEventListener('click', function () { 347 if (rightSidebarContainer) { 348 rightSidebarContainer.classList.remove('show'); 349 } 350 if (rightSidebar) { 351 rightSidebar.classList.remove('show'); 352 } 353 setClosedIcon(); 354 document.body.classList.remove('right-sidebar-open'); 355 rightNavToggleOverlay.classList.remove('show'); 356 }); 357 } 358 } 359 rightSidebarToggle(); 360 361 // ------------------------------ 362 // 2.1) ÐодÑвеÑка акÑивного пÑнкÑа Ð¼ÐµÐ½Ñ Ð¿Ð¾ URL 363 // ------------------------------ 364 function highlightActiveMenuItem() { 365 const path = window.location.pathname; 366 const search = window.location.search; 367 const fullPath = path + search; 368 const navItems = document.querySelectorAll('.sidenav .navbar-nav .nav-item'); 369 const dropMenuLinks = document.querySelectorAll('.sidenav .drop-menu a'); 370 371 // ÐÑиоÑиÑÐµÑ Ñовпадений Ð´Ð»Ñ Ð¿ÑнкÑов 1 ÑÑÐ¾Ð²Ð½Ñ (Ð¾Ñ Ñамого ÑпеÑиÑиÑного к обÑемÑ) 372 const menuPaths = [ 373 { prefix: '/bible', selector: 'a[href="/bible"]' }, 374 { prefix: '/calendar', selector: 'a[href="/calendar"]' }, 375 { prefix: '/blog', selector: 'a[href="/blog"]' }, 376 { prefix: '/lecture', selector: 'a[href="/lecture/"]', parent: 'media' }, 377 { prefix: '/popular', selector: 'a[href="/media"]', parent: 'media' }, 378 { prefix: '/authors', selector: 'a[href="/media"]', parent: 'media' }, 379 { prefix: '/media', selector: 'a[href="/media"]' }, 380 { prefix: '/new', selector: 'a[href="/media"]', parent: 'media' }, 381 { prefix: '/special-projects', selector: 'a[href="/special-projects"]' }, 382 { prefix: '/kachu', selector: 'a[href="/kachu"]', parent: 'projects' }, 383 { prefix: '/hibiny', selector: 'a[href="/hibiny"]', parent: 'projects' }, 384 { prefix: '/fond', selector: 'a[href="/fond"]' }, 385 { prefix: '/crew', selector: 'a[href="/crew/"]', parent: 'about' }, 386 { prefix: '/sovet', selector: 'a[href="/sovet/"]', parent: 'about' }, 387 { prefix: '/friends', selector: 'a[href="/friends/"]', parent: 'about' }, 388 { prefix: '/about', selector: 'a[href="/about/"]' }, 389 ]; 390 391 // ÐÑнкÑÑ 2 ÑÑÐ¾Ð²Ð½Ñ Ð´Ð»Ñ ÑоÑного ÑÐ¾Ð²Ð¿Ð°Ð´ÐµÐ½Ð¸Ñ 392 const subMenuPaths = [ 393 // Ð Ð½Ð°Ñ 394 { path: '/crew/', href: '/crew/' }, 395 { path: '/sovet/', href: '/sovet/' }, 396 { path: '/friends/', href: '/friends/' }, 397 { path: '/about/leaders/', href: '/about/leaders/' }, 398 // Фонд 399 { path: '/fond/about/', href: '/fond/about/' }, 400 { path: '/fond/blago/blog/post/news', href: '/fond/blago/blog/post/news/1/all' }, 401 { path: '/fond/blago/blog/post/reports', href: '/fond/blago/blog/post/reports' }, 402 { path: '/fond/archive/', href: '/fond/archive/' }, 403 { path: '/fond/programmes', href: '/fond/programmes' }, 404 { path: '/fond/recurrent/', href: '/fond/recurrent/' }, 405 { path: '/fond/security/', href: '/fond/security/' }, 406 { path: '/fond/refund/', href: '/fond/refund/' }, 407 { path: '/fond/about/map', href: '/fond/about/map' }, 408 // ÐедиаÑека 409 { path: '/lecture/', href: '/lecture/' }, 410 { path: '/new', exact: true, href: '/new' }, 411 { path: '/media', exact: true, href: '/media' }, 412 { path: '/popular', href: '/popular' }, 413 { path: '/authors', href: '/authors' }, 414 // СпеÑпÑоекÑÑ 415 { path: '/kachu', href: '/kachu' }, 416 { path: '/hibiny', href: '/hibiny' }, 417 ]; 418 419 // СнаÑала ÑбиÑаем вÑе active
420 navItems.forEach(item => item.classList.remove('active')); 421 dropMenuLinks.forEach(link => link.classList.remove('active')); 422 423 // ÐÑем Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑий пÑÐ½ÐºÑ Ð¼ÐµÐ½Ñ 1 ÑÑÐ¾Ð²Ð½Ñ 424 let matched = false; 425 for (const mp of menuPaths) { 426 if (path.startsWith(mp.prefix)) { 427 const link = document.querySelector('.sidenav .navbar-nav ' + mp.selector); 428 if (link) { 429 const navItem = link.closest('.nav-item'); 430 if (navItem) { 431 navItem.classList.add('active'); 432 matched = true; 433 break; 434 } 435 } 436 // ÐÑли еÑÑÑ parent, акÑивиÑÑем ÑодиÑелÑÑкÑÑ Ð³ÑÑÐ¿Ð¿Ñ 437 if (mp.parent) { 438 const parentLink = document.querySelector('.sidenav .navbar-nav .sidebar-group[data-sidebar-group="' + mp.parent + '"]'); 439 if (parentLink) { 440 parentLink.classList.add('active'); 441 matched = true; 442 } 443 } 444 } 445 } 446 447 // ÐÑем Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑий пÑÐ½ÐºÑ Ð¼ÐµÐ½Ñ 2 ÑÑÐ¾Ð²Ð½Ñ 448 // ÐоÑмализÑем path: ÑбиÑаем trailing slash Ð´Ð»Ñ ÑÑÐ°Ð²Ð½ÐµÐ½Ð¸Ñ 449 const normPath = path.length > 1 ? path.replace(/\/+$/, '') : path; 450 for (const sp of subMenuPaths) { 451 let isMatch = false; 452 const normSp = sp.path.length > 1 ? sp.path.replace(/\/+$/, '') : sp.path; 453 if (sp.checkSearch) { 454 isMatch = fullPath === sp.path || fullPath.startsWith(sp.path); 455 } else if (sp.exact) { 456 isMatch = normPath === normSp && !search.includes('sort='); 457 } else { 458 isMatch = normPath === normSp || normPath.startsWith(normSp + '/'); 459 } 460 461 if (isMatch) { 462 const subLink = document.querySelector('.sidenav .drop-menu a[href="' + sp.href + '"]'); 463 if (subLink) { 464 subLink.classList.add('active'); 465 break; 466 } 467 } 468 } 469 470 // ÐÑли ниÑего не Ñовпало и Ð¼Ñ Ð½Ð° главной 471 if (!matched && (path === '/' || path === '')) { 472 const homeLink = document.querySelector('.sidenav .navbar-nav a[href="/"]'); 473 if (homeLink) { 474 const navItem = homeLink.closest('.nav-item'); 475 if (navItem) navItem.classList.add('active'); 476 } 477 } 478 } 479 480 highlightActiveMenuItem(); 481 482 // ------------------------------ 483 // 3) ÐÑÑÐ¿Ð¿Ñ Ð¼ÐµÐ½Ñ Ð² ÑайдбаÑе (dropdown) 484 // ------------------------------ 485 // РеализÑем «ÑаÑкÑÑваÑÑиеÑÑ» пÑнкÑÑ Ð¼ÐµÐ½Ñ (напÑÐ¸Ð¼ÐµÑ "Фонд", "ÐедиаÑека") 486 // ÑеÑез клаÑÑÑ Ð¸ ÐºÐ½Ð¾Ð¿ÐºÑ Ñо ÑÑÑелкой. 487 // 488 // ТÑебованиÑ: 489 // - веÑÑ Ð½Ð¸Ð¹ пÑÐ½ÐºÑ Ð¾ÑÑаÑÑÑÑ ÑÑÑлкой 490 // - ÑаÑкÑÑÑие/ÑвоÑаÑивание ÑолÑко по ÐºÐ»Ð¸ÐºÑ Ð½Ð° ÑÑÑÐµÐ»ÐºÑ 491 // - на невÑÑÐ¾ÐºÐ¸Ñ ÑкÑÐ°Ð½Ð°Ñ (по вÑÑоÑе) по ÑмолÑÐ°Ð½Ð¸Ñ ÑвÑÑнÑÑо 492 // - на обÑÑнÑÑ /вÑÑÐ¾ÐºÐ¸Ñ Ð¿Ð¾ ÑмолÑÐ°Ð½Ð¸Ñ ÑаÑкÑÑÑо 493 // 494 // ÐонÑÑÐ°ÐºÑ ÑазмеÑки: 495 // - конÑÐµÐ¹Ð½ÐµÑ Ð³ÑÑппÑ: .js-sidebar-group (li) 496 // - кнопка пеÑеклÑÑениÑ: .sidebar-group__toggle 497 // - клаÑÑ ÑоÑÑоÑниÑ: .is-open 498 // - подменÑ: .drop-menu 499 function initSidebarGroups() { 500 const groups = document.querySelectorAll('.js-sidebar-group'); 501 if (!groups || groups.length === 0) return; 502 503 // Ðнопка "РазвеÑнÑÑÑ/СвеÑнÑÑÑ Ð²ÑÑ" 504 const expandCollapseBtn = document.getElementById('sidebar-expand-collapse-all'); 505 const expandCollapseIcon = document.getElementById('sidebar-expand-collapse-all-icon'); 506 507 // ÐÑовеÑка: вÑе ли гÑÑÐ¿Ð¿Ñ ÑаÑкÑÑÑÑ 508 const areAllOpen = () => Array.from(groups).every(g => g.classList.contains('is-open')); 509 // ÐÑовеÑка: вÑе ли гÑÑÐ¿Ð¿Ñ ÑвеÑнÑÑÑ 510 const areAllClosed = () => Array.from(groups).every(g => !g.classList.contains('is-open')); 511 512 // ÐбновиÑÑ Ð¸ÐºÐ¾Ð½ÐºÑ/ÑекÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¸ 513 const updateExpandCollapseBtn = () => { 514 if (!expandCollapseBtn || !expandCollapseIcon) return; 515 if (areAllOpen()) { 516 expandCollapseIcon.classList.remove('fa-chevron-down'); 517 expandCollapseIcon.classList.add('fa-chevron-up'); 518 } else { 519 expandCollapseIcon.classList.remove('fa-chevron-up'); 520 expandCollapseIcon.classList.add('fa-chevron-down'); 521 } 522 }; 523 524 // ÐаÑÑовое ÑаÑкÑÑÑие/ÑвоÑаÑивание 525 const setAllGroups = open => { 526 const storedState = readStoredState() || {}; 527 groups.forEach((group, index) => {
528 group.classList.toggle('is-open', open); 529 setToggleState(group); 530 const groupId = getGroupId(group, index); 531 storedState[groupId] = open; 532 }); 533 writeStoredState(storedState); 534 updateExpandCollapseBtn(); 535 }; 536 537 if (expandCollapseBtn) { 538 expandCollapseBtn.addEventListener('click', () => { 539 if (areAllOpen()) { 540 setAllGroups(false); 541 } else { 542 setAllGroups(true); 543 } 544 }); 545 } 546 547 const storageKey = 'sidebarGroups'; 548 549 const readStoredState = () => { 550 try { 551 const raw = localStorage.getItem(storageKey); 552 if (!raw) return null; 553 const parsed = JSON.parse(raw); 554 if (!parsed || typeof parsed !== 'object') return null; 555 return parsed; 556 } catch { 557 return null; 558 } 559 }; 560 561 const writeStoredState = state => { 562 try { 563 localStorage.setItem(storageKey, JSON.stringify(state)); 564 } catch { 565 // ignore (private mode/quota/etc) 566 } 567 }; 568 569 const getGroupId = (group, index) => 570 (group.dataset && group.dataset.sidebarGroup) || `idx:${index}`; 571 572 // "ÐевÑÑокие" ÑкÑанÑ: оÑиенÑиÑÑемÑÑ Ð¸Ð¼ÐµÐ½Ð½Ð¾ на вÑÑоÑÑ (не ÑиÑинÑ). 573 // ÐоÑог 800px ÑоглаÑован Ñ Ð¿Ñавилами ÑайдбаÑа в CSS (max-height: 800px). 574 const lowHeightThreshold = 800; 575 576 // ÐÑоÑÑавлÑÐµÑ aria-expanded в завиÑимоÑÑи Ð¾Ñ Ð½Ð°Ð»Ð¸ÑÐ¸Ñ ÐºÐ»Ð°ÑÑа .is-open. 577 // СÑÑелка анимиÑÑеÑÑÑ ÑеÑез CSS, не нÑжно менÑÑÑ Ð¸ÐºÐ¾Ð½ÐºÑ Ð²ÑÑÑнÑÑ. 578 const setToggleState = group => { 579 const btn = group.querySelector('.sidebar-group__toggle'); 580 if (!btn) return; 581 582 const isOpen = group.classList.contains('is-open'); 583 btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 584 }; 585 586 // ÐаÑалÑное ÑоÑÑоÑние (один Ñаз пÑи загÑÑзке ÑÑÑаниÑÑ). 587 // Ðажно: Ð¼Ñ Ð½Ðµ «пеÑеигÑÑваем» ÑÑо ÑоÑÑоÑние пÑи ÑеÑайзе, 588 // ÑÑÐ¾Ð±Ñ Ð¿Ð¾Ð»ÑзоваÑÐµÐ»Ñ Ð½Ðµ ÑеÑÑл Ñвой вÑÐ±Ð¾Ñ Ð²Ð¾ вÑÐµÐ¼Ñ Ð¿ÑоÑмоÑÑа. 589 const applyDefaultState = () => { 590 const isLowHeight = window.innerHeight <= lowHeightThreshold; 591 592 const storedState = readStoredState(); 593 groups.forEach((group, index) => { 594 const groupId = getGroupId(group, index); 595 const hasStoredValue = 596 storedState && Object.prototype.hasOwnProperty.call(storedState, groupId); 597 598 // ÐÑиоÑиÑеÑ: еÑли ÑоÑÑоÑние ÑÐ¾Ñ Ñанено в localStorage â пÑименÑем его, 599 // в Ñом ÑиÑле на мобилÑнÑÑ /Ð½Ð¸Ð·ÐºÐ¸Ñ ÑкÑÐ°Ð½Ð°Ñ . 600 if (hasStoredValue) { 601 const shouldBeOpen = Boolean(storedState[groupId]); 602 group.classList.toggle('is-open', shouldBeOpen); 603 } else if (isLowHeight) { 604 group.classList.remove('is-open'); 605 } else { 606 group.classList.add('is-open'); 607 } 608 609 setToggleState(group); 610 }); 611 }; 612 613 applyDefaultState(); 614 615 // ÐоÑле пÑÐ¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ ÑоÑÑоÑÐ½Ð¸Ñ â обновиÑÑ ÐºÐ½Ð¾Ð¿ÐºÑ 616 updateExpandCollapseBtn(); 617 618 // ÐоÑле Ñого как Ð¼Ñ Ð¿Ñименили деÑолÑ/ÑÐ¾Ñ ÑанÑнное ÑоÑÑоÑние, 619 // пеÑеклÑÑаем CSS из "pre-init" Ñежима в обÑÑнÑй (по .is-open). 620 // ÐÑо ÑбиÑÐ°ÐµÑ Ð²Ð¸Ð·ÑалÑнÑе ÑкаÑки на пеÑвой оÑÑиÑовке. 621 document.documentElement.classList.add('sidebar-groups-init'); 622 623 groups.forEach((group, index) => { 624 const btn = group.querySelector('.sidebar-group__toggle'); 625 if (!btn) return; 626 627 btn.addEventListener('click', e => { 628 e.preventDefault(); 629 e.stopPropagation(); 630 group.classList.toggle('is-open'); 631 setToggleState(group); 632 633 const storedState = readStoredState() || {}; 634 const groupId = getGroupId(group, index); 635 storedState[groupId] = group.classList.contains('is-open'); 636 writeStoredState(storedState); 637 638 updateExpandCollapseBtn(); 639 }); 640 641 // Ensure correct state on initial render 642 setToggleState(group); 643 }); 644 } 645 646 initSidebarGroups(); 647 648 // ------------------------------ 649 // 3.1) ÐÑпадаÑÑие гÑÑÐ¿Ð¿Ñ ÐºÐ°Ñалога медиаÑеки 650 // ------------------------------ 651 // ÐналогиÑÐ½Ð°Ñ Ð»Ð¾Ð³Ð¸ÐºÐ° Ð´Ð»Ñ ÐºÐ°Ñалога в пÑавом ÑайдбаÑе медиаÑеки. 652 // РазмеÑка: 653 // - конÑÐµÐ¹Ð½ÐµÑ Ð³ÑÑппÑ: .js-media-catalog-group 654 // - кнопка пеÑеклÑÑениÑ: .media-catalog-group__toggle 655 // - клаÑÑ ÑоÑÑоÑниÑ: .is-open 656 // - подменÑ: .media-catalog-drop-menu 657 function initMediaCatalogGroups() { 658 const groups = document.querySelectorAll('.js-media-catalog-group'); 659 if (!groups || groups.length === 0) return; 660 661 const setToggleState = group => { 662 const btn = group.querySelector('.media-catalog-group__toggle'); 663 if (!btn) return; 664 665 const isOpen = group.classList.contains('is-open'); 666 btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 667 }; 668 669 // РаÑкÑÑваем пÑÑÑ Ð´Ð¾ акÑивной каÑегоÑии 670 // ÐÐ°Ñ Ð¾Ð´Ð¸Ð¼ акÑивнÑÑ ÑÑÑÐ»ÐºÑ Ð¸ поднимаемÑÑ Ð²Ð²ÐµÑÑ Ð¿Ð¾ DOM, ÑаÑкÑÑÐ²Ð°Ñ Ð²Ñе ÑодиÑелÑÑкие гÑÑÐ¿Ð¿Ñ 671 const activeLink = document.querySelector( 672 '.media-catalog-tree .media-catalog-link.active', 673 ); 674 if (activeLink) { 675 let parent = activeLink.parentElement; 676 while (parent && !parent.classList.contains('media-catalog-tree')) { 677 if (parent.classList.contains('js-media-catalog-group')) { 678 parent.classList.add('is-open'); 679 } 680 parent = parent.parentElement; 681 } 682 } 683 684 // ÐбновлÑем ÑоÑÑоÑние вÑÐµÑ ÐºÐ½Ð¾Ð¿Ð¾Ðº 685 groups.forEach(group => { 686 setToggleState(group); 687 }); 688 689 groups.forEach(group => { 690 const btn = group.querySelector('.media-catalog-group__toggle'); 691 if (!btn) return; 692 693 btn.addEventListener('click', e => { 694 e.preventDefault(); 695 e.stopPropagation(); 696 group.classList.toggle('is-open'); 697 setToggleState(group); 698 }); 699 700 setToggleState(group); 701 }); 702 } 703 704 initMediaCatalogGroups(); 705 706 // ------------------------------ 707 // 3.2) ÐоÑизонÑалÑное Ð¼ÐµÐ½Ñ ÐºÐ°Ñалога медиаÑеки 708 // ------------------------------ 709 // ÐÐ¾Ð´Ð¼ÐµÐ½Ñ Ð¾ÑкÑÑваеÑÑÑ Ð¿Ñи hover на ÑÑÑÐµÐ»ÐºÑ (desktop) или ÐºÐ»Ð¸ÐºÑ (mobile). 710 function initHorizontalCatalogGroups() { 711 const groups = document.querySelectorAll('.js-h-catalog-group'); 712 if (!groups || groups.length === 0) return; 713 714 const setToggleState = group => { 715 const btn = group.querySelector('.h-catalog-group__toggle'); 716 if (!btn) return; 717 const isOpen = group.classList.contains('is-open'); 718 btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 719 }; 720 721 const closeGroup = group => { 722 group.classList.remove('is-open'); 723 setToggleState(group); 724 }; 725 726 const openGroup = group => { 727 group.classList.add('is-open'); 728 setToggleState(group); 729 }; 730 731 // ÐакÑÑÑÑ ÑоÑедние гÑÑÐ¿Ð¿Ñ Ð½Ð° Ñом же ÑÑовне 732 const closeSiblings = group => { 733 const parent = group.parentElement; 734 if (!parent) return; 735 groups.forEach(g => { 736 if (g !== group && g.parentElement === parent) { 737 closeGroup(g); 738 } 739 }); 740 }; 741 742 groups.forEach(group => { 743 setToggleState(group); 744 const toggle = group.querySelector(':scope > .h-catalog-group__header > .h-catalog-group__toggle'); 745 const dropdown = group.querySelector(':scope > .h-catalog-drop-menu'); 746 if (!toggle) return; 747 748 // Ðлик по ÑÑÑелке (оÑновной обÑабоÑÑик Ð´Ð»Ñ mobile/touch) 749 toggle.addEventListener('click', e => { 750 e.preventDefault(); 751 e.stopPropagation(); 752 closeSiblings(group); 753 group.classList.toggle('is-open'); 754 setToggleState(group); 755 }); 756 757 // Hover по ÑÑÑелке (desktop) â оÑкÑÑÐ²Ð°ÐµÑ Ð¿Ð¾Ð´Ð¼ÐµÐ½Ñ, закÑÑÐ²Ð°ÐµÑ Ð¿Ñи ÑÑ Ð¾Ð´Ðµ мÑÑи 758 if (dropdown && window.matchMedia('(hover: hover)').matches) { 759 let closeTimer; 760 761 const scheduleClose = () => { 762 closeTimer = setTimeout(() => closeGroup(group), 150); 763 }; 764 const cancelClose = () => clearTimeout(closeTimer); 765 766 toggle.addEventListener('mouseenter', () => { 767 cancelClose(); 768 closeSiblings(group); 769 openGroup(group); 770 }); 771 toggle.addEventListener('mouseleave', scheduleClose); 772 773 dropdown.addEventListener('mouseenter', cancelClose); 774 dropdown.addEventListener('mouseleave', scheduleClose); 775 } 776 }); 777 778 // ÐакÑÑÑие по ÐºÐ»Ð¸ÐºÑ Ð²Ð½Ðµ каÑалога 779 document.addEventListener('click', e => { 780 if (!e.target.closest('.h-catalog-nav')) { 781 groups.forEach(group => { 782 closeGroup(group); 783 }); 784 } 785 }); 786 } 787 788 initHorizontalCatalogGroups(); 789 790 // ------------------------------ 791 // 3.2.1) РаÑкÑÑваÑÑиеÑÑ Ð¿Ð¾Ð´ÐºÐ°ÑегоÑии в ÑайдбаÑе каÑалога 792 // ------------------------------ 793 function initSidebarCatalogExpandable() { 794 const expandables = document.querySelectorAll('.js-sidebar-catalog-expandable'); 795 if (!expandables.length) return; 796
797 expandables.forEach(item => { 798 const toggle = item.querySelector('.sidebar-catalog-expandable__toggle'); 799 if (!toggle) return; 800 801 toggle.addEventListener('click', e => { 802 e.preventDefault(); 803 e.stopPropagation(); 804 item.classList.toggle('is-open'); 805 }); 806 }); 807 } 808 809 initSidebarCatalogExpandable(); 810 811 // ------------------------------ 812 // 3.3) Ðнопка "ÐоказаÑÑ/СкÑÑÑÑ ÐºÐ°Ñалог" Ð´Ð»Ñ Ð¼Ð¾Ð±Ð¸Ð»ÑнÑÑ 813 // ------------------------------ 814 (function initMobileCatalogToggle() { 815 // ÐÐ°Ñ Ð¾Ð´Ð¸Ð¼ вÑе кнопки и каÑалоги 816 document.querySelectorAll('.js-mobile-catalog-btn').forEach(function(btn) { 817 btn.addEventListener('click', function() { 818 var catalog = btn.parentElement.querySelector('.js-mobile-catalog-content'); 819 if (!catalog) return; 820 821 var isOpen = catalog.style.display !== 'none'; 822 catalog.style.display = isOpen ? 'none' : 'block'; 823 btn.setAttribute('aria-expanded', !isOpen); 824 825 // ÐенÑем ÑекÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¸ 826 var textEl = btn.querySelector('.js-mobile-catalog-btn-text'); 827 if (textEl) { 828 textEl.textContent = isOpen ? 'ÐоказаÑÑ ÐºÐ°Ñалог' : 'СкÑÑÑÑ ÐºÐ°Ñалог'; 829 } 830 831 // ÐовоÑаÑиваем ÑÑÑÐµÐ»ÐºÑ 832 var arrow = btn.querySelector('.js-mobile-catalog-btn-arrow'); 833 if (arrow) { 834 arrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)'; 835 } 836 }); 837 }); 838 })(); 839 840 // ------------------------------ 841 // 3.4) Ðнопка оÑкÑÑÑÐ¸Ñ Ð¿Ñавого ÑайдбаÑа из подÑказки каÑалога 842 // ------------------------------ 843 function initOpenRightSidebarHint() { 844 const hintBtns = document.querySelectorAll('.js-open-right-sidebar'); 845 if (!hintBtns.length) return; 846 847 hintBtns.forEach(btn => { 848 btn.addEventListener('click', () => { 849 // ТÑиггеÑим клик на ÐºÐ½Ð¾Ð¿ÐºÑ Ð¿Ñавого ÑайдбаÑа 850 const rightNavToggle = document.querySelector('.right-nav-toggle'); 851 if (rightNavToggle) { 852 rightNavToggle.click(); 853 } 854 }); 855 }); 856 } 857 858 initOpenRightSidebarHint(); 859 860 // ------------------------------ 861 // 4) ÐодÑказки поиÑка (autocomplete) 862 // ------------------------------ 863 // РеализаÑÐ¸Ñ Ð²ÑнеÑена в оÑделÑнÑй Ñайл search-autocomplete.js 864 // ÐодклÑÑаеÑÑÑ Ð¾ÑделÑнÑм <script> Ñегом Ð´Ð»Ñ Ð¼Ð¾Ð´ÑлÑноÑÑи 865 // ÐдеÑÑ ÑолÑко иниÑиализаÑÐ¸Ñ ÐµÑли модÑÐ»Ñ Ð·Ð°Ð³ÑÑжен 866 if (typeof window.initSearchAutocomplete === 'function') { 867 window.initSearchAutocomplete(); 868 } else { 869 // Fallback: вÑÑÑÐ¾ÐµÐ½Ð½Ð°Ñ Ð¸Ð½Ð¸ÑиализаÑÐ¸Ñ ÐµÑли модÑÐ»Ñ Ð½Ðµ загÑÑжен 870 initSearchAutocompleteInline(); 871 } 872 873 /** 874 * ÐÑÑÑÐ¾ÐµÐ½Ð½Ð°Ñ ÑеализаÑÐ¸Ñ Ð¿Ð¾Ð¸ÑковÑÑ Ð¿Ð¾Ð´Ñказок (legacy fallback). 875 * ÐÑполÑзÑеÑÑÑ ÐµÑли search-autocomplete.js не подклÑÑÑн. 876 * 877 * @see search-autocomplete.js Ð´Ð»Ñ Ð¿Ð¾Ð´Ñобной докÑменÑаÑии 878 */ 879 function initSearchAutocompleteInline() { 880 const form = document.querySelector('form.search-form'); 881 if (!form) return; 882 883 const MIN_QUERY_LENGTH = 3; 884 const SUGGEST_DEBOUNCE_MS = 350; 885 886 const input = form.querySelector('input[name="q"]'); 887 const menu = form.querySelector('.search-suggestions'); 888 if (!input || !menu) return; 889 890 // ÐеÑеноÑим Ð¼ÐµÐ½Ñ Ð² <body> ÑÑÐ¾Ð±Ñ Ð¾Ð½Ð¾ вÑÑло из stacking context ÑайдбаÑа 891 if (!form.classList.contains('mobile-search-form')) { 892 document.body.appendChild(menu); 893 } 894 895 let debounceTimer = null; 896 let abortController = null; 897 898 // СловаÑи Ð´Ð»Ñ Ñипов конÑенÑа 899 const typeLabels = { 900 category: 'ÐаÑалог', 901 author: 'ÐвÑоÑÑ', 902 work: 'ÐÑоизведениÑ', 903 audio: 'ÐÑдио', 904 video: 'Ðидео', 905 book: 'Ðниги', 906 music: 'ÐÑзÑка', 907 lecture: 'ÐекÑии', 908 recipient: 'ÐÑждаÑÑиеÑÑ', 909 fond_news: 'ÐовоÑÑи Ñонда', 910 blog_post: 'Ðлог', 911 blog_author: 'ÐвÑоÑÑ Ð±Ð»Ð¾Ð³Ð°', 912 }; 913 const typeIcons = { 914 category: 'fa-layer-group', 915 work: 'fa-folder-open', 916 audio: 'fa-headphones', 917 video: 'fa-play', 918 book: 'fa-book', 919 music: 'fa-music', 920 lecture: 'fa-chalkboard-teacher', 921 author: 'fa-user', 922 recipient: 'fa-hand-holding-heart', 923 fond_news: 'fa-newspaper', 924 blog_post: 'fa-blog', 925 blog_author: 'fa-pen-fancy', 926 }; 927 const typeOrder = ['category', 'recipient', 'fond_news', 'author', 'work', 'audio', 'video', 'book', 'music', 'lecture', 'blog_author', 'blog_post']; 928 929 // ÐозиÑиониÑÑем Ð¼ÐµÐ½Ñ Ð¾ÑноÑиÑелÑно input 930 const positionMenu = () => { 931 const rect = input.getBoundingClientRect(); 932 const isMobile = window.innerWidth <= 767; 933 934 if (isMobile) { 935 // Ðа мобилÑном показÑваем ниже input 936 menu.style.top = (rect.bottom + 8) + 'px'; 937 menu.style.left = '12px'; 938 menu.style.right = '12px'; 939 menu.style.width = 'auto';
940 menu.style.maxHeight = `calc(100vh - ${rect.bottom + 20}px)`; 941 } else { 942 // Ðа деÑкÑопе показÑваем ниже input 943 menu.style.top = rect.bottom + 6 + 'px'; 944 menu.style.left = rect.left + 'px'; 945 menu.style.right = ''; 946 menu.style.maxHeight = ''; 947 } 948 }; 949 950 const hide = () => { 951 menu.classList.remove('show'); 952 menu.innerHTML = ''; 953 }; 954 955 const escapeHtml = str => 956 String(str || '') 957 .replace(/</g, '<') 958 .replace(/>/g, '>'); 959 960 const getSuggestionItemClasses = r => { 961 const classes = ['search-suggestion-item']; 962 if (r.thumbUrl) classes.push('has-thumb'); 963 if (r.isInactiveRecipient) classes.push('search-suggestion-item--recipient-inactive'); 964 if (r.isDeceasedRecipient) classes.push('search-suggestion-item--recipient-deceased'); 965 return classes.join(' '); 966 }; 967 968 const renderSuggestionStatus = r => 969 r.recipientStatusLabel 970 ? `<span class="search-suggestion-status${r.isDeceasedRecipient ? ' search-suggestion-status--recipient-deceased' : ''}">${escapeHtml(r.recipientStatusLabel)}</span>` 971 : ''; 972 973 const render = items => { 974 if (!items || items.length === 0) { 975 hide(); 976 return; 977 } 978 979 // ÐÑÑппиÑÑем по ÑÐ¸Ð¿Ñ 980 const grouped = {}; 981 items.forEach(r => { 982 if (!r.href) return; 983 const type = r.type || 'other'; 984 if (!grouped[type]) grouped[type] = []; 985 grouped[type].push(r); 986 }); 987 988 // СоÑÑиÑÑем ÑÐ¸Ð¿Ñ Ð¿Ð¾ поÑÑÐ´ÐºÑ Ð¸ ÑилÑÑÑÑем пÑÑÑÑе 989 const sortedTypes = typeOrder.filter(t => grouped[t] && grouped[t].length > 0); 990 // ÐобавлÑем ÑипÑ, коÑоÑÑÑ Ð½ÐµÑ Ð² typeOrder 991 Object.keys(grouped).forEach(t => { 992 if (!sortedTypes.includes(t)) sortedTypes.push(t); 993 }); 994 995 if (sortedTypes.length === 0) { 996 hide(); 997 return; 998 } 999 1000 let html = '<div class="search-suggestions-grid">'; 1001 sortedTypes.forEach(type => { 1002 const label = typeLabels[type] || type; 1003 const icon = typeIcons[type] || 'fa-file'; 1004 const typeItems = grouped[type]; 1005 1006 html += `<div class="search-suggestion-column"> 1007 <div class="search-suggestion-header"> 1008 <i class="fas ${icon}"></i> 1009 <span>${escapeHtml(label)}</span> 1010 </div> 1011 <div class="search-suggestion-list">`; 1012
1013 typeItems.slice(0, 5).forEach(r => { 1014 const title = r.title.length > 80 1015 ? escapeHtml(r.title.slice(0, 80)) + 'â¦' 1016 : escapeHtml(r.title); 1017 const desc = r.description 1018 ? escapeHtml(r.description).slice(0, 80) + 1019 (r.description.length > 80 ? 'â¦' : '') 1020 : ''; 1021 const thumb = r.thumbUrl ? `<img class="search-suggestion-thumb" src="${r.thumbUrl}" alt="" loading="lazy" />` : ''; 1022 const status = renderSuggestionStatus(r); 1023 html += `<a class="${getSuggestionItemClasses(r)}" href="${r.href}"> 1024 ${thumb} 1025 <div class="search-suggestion-content"> 1026 <span class="search-suggestion-title">${title}</span> 1027 ${status} 1028 ${desc ? `<span class="search-suggestion-desc">${desc}</span>` : ''} 1029 </div> 1030 </a>`; 1031 }); 1032 1033 html += '</div></div>'; 1034 }); 1035 html += '</div>'; 1036 1037 // ÐобавлÑем ÑÑÑÐ»ÐºÑ "ÐÑе ÑезÑлÑÑаÑÑ" Ð²Ð½Ð¸Ð·Ñ 1038 const currentQuery = encodeURIComponent(input.value.trim()); 1039 html += `<div class="search-suggestions-footer"> 1040 <a class="search-suggestions-all-link" href="/search?q=${currentQuery}"> 1041 <i class="fas fa-search"></i> 1042 <span>ÐÑе ÑезÑлÑÑаÑÑ</span> 1043 <i class="fas fa-arrow-right"></i> 1044 </a> 1045 </div>`; 1046 1047 menu.innerHTML = html; 1048 1049 // ÐинамиÑеÑÐºÐ°Ñ ÑиÑина: ~180px на ÐºÐ¾Ð»Ð¾Ð½ÐºÑ + padding 1050 const colCount = sortedTypes.length; 1051 const colWidth = 180; 1052 const calculatedWidth = colCount * colWidth + 32; // 32px на padding 1053 menu.style.width = Math.max(calculatedWidth, 220) + 'px'; 1054 menu.style.maxWidth = '90vw'; 1055 1056 positionMenu(); 1057 menu.classList.add('show'); 1058 }; 1059 1060 async function fetchSuggestions() { 1061 const q = (input.value || '').trim(); 1062 if (q.length < MIN_QUERY_LENGTH) { 1063 hide(); 1064 return; 1065 } 1066 1067 if (abortController) abortController.abort(); 1068 abortController = new AbortController(); 1069 1070 try { 1071 const url = `/search/suggest?q=${encodeURIComponent(q)}`; 1072 const resp = await fetch(url, { 1073 method: 'GET', 1074 headers: { Accept: 'application/json' }, 1075 signal: abortController.signal, 1076 }); 1077 if (!resp.ok) { 1078 hide(); 1079 return; 1080 } 1081 1082 const data = await resp.json(); 1083 render((data && data.results) || []); 1084 } catch (e) { 1085 // abort или ÑеÑевÑе оÑибки â пÑоÑÑо пÑÑÑем 1086 hide(); 1087 } 1088 } 1089 1090 function scheduleFetch() { 1091 if (debounceTimer) window.clearTimeout(debounceTimer); 1092 debounceTimer = window.setTimeout(fetchSuggestions, SUGGEST_DEBOUNCE_MS); 1093 } 1094 1095 input.addEventListener('input', scheduleFetch); 1096 input.addEventListener('focus', scheduleFetch); 1097 1098 document.addEventListener('click', e => { 1099 const target = e.target; 1100 if (target && (form.contains(target) || menu.contains(target))) return; 1101 hide(); 1102 }); 1103 1104 input.addEventListener('keydown', e => { 1105 if (e.key === 'Escape') hide(); 1106 }); 1107 } 1108 1109 // ÐниÑиализиÑÑем вÑÑÑоеннÑÑ Ð²ÐµÑÑÐ¸Ñ ÐµÑли вÑзвана 1110 // (вÑзÑваеÑÑÑ ÑолÑко еÑли search-autocomplete.js не загÑÑжен) 1111 1112 // ------------------------------ 1113 // 5) ÐалидаÑÐ¸Ñ ÑоÑм 1114 // ------------------------------ 1115 // ÐÑо ÑÑандаÑÑнÑй паÑÑеÑн Bootstrap: еÑли ÑоÑма Ð¸Ð¼ÐµÐµÑ ÐºÐ»Ð°ÑÑ 1116 // .needs-validation, Ñо пÑи submit Ð¼Ñ Ð½Ðµ оÑпÑавлÑем еÑ, 1117 // пока бÑаÑзеÑÐ½Ð°Ñ checkValidity() не пÑойдÑÑ. 1118 // 1119 // ÐонÑÑакÑ: 1120 // - ÑоÑмÑ: .needs-validation 1121 // - пÑи попÑÑке submit добавлÑем клаÑÑ .was-validated, 1122 // ÑÑÐ¾Ð±Ñ CSS/Bootstrap подÑвеÑили оÑибки. 1123 const forms = document.getElementsByClassName('needs-validation');
1124 Array.from(forms).forEach(function (form) { 1125 form.addEventListener( 1126 'submit', 1127 function (event) { 1128 if (!form.checkValidity()) { 1129 event.preventDefault(); 1130 event.stopPropagation(); 1131 } 1132 form.classList.add('was-validated'); 1133 }, 1134 false, 1135 ); 1136 }); 1137}); 1138 1139function toggleTheme() { 1140 // ------------------------------ 1141 // 6) ÐеÑеклÑÑение ÑÐµÐ¼Ñ (light/dark) 1142 // ------------------------------ 1143 // ÐоддеÑживаем как "новÑÑ" ÐºÐ½Ð¾Ð¿ÐºÑ #theme-toggle, 1144 // Ñак и запаÑной ваÑÐ¸Ð°Ð½Ñ button.switch. 1145 // 1146 // СоÑÑоÑние Ñ ÑаниÑÑÑ Ð² localStorage('theme') и пÑименÑеÑÑÑ Ðº: 1147 // - document.documentElement.style.colorScheme 1148 // - аÑÑибÑÑÑ data-theme на <html> 1149 // - клаÑÑÑ is-active (нÑжен Ð´Ð»Ñ Ð½ÐµÐºÐ¾ÑоÑÑÑ CSS-ÑÑÑекÑов) 1150 const htmlElement = document.documentElement; 1151 const switchButton = 1152 document.getElementById('theme-toggle') || 1153 document.querySelector('button.switch'); 1154 const currentScheme = 1155 htmlElement.getAttribute('data-theme') || 1156 htmlElement.style.colorScheme || 1157 'light'; 1158 const nextScheme = currentScheme === 'light' ? 'dark' : 'light'; 1159 htmlElement.style.colorScheme = nextScheme; 1160 htmlElement.setAttribute('data-theme', nextScheme); 1161 localStorage.setItem('theme', nextScheme); 1162 if (nextScheme === 'dark') { 1163 htmlElement.classList.add('is-active'); 1164 if (switchButton) switchButton.classList.add('is-active'); 1165 } else { 1166 htmlElement.classList.remove('is-active'); 1167 if (switchButton) switchButton.classList.remove('is-active'); 1168 } 1169} 1170 1171// ------------------------------ 1172// 7) ÐодпиÑка на ÑобÑÑÐ¸Ñ Ð¿ÐµÑеклÑÑÐµÐ½Ð¸Ñ ÑÐµÐ¼Ñ 1173// ------------------------------ 1174// ÐÑедпоÑиÑаем пÑÑмÑÑ ÐºÐ½Ð¾Ð¿ÐºÑ Ð¿Ð¾ id, но оÑÑавлÑем делегиÑованнÑй ваÑÐ¸Ð°Ð½Ñ 1175// Ð´Ð»Ñ ÑовмеÑÑимоÑÑи/ÑобаÑÑноÑÑи. 1176const themeToggleButton = document.getElementById('theme-toggle'); 1177if (themeToggleButton) { 1178 themeToggleButton.addEventListener('click', () => toggleTheme()); 1179} else { 1180 // Delegated listener: any click on a button with class `switch` will toggle theme 1181 document.addEventListener('click', e => { 1182 const btn = e.target.closest && e.target.closest('button.switch'); 1183 if (btn) toggleTheme(); 1184 }); 1185} 1186 1187// ------------------------------ 1188// 8) Ð¡Ð¸Ð½Ñ ÑонизаÑÐ¸Ñ UI кнопки ÑÐµÐ¼Ñ Ð¿Ñи загÑÑзке 1189// ------------------------------ 1190// Тема Ñже пÑименена инлайн-ÑкÑипÑом в <head> (views/layout/header.pug) 1191// ÑÑÐ¾Ð±Ñ Ð¸Ð·Ð±ÐµÐ¶Ð°ÑÑ Ð¼Ð¸Ð³Ð°Ð½Ð¸Ñ (FOUC). ÐдеÑÑ ÑолÑко ÑÐ¸Ð½Ñ ÑонизиÑÑем ÑоÑÑоÑние кнопки. 1192// ÐÑли Ñема не ÑÐ¾Ñ Ñанена â авÑовÑÐ±Ð¾Ñ Ð¿Ð¾ вÑемени: 21:00-07:00 ÑÑмнаÑ, 07:00-21:00 ÑвеÑÐ»Ð°Ñ 1193document.addEventListener('DOMContentLoaded', () => { 1194 const storedTheme = localStorage.getItem('theme'); 1195 const switchButton = 1196 document.getElementById('theme-toggle') || 1197 document.querySelector('button.switch'); 1198 1199 // ÐпÑеделÑем акÑивнÑÑ ÑÐµÐ¼Ñ (ÑÐ¾Ñ ÑанÑÐ½Ð½Ð°Ñ Ð¸Ð»Ð¸ ÑвеÑÐ»Ð°Ñ Ð¿Ð¾ ÑмолÑаниÑ) 1200 // ÐвÑо-ÑÑÐ¼Ð½Ð°Ñ Ñема по вÑемени ÑÑÑок оÑклÑÑена. 1201 // РанÑÑе: const hour = new Date().getHours(); 1202 // activeTheme = hour >= 21 || hour < 7 ? 'dark' : 'light'; 1203 let activeTheme = storedTheme; 1204 if (activeTheme !== 'dark' && activeTheme !== 'light') { 1205 activeTheme = 'light'; 1206 } 1207 1208 if (activeTheme === 'dark') { 1209 if (switchButton) switchButton.classList.add('is-active'); 1210 } else { 1211 if (switchButton) switchButton.classList.remove('is-active'); 1212 } 1213}); 1214//('click', ({ currentTarget }) => currentTarget.classList.toggle('is-active')) 1215 1216// ------------------------------ 1217// ÐазнаÑение гÑадиенÑов Ñамок кнопкам Ð¼ÐµÐ½Ñ (без пеÑемеÑÐ¸Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÑÑдка) 1218// ------------------------------ 1219document.addEventListener('DOMContentLoaded', function () { 1220 const nav = document.querySelector('.feed-nav'); 1221 if (!nav) return; 1222 1223 const buttons = Array.from(nav.querySelectorAll('.feed-nav__btn')); 1224 if (buttons.length === 0) return; 1225 1226 // ÐÑадиенÑÑ Ð´Ð»Ñ Ñамок 1227 const gradients = [ 1228 'linear-gradient(135deg, #667eea, #764ba2)', 1229 'linear-gradient(135deg, #f093fb, #f5576c)', 1230 'linear-gradient(135deg, #4facfe, #00f2fe)', 1231 'linear-gradient(135deg, #43e97b, #38f9d7)', 1232 'linear-gradient(135deg, #fa709a, #fee140)', 1233 'linear-gradient(135deg, #a8edea, #fed6e3)', 1234 'linear-gradient(135deg, #ff9a9e, #fecfef)', 1235 'linear-gradient(135deg, #ffecd2, #fcb69f)', 1236 'linear-gradient(135deg, #a1c4fd, #c2e9fb)',
1237 'linear-gradient(135deg, #d299c2, #fef9d7)', 1238 'linear-gradient(135deg, #89f7fe, #66a6ff)', 1239 'linear-gradient(135deg, #fddb92, #d1fdff)', 1240 ]; 1241 1242 // ÐазнаÑаем гÑадиенÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ°Ð¼ (без пеÑемеÑиваниÑ) 1243 buttons.forEach(function (btn, index) { 1244 btn.style.setProperty( 1245 '--btn-gradient', 1246 gradients[index % gradients.length], 1247 ); 1248 }); 1249}); 1250 1251// ------------------------------ 1252// ÐбÑабоÑка загÑÑзки изобÑажений каÑÑоÑек 1253// ------------------------------ 1254document.addEventListener('DOMContentLoaded', function () { 1255 // ------------------------------ 1256 // ÐоказÑваем каÑÑинки поÑле Ð¸Ñ Ð·Ð°Ð³ÑÑзки (ÑÑÐ¾Ð±Ñ Ð½Ðµ мигала Ñамка) 1257 // ------------------------------ 1258 document 1259 .querySelectorAll('.feed-card__image:not(.feed-card__image--placeholder)') 1260 .forEach(function (img) { 1261 if (img.complete && img.naturalWidth > 0) { 1262 // ÐаÑÑинка Ñже загÑÑжена (из кÑÑа) 1263 img.classList.add('is-loaded'); 1264 } else { 1265 img.addEventListener('load', function () { 1266 img.classList.add('is-loaded'); 1267 }); 1268 } 1269 }); 1270 1271 // ------------------------------ 1272 // ÐÑли каÑÑинка не загÑÑзилаÑÑ â подÑÑавлÑем плейÑÑ Ð¾Ð»Ð´ÐµÑ 1273 // (error не вÑплÑваеÑ, поÑÑÐ¾Ð¼Ñ Ð¸ÑполÑзÑем capture=true) 1274 // ------------------------------ 1275 document.addEventListener( 1276 'error', 1277 function (e) { 1278 const target = e.target; 1279 if (!(target instanceof HTMLImageElement)) return; 1280 1281 const wrapper = target.closest('.feed-card-wrapper'); 1282 if (!wrapper) return; 1283 1284 wrapper.classList.add('feed-card-wrapper--placeholder'); 1285 1286 if (target.classList.contains('feed-card__image')) { 1287 // ÐодÑÑавлÑем плейÑÑ Ð¾Ð»Ð´ÐµÑ-каÑÑÐ¸Ð½ÐºÑ Ð±ÐµÐ· кÑÑглой Ñамки 1288 const imageWrapper = target.closest('.feed-card__image-wrapper'); 1289 if (imageWrapper) { 1290 imageWrapper.classList.add('feed-card__image-wrapper--placeholder'); 1291 imageWrapper.innerHTML = 1292 '<img class="feed-card__image feed-card__image--placeholder" src="/images/placeholder.png" alt="ÐзобÑажение">'; 1293 } 1294 // Также обновлÑем клаÑÑ ÐºÐ°ÑÑоÑки 1295 const card = wrapper.querySelector('.feed-card'); 1296 if (card) { 1297 card.classList.add('feed-card--placeholder'); 1298 } 1299 } 1300 }, 1301 true, 1302 ); 1303}); 1304 1305// ========================================================================== 1306// Separation dropdown toggle (ÑазделÑннÑе пожеÑÑвованиÑ) 1307// ========================================================================== 1308 1309// ÐкÑивнÑй плаваÑÑий dropdown (вÑÑавлен в body, ÑÑÐ¾Ð±Ñ Ð¾Ð±Ð¾Ð¹Ñи stacking context / mask-image) 1310// ÐÑполÑзÑеÑÑÑ Ð¢ÐÐЬÐÐ Ð´Ð»Ñ .donation-tag-wrapper (гоÑизонÑалÑÐ½Ð°Ñ Ð»ÐµÐ½Ñа). 1311// ÐÐ»Ñ .separation-inline (ÑÑÑаниÑа donations) dropdown показÑваеÑÑÑ Ð¾Ð±ÑÑнÑм CSS. 1312var activeSepDropdown = null; 1313 1314function closeSeparationDropdown() { 1315 if (activeSepDropdown) { 1316 activeSepDropdown.remove(); 1317 activeSepDropdown = null; 1318 } 1319 document.querySelectorAll('.separation-open').forEach(function (el) { 1320 el.classList.remove('separation-open'); 1321 }); 1322} 1323 1324function openFloatingDropdown(container) { 1325 var srcDropdown = container.querySelector('.separation-dropdown'); 1326 if (!srcDropdown) return; 1327 1328 // ÐлониÑÑем dropdown в body 1329 var clone = srcDropdown.cloneNode(true); 1330 clone.style.display = 'block'; 1331 clone.style.position = 'fixed'; 1332 clone.style.zIndex = '9999'; 1333 clone.classList.add('separation-dropdown--floating'); 1334 document.body.appendChild(clone); 1335 activeSepDropdown = clone; 1336 1337 // ÐозиÑиониÑÑем 1338 var toggle = container.querySelector('.js-separation-toggle') || container; 1339 var rect = toggle.getBoundingClientRect(); 1340 var left = rect.left + rect.width / 2 - clone.offsetWidth / 2; 1341 if (left < 8) left = 8; 1342 if (left + clone.offsetWidth > window.innerWidth - 8) { 1343 left = window.innerWidth - clone.offsetWidth - 8; 1344 } 1345 clone.style.top = (rect.bottom + 4) + 'px'; 1346 clone.style.left = left + 'px'; 1347 1348 container.classList.add('separation-open'); 1349} 1350 1351// ÐакÑÑваем floating dropdown пÑи ÑкÑолле (но не Ð¾Ñ Ð°Ð²ÑоÑкÑолла ленÑÑ) 1352document.addEventListener('scroll', function (e) { 1353 if (!activeSepDropdown) return; 1354 if (e.target && e.target.classList && e.target.classList.contains('donation-tags-scroll')) return; 1355 closeSeparationDropdown(); 1356}, true); 1357 1358document.addEventListener('click', function (e) { 1359 // Ðлик внÑÑÑи плаваÑÑего dropdown â не закÑÑваем 1360 if (activeSepDropdown && activeSepDropdown.contains(e.target)) return; 1361 1362 // Ðлик внÑÑÑи обÑÑного dropdown (separation-inline) â не закÑÑваем 1363 if (e.target.closest('.separation-dropdown')) return; 1364 1365 var toggle = e.target.closest('.js-separation-toggle'); 1366 1367 if (toggle) { 1368 e.preventDefault(); 1369 e.stopPropagation(); 1370 1371 var container = 1372 toggle.closest('.donation-tag-wrapper') || 1373 toggle.closest('.separation-inline'); 1374 if (!container) return; 1375 1376 var wasOpen = container.classList.contains('separation-open'); 1377 closeSeparationDropdown(); 1378 1379 if (!wasOpen) { 1380 // .donation-tag-wrapper â иÑполÑзÑем floating clone (Ð¾Ð±Ñ Ð¾Ð´ mask-image) 1381 // .separation-inline â пÑоÑÑо toggle клаÑÑа, CSS Ð¿Ð¾ÐºÐ°Ð¶ÐµÑ dropdown 1382 if (container.classList.contains('donation-tag-wrapper')) { 1383 openFloatingDropdown(container); 1384 } else { 1385 container.classList.add('separation-open'); 1386 } 1387 } 1388 return; 1389 } 1390 1391 // Ðлик ÑнаÑÑжи â закÑÑваем вÑе 1392 closeSeparationDropdown(); 1393}); 1394 1395// ========================================================================== 1396// Sidebar: обÑабоÑÑик вÑÑ Ð¾Ð´Ð° из аккаÑнÑа 1397// (РендеÑинг блока авÑоÑизаÑии ÑепеÑÑ Ð²ÑполнÑеÑÑÑ Ð½Ð° ÑеÑвеÑе ÑеÑез Pug SSR) 1398// ========================================================================== 1399 1400(function () { 1401 function buildSidebarAuthHtml(user) { 1402 if (user && user.email) { 1403 return '' 1404 + '<div class="sidebar-auth__row">' 1405 + '<a class="sidebar-auth__link" href="/profile">' 1406 + '<i class="fa-solid fa-user-circle sidebar-auth__icon"></i>' 1407 + 'ÐиÑнÑй кабинеÑ' 1408 + '</a>' 1409 + '<a class="sidebar-auth__link sidebar-auth__link--logout js-sidebar-logout" href="#" role="button" title="ÐÑйÑи">' 1410 + '<i class="fa-solid fa-arrow-right-from-bracket fa-flip-horizontal"></i>' 1411 + '</a>' 1412 + '</div>'; 1413 } 1414 1415 return '' 1416 + '<a class="sidebar-auth__link" href="/login">' 1417 + '<i class="fa-solid fa-sign-in-alt sidebar-auth__icon"></i>' 1418 + 'ÐойÑи' 1419 + '</a>' 1420 + '<a class="sidebar-auth__link" href="/register">' 1421 + '<i class="fa-solid fa-user-plus sidebar-auth__icon"></i>' 1422 + 'РегиÑÑÑаÑиÑ' 1423 + '</a>'; 1424 } 1425
1426 function renderSidebarAuth(user) { 1427 var sidebarAuth = document.querySelector('.sidebar-auth'); 1428 if (!sidebarAuth) return; 1429 sidebarAuth.innerHTML = buildSidebarAuthHtml(user); 1430 bindSidebarLogout(); 1431 } 1432 1433 function applyAuthUserToPage(user, source) { 1434 window.__predanieAuthUser = user && user.email ? user : null; 1435 renderSidebarAuth(window.__predanieAuthUser); 1436 window.dispatchEvent(new CustomEvent('predanieAuthStateChanged', { 1437 detail: { 1438 source: source || 'unknown', 1439 user: window.__predanieAuthUser, 1440 }, 1441 })); 1442 } 1443 1444 function buildCleanLocationWithoutAuthRefresh(url) { 1445 var cleanUrl = new URL(url.toString()); 1446 cleanUrl.searchParams.delete('__auth_refresh'); 1447 return cleanUrl.pathname + cleanUrl.search + cleanUrl.hash; 1448 } 1449 1450 function bindSidebarLogout() { 1451 var logoutBtn = document.querySelector('.js-sidebar-logout'); 1452 if (!logoutBtn || logoutBtn.__predanieBound) return; 1453 logoutBtn.__predanieBound = true; 1454 1455 logoutBtn.addEventListener('click', function (e) { 1456 e.preventDefault(); 1457 fetch('/auth/logout', { 1458 method: 'POST', 1459 credentials: 'same-origin', 1460 headers: { 'Content-Type': 'application/json' }, 1461 body: '{}', 1462 }) 1463 .catch(function () {}) 1464 .then(function () { 1465 applyAuthUserToPage(null, 'sidebar-logout'); 1466 try { document.cookie = '__at=; path=/; max-age=0; SameSite=Strict'; } catch (ex) {} 1467 window.location.href = '/'; 1468 }); 1469 }); 1470 } 1471 1472 function syncAuthStateAfterRedirect() { 1473 var url = new URL(window.location.href); 1474 if (!url.searchParams.has('__auth_refresh')) { 1475 return; 1476 } 1477 1478 fetch('/auth/me', { 1479 credentials: 'same-origin', 1480 cache: 'no-store', 1481 }) 1482 .then(function (resp) { 1483 if (!resp.ok) return null; 1484 return resp.json(); 1485 }) 1486 .then(function (user) { 1487 applyAuthUserToPage(user && user.email ? user : null, 'post-auth-redirect'); 1488 }) 1489 .catch(function () {}) 1490 .then(function () { 1491 window.history.replaceState({}, document.title, buildCleanLocationWithoutAuthRefresh(url)); 1492 }); 1493 } 1494 1495 bindSidebarLogout(); 1496 syncAuthStateAfterRedirect(); 1497 window.__predanieApplyAuthUserToPage = applyAuthUserToPage; 1498})(); 1499 1500// ========================================================================== 1501// ÐобилÑнÑй header-bar: гамбÑÑÐ³ÐµÑ â левÑй ÑайдбаÑ, лÑпа â поиÑк-overlay 1502// ========================================================================== 1503(function () { 1504 // --- ÐамбÑÑÐ³ÐµÑ Ð² header-bar: оÑкÑÑÐ²Ð°ÐµÑ Ð»ÐµÐ²Ñй ÑÐ°Ð¹Ð´Ð±Ð°Ñ --- 1505 var headerHamburger = document.querySelector('.mobile-header-hamburger'); 1506 if (headerHamburger) { 1507 headerHamburger.addEventListener('click', function () { 1508 // ÐÑполÑзÑем ÑÐ¾Ñ Ð¶Ðµ .nav-toggle, ÑÑо и FAB-кнопка 1509 var navToggleFAB = document.querySelector('.mobile-nav-toggles .nav-toggle'); 1510 if (navToggleFAB) navToggleFAB.click(); 1511 }); 1512 } 1513 1514 // --- ÐÑпа â оÑкÑÑÑÑ Ð¿Ð¾Ð¸Ñк-overlay --- 1515 var searchBtn = document.querySelector('.mobile-header-search-btn'); 1516 var searchOverlay = document.querySelector('.mobile-search-overlay'); 1517 var searchCloseBtn = document.querySelector('.mobile-search-close-btn'); 1518 var searchInput = document.querySelector('.mobile-search-input'); 1519 1520 function openMobileSearch() { 1521 if (!searchOverlay) return; 1522 searchOverlay.classList.add('show'); 1523 if (searchInput) { 1524 setTimeout(function () { searchInput.focus(); }, 100); 1525 } 1526 } 1527 1528 function closeMobileSearch() { 1529 if (!searchOverlay) return; 1530 searchOverlay.classList.remove('show'); 1531 // ÐакÑÑваем подÑказки 1532 var suggestions = searchOverlay.querySelector('.search-suggestions'); 1533 if (suggestions) { 1534 suggestions.classList.remove('show'); 1535 suggestions.innerHTML = ''; 1536 } 1537 } 1538 1539 if (searchBtn) { 1540 searchBtn.addEventListener('click', openMobileSearch); 1541 } 1542 if (searchCloseBtn) { 1543 searchCloseBtn.addEventListener('click', function (e) { 1544 e.preventDefault(); 1545 closeMobileSearch(); 1546 }); 1547 } 1548 1549 // Escape закÑÑÐ²Ð°ÐµÑ Ð¿Ð¾Ð¸Ñк 1550 document.addEventListener('keydown', function (e) { 1551 if (e.key === 'Escape' && searchOverlay && searchOverlay.classList.contains('show')) { 1552 closeMobileSearch(); 1553 } 1554 }); 1555 1556 // --- Autocomplete Ð´Ð»Ñ Ð¼Ð¾Ð±Ð¸Ð»Ñного поиÑка --- 1557 if (searchInput && searchOverlay) { 1558 var mobileForm = searchOverlay.querySelector('form.search-form'); 1559 var mobileMenu = searchOverlay.querySelector('.search-suggestions'); 1560 if (mobileForm && mobileMenu) { 1561 var debounceTimer = null; 1562 var abortCtrl = null; 1563 1564 var typeLabels = { 1565 category: 'ÐаÑалог', author: 'ÐвÑоÑÑ', work: 'ÐÑоизведениÑ', 1566 audio: 'ÐÑдио', video: 'Ðидео', book: 'Ðниги', music: 'ÐÑзÑка', 1567 lecture: 'ÐекÑии', recipient: 'ÐÑждаÑÑиеÑÑ', fond_news: 'ÐовоÑÑи Ñонда', blog_post: 'Ðлог', 1568 blog_author: 'ÐвÑоÑÑ Ð±Ð»Ð¾Ð³Ð°', 1569 }; 1570 var typeIcons = { 1571 category: 'fa-layer-group', work: 'fa-folder-open', audio: 'fa-headphones', 1572 video: 'fa-play', book: 'fa-book', music: 'fa-music', 1573 lecture: 'fa-chalkboard-teacher', author: 'fa-user', 1574 recipient: 'fa-hand-holding-heart', fond_news: 'fa-newspaper', blog_post: 'fa-blog', blog_author: 'fa-pen-fancy', 1575 }; 1576 var typeOrder = ['category','recipient','fond_news','author','work','audio','video','book','music','lecture','blog_author','blog_post']; 1577 1578 function escHtml(s) { return String(s||'').replace(/</g,'<').replace(/>/g,'>'); } 1579 function getMobileSuggestionItemClasses(r) { 1580 var classes = ['search-suggestion-item']; 1581 if (r.thumbUrl) classes.push('has-thumb'); 1582 if (r.isInactiveRecipient) classes.push('search-suggestion-item--recipient-inactive'); 1583 if (r.isDeceasedRecipient) classes.push('search-suggestion-item--recipient-deceased'); 1584 return classes.join(' '); 1585 } 1586 function renderMobileSuggestionStatus(r) { 1587 return r.recipientStatusLabel 1588 ? '<span class="search-suggestion-status' + (r.isDeceasedRecipient ? ' search-suggestion-status--recipient-deceased' : '') + '">' + escHtml(r.recipientStatusLabel) + '</span>' 1589 : ''; 1590 } 1591 1592 function hideMobile() { mobileMenu.classList.remove('show'); mobileMenu.innerHTML = ''; } 1593 1594 function renderMobile(items) { 1595 if (!items || !items.length) { hideMobile(); return; }
1596 var grouped = {}; 1597 items.forEach(function(r) { 1598 if (!r.href) return; 1599 var t = r.type || 'other'; 1600 if (!grouped[t]) grouped[t] = []; 1601 grouped[t].push(r); 1602 }); 1603 var sorted = typeOrder.filter(function(t){return grouped[t]&&grouped[t].length;}); 1604 Object.keys(grouped).forEach(function(t){ if (sorted.indexOf(t)===-1) sorted.push(t); }); 1605 if (!sorted.length) { hideMobile(); return; } 1606 1607 var html = '<div class="search-suggestions-grid">'; 1608 sorted.forEach(function(type) { 1609 var label = typeLabels[type]||type, icon = typeIcons[type]||'fa-file'; 1610 html += '<div class="search-suggestion-column"><div class="search-suggestion-header"><i class="fas '+icon+'"></i><span>'+escHtml(label)+'</span></div><div class="search-suggestion-list">'; 1611 grouped[type].slice(0,5).forEach(function(r) { 1612 var title = r.title.length>80 ? escHtml(r.title.slice(0,80))+'â¦' : escHtml(r.title); 1613 var desc = r.description ? escHtml(r.description).slice(0,80)+(r.description.length>80?'â¦':'') : ''; 1614 var thumb = r.thumbUrl ? '<img class="search-suggestion-thumb" src="'+r.thumbUrl+'" alt="" loading="lazy" />' : ''; 1615 var status = renderMobileSuggestionStatus(r); 1616 html += '<a class="'+getMobileSuggestionItemClasses(r)+'" href="'+r.href+'">'+thumb+'<div class="search-suggestion-content"><span class="search-suggestion-title">'+title+'</span>'+status+(desc?'<span class="search-suggestion-desc">'+desc+'</span>':'')+'</div></a>'; 1617 }); 1618 html += '</div></div>'; 1619 }); 1620 html += '</div>'; 1621 var cq = encodeURIComponent(searchInput.value.trim()); 1622 html += '<div class="search-suggestions-footer"><a class="search-suggestions-all-link" href="/search?q='+cq+'"><i class="fas fa-search"></i><span>ÐÑе ÑезÑлÑÑаÑÑ</span><i class="fas fa-arrow-right"></i></a></div>'; 1623 mobileMenu.innerHTML = html; 1624 mobileMenu.style.width = 'auto'; 1625 mobileMenu.style.maxWidth = '100%'; 1626 mobileMenu.classList.add('show'); 1627 } 1628 1629 function fetchMobile() { 1630 var q = (searchInput.value||'').trim(); 1631 if (q.length<3) { hideMobile(); return; } 1632 if (abortCtrl) abortCtrl.abort(); 1633 abortCtrl = new AbortController(); 1634 fetch('/search/suggest?q='+encodeURIComponent(q), { 1635 method:'GET', headers:{Accept:'application/json'}, signal:abortCtrl.signal 1636 }).then(function(r){ return r.ok ? r.json() : null; }) 1637 .then(function(d){ renderMobile(d&&d.results||[]); }) 1638 .catch(function(){ hideMobile(); }); 1639 } 1640 1641 function scheduleMobile() { 1642 if (debounceTimer) clearTimeout(debounceTimer); 1643 debounceTimer = setTimeout(fetchMobile, 120); 1644 } 1645 1646 searchInput.addEventListener('input', scheduleMobile); 1647 searchInput.addEventListener('focus', scheduleMobile); 1648 searchInput.addEventListener('keydown', function(e) { if (e.key==='Escape') hideMobile(); }); 1649 } 1650 } 1651})(); 1652 1653// ========================================================================== 1654// Donation modal openers + monthly project support popup 1655// ========================================================================== 1656(function () { 1657 function runWhenReady(fn) { 1658 if (document.readyState === 'loading') { 1659 document.addEventListener('DOMContentLoaded', fn); 1660 return; 1661 } 1662 1663 fn(); 1664 } 1665 1666 function whenDonationModalReady(fn) { 1667 if (window.__fondDonationModalReady) { 1668 fn(); 1669 return; 1670 } 1671 1672 var resolved = false; 1673 1674 function finish() { 1675 if (resolved) return; 1676 resolved = true; 1677 window.removeEventListener('fondDonationModalReady', finish); 1678 fn(); 1679 } 1680 1681 window.addEventListener('fondDonationModalReady', finish, { once: true }); 1682 window.setTimeout(finish, 1500); 1683 } 1684 1685 function readTriggerValue(trigger, attrName, fallback) { 1686 if (!trigger || !trigger.getAttribute) return fallback; 1687 var value = trigger.getAttribute(attrName); 1688 return value === null || typeof value === 'undefined' ? fallback : value; 1689 } 1690 1691 function hasTriggerAttribute(trigger, attrName) { 1692 return !!(trigger && trigger.hasAttribute && trigger.hasAttribute(attrName)); 1693 } 1694 1695 function getModalLabelText(trigger) { 1696 var explicitLabel = readTriggerValue(trigger, 'data-modal-label', ''); 1697 if (explicitLabel) return explicitLabel; 1698 1699 var recipientTitle = readTriggerValue(trigger, 'data-recipient-title', ''); 1700 return recipientTitle ? 'ÐожеÑÑвоваÑÑ ' + recipientTitle : 'ÐожеÑÑвоваÑÑ'; 1701 } 1702 1703 function buildDonationRedirectUrl(trigger) { 1704 var params = new URLSearchParams(); 1705 params.set('openDonation', '1'); 1706 params.set('donationRecipientId', readTriggerValue(trigger, 'data-recipient-id', '341489')); 1707 params.set('donationModalLabel', getModalLabelText(trigger)); 1708 1709 if (hasTriggerAttribute(trigger, 'data-recipient-title')) { 1710 params.set('donationRecipientTitle', readTriggerValue(trigger, 'data-recipient-title', '')); 1711 } 1712 1713 if (hasTriggerAttribute(trigger, 'data-recipient-purpose')) { 1714 params.set('donationRecipientPurpose', readTriggerValue(trigger, 'data-recipient-purpose', '')); 1715 } 1716 1717 var openKind = readTriggerValue(trigger, 'data-open-kind', ''); 1718 if (openKind) params.set('donationOpenKind', openKind); 1719 1720 var recurringProvider = readTriggerValue(trigger, 'data-open-recurring-provider', ''); 1721 if (recurringProvider) params.set('donationRecurringProvider', recurringProvider); 1722 1723 if (readTriggerValue(trigger, 'data-is-programme', '') === 'true') { 1724 params.set('donationIsProgramme', '1'); 1725 } 1726 1727 return '/fond?' + params.toString(); 1728 } 1729 1730 function configureDonationModal(trigger) { 1731 var donationModal = document.getElementById('donationModal'); 1732 if (!donationModal) return null; 1733 1734 var donationForm = document.getElementById('donationForm'); 1735 var modalLabel = document.getElementById('donationModalLabel'); 1736 var recipientIdInput = donationForm ? donationForm.querySelector('input[name="recipientId"]') : null; 1737 var hiddenRecipientTitle = document.getElementById('hiddenRecipientTitle'); 1738 var hiddenRecipientPurpose = document.getElementById('hiddenRecipientPurpose'); 1739 var defaultRecipientId = recipientIdInput ? (recipientIdInput.getAttribute('value') || '341489') : '341489'; 1740 var defaultRecipientTitle = hiddenRecipientTitle ? (hiddenRecipientTitle.getAttribute('value') || '') : ''; 1741 var defaultRecipientPurpose = hiddenRecipientPurpose ? (hiddenRecipientPurpose.getAttribute('value') || '') : ''; 1742 1743 var recipientId = readTriggerValue(trigger, 'data-recipient-id', '341489'); 1744 var recipientTitle = hasTriggerAttribute(trigger, 'data-recipient-title') 1745 ? readTriggerValue(trigger, 'data-recipient-title', '') 1746 : defaultRecipientTitle; 1747 var recipientPurpose = hasTriggerAttribute(trigger, 'data-recipient-purpose') 1748 ? readTriggerValue(trigger, 'data-recipient-purpose', '') 1749 : defaultRecipientPurpose; 1750 var openKind = readTriggerValue(trigger, 'data-open-kind', ''); 1751 var recurringProvider = readTriggerValue(trigger, 'data-open-recurring-provider', ''); 1752 var isProgramme = readTriggerValue(trigger, 'data-is-programme', '') === 'true'; 1753 1754 if (recipientIdInput) recipientIdInput.value = recipientId || defaultRecipientId; 1755 if (hiddenRecipientTitle) hiddenRecipientTitle.value = recipientTitle; 1756 if (hiddenRecipientPurpose) hiddenRecipientPurpose.value = recipientPurpose; 1757 if (modalLabel) modalLabel.textContent = getModalLabelText(trigger); 1758 1759 if (openKind) { 1760 donationModal.setAttribute('data-open-kind', openKind); 1761 } else { 1762 donationModal.removeAttribute('data-open-kind'); 1763 } 1764 1765 if (recurringProvider) { 1766 donationModal.setAttribute('data-open-recurring-provider', recurringProvider); 1767 } else { 1768 donationModal.removeAttribute('data-open-recurring-provider'); 1769 } 1770 1771 if (isProgramme) { 1772 donationModal.setAttribute('data-is-programme', 'true'); 1773 } else { 1774 donationModal.removeAttribute('data-is-programme'); 1775 } 1776 1777 donationModal.classList.remove('is-step2'); 1778 return donationModal; 1779 } 1780 1781 function openDonationModalFromTrigger(trigger) { 1782 var donationModal = configureDonationModal(trigger); 1783 if (!donationModal || typeof bootstrap === 'undefined' || !bootstrap || !bootstrap.Modal) { 1784 window.location.href = buildDonationRedirectUrl(trigger); 1785 return; 1786 } 1787 1788 var modalInstance = bootstrap.Modal.getOrCreateInstance 1789 ? bootstrap.Modal.getOrCreateInstance(donationModal) 1790 : new bootstrap.Modal(donationModal); 1791 modalInstance.show(); 1792 } 1793 1794 window.openDonationModalFromTrigger = openDonationModalFromTrigger; 1795 1796 document.addEventListener('click', function (e) { 1797 var trigger = e.target.closest('.open-donation-modal'); 1798 if (!trigger) return; 1799 1800 e.preventDefault(); 1801 1802 var parentFab = trigger.closest('.mobile-charity-fab'); 1803 if (parentFab) parentFab.classList.remove('is-open'); 1804 1805 openDonationModalFromTrigger(trigger); 1806 }); 1807 1808 runWhenReady(function () { 1809 var params = new URLSearchParams(window.location.search); 1810 if (params.get('openDonation') !== '1') return; 1811 1812 if (params.get('predanieAppReturn') === '1') { 1813 // ÐеÑка пеÑÐµÐ¶Ð¸Ð²Ð°ÐµÑ Ð¿ÐµÑÐµÑ Ð¾Ð´ к пÑовайдеÑÑ Ð² ÑÑом же бÑаÑзеÑе. 1814 document.cookie = 'predanie_donation_app_return=1; path=/fond/payment; max-age=7200; SameSite=Lax; Secure'; 1815 } 1816 1817 window.__skipMonthlyProjectSupportPopup = true; 1818 1819 var trigger = document.createElement('button'); 1820 trigger.type = 'button'; 1821 trigger.setAttribute('data-recipient-id', params.get('donationRecipientId') || '341489'); 1822 1823 if (params.has('donationRecipientTitle')) { 1824 trigger.setAttribute('data-recipient-title', params.get('donationRecipientTitle') || ''); 1825 } 1826 1827 if (params.has('donationRecipientPurpose')) { 1828 trigger.setAttribute('data-recipient-purpose', params.get('donationRecipientPurpose') || ''); 1829 } 1830 1831 var openKind = params.get('donationOpenKind'); 1832 if (openKind) trigger.setAttribute('data-open-kind', openKind); 1833 1834 var recurringProvider = params.get('donationRecurringProvider'); 1835 if (recurringProvider) trigger.setAttribute('data-open-recurring-provider', recurringProvider); 1836 1837 var modalLabel = params.get('donationModalLabel'); 1838 if (modalLabel) trigger.setAttribute('data-modal-label', modalLabel); 1839 1840 if (params.get('donationIsProgramme') === '1') { 1841 trigger.setAttribute('data-is-programme', 'true'); 1842 } 1843 1844 whenDonationModalReady(function () { 1845 openDonationModalFromTrigger(trigger); 1846 1847 params.delete('openDonation'); 1848 params.delete('predanieAppReturn'); 1849 params.delete('donationRecipientId'); 1850 params.delete('donationRecipientTitle'); 1851 params.delete('donationRecipientPurpose'); 1852 params.delete('donationOpenKind'); 1853 params.delete('donationRecurringProvider'); 1854 params.delete('donationModalLabel'); 1855 params.delete('donationIsProgramme'); 1856 1857 var cleanUrl = window.location.pathname + (params.toString() ? '?' + params.toString() : '') + window.location.hash; 1858 window.history.replaceState({}, document.title, cleanUrl); 1859 }); 1860 }); 1861})(); 1862 1863(function () { 1864 var POPUP_ID = 'monthlyProjectSupportPopup'; 1865 var POPUP_LAST_SHOWN_KEY = 'predanie_monthly_support_popup_last_shown_at'; 1866 var LEGACY_POPUP_MONTH_COOKIE = 'predanie_monthly_support_popup_closed_month'; 1867 var POPUP_DELAY_MS = 120000; 1868 var BODY_CLASS = 'monthly-project-support-popup-open';
1869 var FOND_PATH_RE = /^\/fond(?:\/|$)/; 1870 1871 function getCookie(name) { 1872 var escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 1873 var match = document.cookie.match(new RegExp('(?:^|; )' + escapedName + '=([^;]*)')); 1874 return match ? decodeURIComponent(match[1]) : ''; 1875 } 1876 1877 function setCookie(name, value) { 1878 document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; max-age=34560000; SameSite=Lax'; 1879 } 1880 1881 function getCurrentMonthStamp() { 1882 var now = new Date(); 1883 var month = String(now.getMonth() + 1).padStart(2, '0'); 1884 return String(now.getFullYear()) + '-' + month; 1885 } 1886 1887 function isFondPath() { 1888 return FOND_PATH_RE.test(window.location.pathname || ''); 1889 } 1890 1891 function wasPopupShownRecently() { 1892 var gate = window.predaniePromoPopupGate; 1893 if (gate && gate.wasShownRecently(POPUP_LAST_SHOWN_KEY)) return true; 1894 1895 // СовмеÑÑимоÑÑÑ Ñ Ð¾ÑмеÑкой, коÑоÑÑÑ ÑÑавила пÑедÑдÑÑÐ°Ñ Ð²ÐµÑÑÐ¸Ñ Ð¿Ð¾Ð¿Ð°Ð¿Ð°. 1896 return getCookie(LEGACY_POPUP_MONTH_COOKIE) === getCurrentMonthStamp(); 1897 } 1898 1899 function isSiteBannerPopupVisible() { 1900 var overlay = document.getElementById('popup-banner-overlay'); 1901 if (!overlay) return false; 1902 1903 return document.documentElement.getAttribute('data-popup-banner-hidden') !== 'true'; 1904 } 1905 1906 function hasAnotherPromoPopup() { 1907 return Boolean(document.getElementById('push-popup-offer')); 1908 } 1909 1910 function initMonthlyProjectSupportPopup() { 1911 var popup = document.getElementById(POPUP_ID); 1912 if (!popup) return; 1913 if (popup.dataset.monthlySupportPopupInitialized === '1') return; 1914 1915 popup.dataset.monthlySupportPopupInitialized = '1'; 1916 1917 var dialog = popup.querySelector('.monthly-project-support-popup__dialog'); 1918 var closeBtn = popup.querySelector('.monthly-project-support-popup__close'); 1919 var ctaBtn = popup.querySelector('.monthly-project-support-popup__cta'); 1920 var isVisible = false; 1921 var showTimer = null; 1922 var timerStartedAt = 0; 1923 var remainingDelayMs = POPUP_DELAY_MS; 1924 1925 function shouldSkipPopup() { 1926 if (isFondPath()) return true; 1927 if (window.__skipMonthlyProjectSupportPopup) return true; 1928 if (wasPopupShownRecently()) return true; 1929 if (document.querySelector('.modal.show') || document.body.classList.contains('modal-open')) return true; 1930 if (isSiteBannerPopupVisible()) return true; 1931 if (hasAnotherPromoPopup()) return true; 1932 1933 return false; 1934 } 1935 1936 function claimPopupDisplay() { 1937 var gate = window.predaniePromoPopupGate; 1938 1939 if (gate && !gate.claim(POPUP_LAST_SHOWN_KEY)) return false; 1940 1941 // СÑаÑÑÑ cookie пÑодолжаем обновлÑÑÑ, ÑÑÐ¾Ð±Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ° Ñо ÑÑаÑÑм кеÑиÑованнÑм 1942 // script.js Ñоже не показала попап повÑоÑно. 1943 setCookie(LEGACY_POPUP_MONTH_COOKIE, getCurrentMonthStamp()); 1944 return true; 1945 } 1946 1947 function hidePopup() { 1948 if (!isVisible) return; 1949 isVisible = false; 1950 popup.classList.remove('is-visible'); 1951 popup.setAttribute('aria-hidden', 'true'); 1952 popup.hidden = true; 1953 document.body.classList.remove(BODY_CLASS); 1954 } 1955 1956 function pauseShowTimer() { 1957 if (!showTimer) return; 1958 1959 window.clearTimeout(showTimer); 1960 showTimer = null; 1961 1962 if (timerStartedAt) { 1963 remainingDelayMs = Math.max(0, remainingDelayMs - (Date.now() - timerStartedAt)); 1964 timerStartedAt = 0; 1965 } 1966 } 1967 1968 function showPopup() { 1969 if (shouldSkipPopup()) return; 1970 if (!claimPopupDisplay()) return; 1971 isVisible = true; 1972 popup.hidden = false; 1973 popup.setAttribute('aria-hidden', 'false'); 1974 document.body.classList.add(BODY_CLASS); 1975 1976 window.requestAnimationFrame(function () { 1977 popup.classList.add('is-visible'); 1978 }); 1979 } 1980 1981 function scheduleShowPopup() { 1982 if (isVisible || showTimer) return; 1983 if (shouldSkipPopup()) return; 1984 if (document.visibilityState === 'hidden') return; 1985 1986 timerStartedAt = Date.now(); 1987 showTimer = window.setTimeout(function () { 1988 showTimer = null; 1989 timerStartedAt = 0; 1990 remainingDelayMs = 0; 1991 showPopup(); 1992 }, remainingDelayMs); 1993 } 1994 1995 scheduleShowPopup(); 1996 1997 if (closeBtn) { 1998 closeBtn.addEventListener('click', function () { 1999 hidePopup(); 2000 }); 2001 } 2002 2003 if (ctaBtn) { 2004 ctaBtn.addEventListener('click', function () { 2005 hidePopup(); 2006 }); 2007 } 2008 2009 popup.addEventListener('click', function (e) { 2010 if (dialog && dialog.contains(e.target)) return; 2011 hidePopup(); 2012 }); 2013 2014 document.addEventListener('keydown', function (e) { 2015 if (e.key === 'Escape' && isVisible) { 2016 hidePopup(); 2017 } 2018 }); 2019 2020 document.addEventListener('visibilitychange', function () { 2021 if (document.visibilityState === 'hidden') { 2022 pauseShowTimer(); 2023 return; 2024 } 2025 2026 scheduleShowPopup(); 2027 }); 2028 2029 // ÐÑли ÑÐ¾Ñ Ð¶Ðµ попап показалÑÑ Ð² дÑÑгой вкладке, ÑекÑÑÐ°Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ° ÑÑÐ°Ð·Ñ 2030 // закÑÑÐ²Ð°ÐµÑ Ñвой ÑкземплÑÑ. 2031 window.addEventListener('storage', function (event) { 2032 if (event.key === POPUP_LAST_SHOWN_KEY) hidePopup(); 2033 }); 2034 } 2035 2036 if (document.readyState === 'loading') { 2037 document.addEventListener('DOMContentLoaded', initMonthlyProjectSupportPopup); 2038 } else { 2039 initMonthlyProjectSupportPopup(); 2040 } 2041})(); 2042 2043// ========================================================================== 2044// FAB-Ð¼ÐµÐ½Ñ Ð±Ð»Ð°Ð³Ð¾ÑвоÑиÑелÑноÑÑи (ÑаÑкÑÑÑие/ÑкÑÑÑие) 2045// ========================================================================== 2046(function () { 2047 var fabs = document.querySelectorAll('.mobile-charity-fab'); 2048 if (!fabs.length) return; 2049
2050 fabs.forEach(function (fab) { 2051 var trigger = fab.querySelector('.mobile-charity-fab__trigger'); 2052 var overlay = fab.querySelector('.mobile-charity-fab__overlay'); 2053 var isDirectMode = fab.getAttribute('data-charity-fab-mode') === 'direct'; 2054 2055 function toggleFab() { 2056 fab.classList.toggle('is-open'); 2057 } 2058 2059 function closeFab() { 2060 fab.classList.remove('is-open'); 2061 } 2062 2063 if (trigger && !isDirectMode) { 2064 trigger.addEventListener('click', function (e) { 2065 e.stopPropagation(); 2066 // ÐакÑÑваем дÑÑгие оÑкÑÑÑÑе FAB 2067 fabs.forEach(function (other) { 2068 if (other !== fab) other.classList.remove('is-open'); 2069 }); 2070 toggleFab(); 2071 }); 2072 } 2073 2074 if (overlay) { 2075 overlay.addEventListener('click', function () { 2076 closeFab(); 2077 }); 2078 } 2079 2080 // ÐакÑÑваем пÑи нажаÑии на пÑÐ½ÐºÑ Â«ÐомоÑÑ» 2081 var donateBtn = fab.querySelector('.mobile-charity-fab__item--donate'); 2082 if (donateBtn) { 2083 donateBtn.addEventListener('click', function () { 2084 closeFab(); 2085 }); 2086 } 2087 2088 // ÐакÑÑваем пÑи нажаÑии на «ÐлагоÑвоÑиÑелÑноÑÑÑ» 2089 var fondLink = fab.querySelector('.mobile-charity-fab__item--fond'); 2090 if (fondLink) { 2091 fondLink.addEventListener('click', function () { 2092 closeFab(); 2093 }); 2094 } 2095 }); 2096})();
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.