PageSourceSearch

https://www.paleophilatelie.eu/scripts/main.js

js paleophilatelie.eu collected 2026-10-02 12:28:08 UTC 11,863 bytes, 309 lines download raw bytes

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.