1/** 2 * MAIN JAVASCRIPT 3 * Core functionality for the Paleophilatelie.eu website 4 */ 5 6(function() { 7 'use strict'; 8 9 /** 10 * Initialize when DOM is ready 11 */ 12 function init() { 13 initStickyHeader(); 14 initToolbarSticky(); 15 initSmoothScroll(); 16 initImageLazyLoad(); 17 initMobileMenu(); 18 initYearAutoTOC(); 19 markEmptyGalleryCells(); 20 } 21 22 /** 23 * Sticky Header - Add shadow when scrolled 24 */ 25 function initStickyHeader() { 26 const header = document.querySelector('.site-header'); 27 if (!header) return; 28 29 let lastScroll = 0; 30 31 window.addEventListener('scroll', () => { 32 const currentScroll = window.pageYOffset; 33 34 if (currentScroll > 10) { 35 header.classList.add('site-header--scrolled'); 36 } else { 37 header.classList.remove('site-header--scrolled'); 38 } 39 40 // Hide/show header on scroll direction (optional) 41 if (currentScroll > lastScroll && currentScroll > 100) { 42 header.classList.add('site-header--hidden'); 43 } else { 44 header.classList.remove('site-header--hidden'); 45 } 46 47 lastScroll = currentScroll; 48 }, { passive: true }); 49 } 50 51 /** 52 * Toolbar sticky toggle - pin/unpin the search & translate bar 53 */ 54 function initToolbarSticky() { 55 const toolbar = document.querySelector('.header-toolbar'); 56 const checkbox = document.getElementById('toolbar-sticky-checkbox'); 57 const headerTop = document.querySelector('.header-top'); 58 if (!toolbar || !checkbox) { 59 // No toolbar (e.g., page loaded inside iframe) â set offset based on header height or 0 60 var h = headerTop ? headerTop.offsetHeight : 0; 61 document.documentElement.style.setProperty('--header-top-height', h + 'px'); 62 document.documentElement.style.setProperty('--az-nav-top', h + 'px'); 63 var azNav = document.querySelector('.az-navigation'); 64 var azNavH = azNav ? azNav.offsetHeight : 0; 65 document.documentElement.style.setProperty('--table-header-top', (h + azNavH) + 'px'); 66 return; 67 } 68 69 // Measure header-top height and set CSS variable 70 function updateHeaderHeight() { 71 if (headerTop) { 72 const h = headerTop.offsetHeight; 73 document.documentElement.style.setProperty('--header-top-height', h + 'px'); 74 } 75 } 76 77 // Update --az-nav-top so sticky A-Z nav sits below pinned toolbar 78 // Also update --table-header-top for sticky table headers below the A-Z nav 79 function updateAzNavTop(isSticky) { 80 var headerH = headerTop ? headerTop.offsetHeight : 64; 81 var azNavTop; 82 if (isSticky) { 83 var toolbarH = toolbar.offsetHeight; 84 azNavTop = headerH + toolbarH; 85 } else { 86 azNavTop = headerH; 87 } 88 document.documentElement.style.setProperty('--az-nav-top', azNavTop + 'px'); 89 var azNav = document.querySelector('.az-navigation'); 90 var azNavH = azNav ? azNav.offsetHeight : 0; 91 document.documentElement.style.setProperty('--table-header-top', (azNavTop + azNavH) + 'px'); 92 } 93 94 updateHeaderHeight(); 95 window.addEventListener('resize', function() { 96 updateHeaderHeight(); 97 updateAzNavTop(checkbox.checked); 98 }, { passive: true }); 99 100 // Apply saved preference (default: pinned on desktop, unpinned on mobile) 101 var isMobile = window.matchMedia('(max-width: 991px)').matches; 102 var saved = !isMobile; 103 try { 104 var stored = localStorage.getItem('toolbarSticky'); 105 if (stored !== null) saved = stored === 'true'; 106 } catch(e) {} 107 if (saved) { 108 toolbar.classList.add('header-toolbar--sticky'); 109 checkbox.checked = true; 110 } 111 updateAzNavTop(saved); 112 113 // Toggle handler 114 checkbox.addEventListener('change', function() { 115 if (this.checked) { 116 updateHeaderHeight(); 117 toolbar.classList.add('header-toolbar--sticky'); 118 } else { 119 toolbar.classList.remove('header-toolbar--sticky'); 120 } 121 updateAzNavTop(this.checked); 122 try { localStorage.setItem('toolbarSticky', this.checked); } catch(e) {} 123 }); 124 } 125 126 /** 127 * Smooth scroll for anchor links 128 */ 129 function initSmoothScroll() { 130 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 131 anchor.addEventListener('click', function(e) { 132 const targetId = this.getAttribute('href'); 133 if (targetId === '#') return; 134 135 const target = document.querySelector(targetId) || document.querySelector('a[name="' + CSS.escape(targetId.substring(1)) + '"]'); 136 if (target) { 137 e.preventDefault(); 138 // Measure actual sticky header height (black bar + toolbar if sticky) 139 var headerTop = document.querySelector('.header-top'); 140 var toolbar = document.querySelector('.header-toolbar'); 141 var stickyHeight = 0; 142 if (headerTop) stickyHeight += headerTop.offsetHeight; 143 if (toolbar && toolbar.classList.contains('header-toolbar--sticky')) { 144 stickyHeight += toolbar.offsetHeight; 145 } 146 var scrollOffset = stickyHeight > 0 ? stickyHeight + 20 : 180; 147 var top = target.getBoundingClientRect().top + window.scrollY - scrollOffset; 148 window.scrollTo({ top: top, behavior: 'smooth' });
149 // Update URL hash so the anchor appears in the address bar 150 history.pushState(null, '', targetId); 151 } 152 }); 153 }); 154 } 155 156 /** 157 * Lazy load images with native loading="lazy" fallback 158 */ 159 function initImageLazyLoad() { 160 // Check for native lazy loading support 161 if ('loading' in HTMLImageElement.prototype) { 162 // Native lazy loading supported 163 document.querySelectorAll('img[data-src]').forEach(img => { 164 img.src = img.dataset.src; 165 img.loading = 'lazy'; 166 }); 167 } else { 168 // Fallback with Intersection Observer 169 const imageObserver = new IntersectionObserver((entries, observer) => { 170 entries.forEach(entry => { 171 if (entry.isIntersecting) { 172 const img = entry.target; 173 img.src = img.dataset.src; 174 img.classList.remove('lazy'); 175 observer.unobserve(img); 176 } 177 }); 178 }); 179 180 document.querySelectorAll('img[data-src]').forEach(img => { 181 imageObserver.observe(img); 182 }); 183 } 184 } 185 186 /** 187 * Mobile menu toggle 188 */ 189 function initMobileMenu() { 190 const menuToggle = document.querySelector('.menu-toggle'); 191 const nav = document.querySelector('.main-nav'); 192 193 if (!menuToggle || !nav) return; 194 195 menuToggle.addEventListener('click', () => { 196 const isOpen = nav.classList.toggle('main-nav--open'); 197 menuToggle.classList.toggle('menu-toggle--active'); 198 menuToggle.setAttribute('aria-expanded', isOpen); 199 200 // Prevent body scroll when menu is open 201 document.body.classList.toggle('menu-open', isOpen); 202 }); 203 204 // Close menu when clicking outside 205 document.addEventListener('click', (e) => { 206 if (!nav.contains(e.target) && !menuToggle.contains(e.target)) { 207 nav.classList.remove('main-nav--open'); 208 menuToggle.classList.remove('menu-toggle--active'); 209 menuToggle.setAttribute('aria-expanded', 'false'); 210 document.body.classList.remove('menu-open'); 211 } 212 }); 213 214 // Close menu on escape key 215 document.addEventListener('keydown', (e) => { 216 if (e.key === 'Escape' && nav.classList.contains('main-nav--open')) { 217 nav.classList.remove('main-nav--open'); 218 menuToggle.classList.remove('menu-toggle--active'); 219 menuToggle.setAttribute('aria-expanded', 'false'); 220 document.body.classList.remove('menu-open'); 221 } 222 }); 223 } 224 225 /** 226 * Mark whitespace-only cells in products-gallery tables as empty 227 * so CSS can skip title decoration on placeholder cells. 228 */ 229 function markEmptyGalleryCells() { 230 document.querySelectorAll('.products-gallery td').forEach(function(td) { 231 if (!td.children.length && !td.textContent.trim()) { 232 td.setAttribute('data-empty', ''); 233 } 234 }); 235 } 236 237 /** 238 * Auto-load toc-fab.js if a sidebar TOC exists on the page. 239 * toc-fab.js is now self-bootstrapping â it detects the sidebar, 240 * auto-generates TOC if needed, creates the FAB, and wires behavior. 241 */ 242 function initYearAutoTOC() { 243 var sidebar = document.querySelector('.year-sidebar, .root-sidebar, .country-sidebar'); 244 if (!sidebar) return; 245 246 // Skip if toc-fab.js is already loaded on this page (via <script> tag) 247 if (document.querySelector('script[src*="toc-fab"]')) return; 248 249 // Dynamically load toc-fab.js 250 var basePath = (document.querySelector('script[src*="main.js"]') || {}).src || ''; 251 var dir = basePath.substring(0, basePath.lastIndexOf('/') + 1); 252 var script = document.createElement('script'); 253 script.src = dir + 'toc-fab.js'; 254 document.body.appendChild(script); 255 } 256 257 /* 258 * Generic lightbox removed â every page that needs a lightbox 259 * (gallery slideshows, stamp detail pages) brings its own 260 * implementation (fdc_preview_script.js, gallery.js, etc.). 261 * Keeping a generic fallback here caused conflicts with those 262 * page-specific scripts. 263 */ 264 265 /** 266 * Copy to clipboard utility 267 */ 268 window.copyToClipboard = function(text) { 269 if (navigator.clipboard) { 270 return navigator.clipboard.writeText(text); 271 } 272 273 // Fallback for older browsers 274 const textarea = document.createElement('textarea'); 275 textarea.value = text; 276 textarea.style.position = 'fixed'; 277 textarea.style.opacity = '0'; 278 document.body.appendChild(textarea); 279 textarea.select(); 280 document.execCommand('copy'); 281 document.body.removeChild(textarea); 282 return Promise.resolve(); 283 }; 284 285 /** 286 * Debounce utility 287 */ 288 window.debounce = function(func, wait) { 289 let timeout;
290 return function executedFunction(...args) { 291 const later = () => { 292 clearTimeout(timeout); 293 func(...args); 294 }; 295 clearTimeout(timeout); 296 timeout = setTimeout(later, wait); 297 }; 298 }; 299 300 // Initialize on DOM ready 301 if (document.readyState === 'loading') { 302 document.addEventListener('DOMContentLoaded', init); 303 } else { 304 init(); 305 } 306 307 // Also initialize after components are loaded 308 document.addEventListener('componentsLoaded', init); 309})();
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.