/** * ThrillWiki Dark Mode Maps - Dark Mode Integration for Maps * * This module provides comprehensive dark mode support for maps, * including automatic theme switching, dark tile layers, and consistent styling */ class DarkModeMaps { constructor(options = {}) { this.options = { enableAutoThemeDetection: true, enableSystemPreference: true, enableStoredPreference: true, storageKey: 'thrillwiki_dark_mode', transitionDuration: 300, tileProviders: { light: { osm: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', cartodb: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }, dark: { osm: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', cartodb: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' } }, ...options }; this.currentTheme = 'light'; this.mapInstances = new Set(); this.tileLayers = new Map(); this.observer = null; this.mediaQuery = null; this.init(); } /** * Initialize dark mode support */ init() { this.detectCurrentTheme(); this.setupThemeObserver(); this.setupSystemPreferenceDetection(); this.setupStorageSync(); this.setupMapThemeStyles(); this.bindEventHandlers(); console.log('Dark mode maps initialized with theme:', this.currentTheme); } /** * Detect current theme from DOM */ detectCurrentTheme() { if (document.documentElement.classList.contains('dark')) { this.currentTheme = 'dark'; } else { this.currentTheme = 'light'; } // Check stored preference if (this.options.enableStoredPreference) { const stored = localStorage.getItem(this.options.storageKey); if (stored && ['light', 'dark', 'auto'].includes(stored)) { this.applyStoredPreference(stored); } } // Check system preference if auto if (this.options.enableSystemPreference && this.getStoredPreference() === 'auto') { this.applySystemPreference(); } } /** * Setup theme observer to watch for changes */ setupThemeObserver() { this.observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { const newTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; if (newTheme !== this.currentTheme) { this.handleThemeChange(newTheme); } } }); }); this.observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); } /** * Setup system preference detection */ setupSystemPreferenceDetection() { if (!this.options.enableSystemPreference) return; this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleSystemChange = (e) => { if (this.getStoredPreference() === 'auto') { const newTheme = e.matches ? 'dark' : 'light'; this.setTheme(newTheme); } }; // Modern browsers if (this.mediaQuery.addEventListener) { this.mediaQuery.addEventListener('change', handleSystemChange); } else { // Fallback for older browsers this.mediaQuery.addListener(handleSystemChange); } } /** * Setup storage synchronization */ setupStorageSync() { // Listen for storage changes from other tabs window.addEventListener('storage', (e) => { if (e.key === this.options.storageKey) { const newPreference = e.newValue; if (newPreference) { this.applyStoredPreference(newPreference); } } }); } /** * Setup map theme styles */ setupMapThemeStyles() { if (document.getElementById('dark-mode-map-styles')) return; const styles = ` `; document.head.insertAdjacentHTML('beforeend', styles); } /** * Bind event handlers */ bindEventHandlers() { // Handle theme toggle buttons const themeToggleButtons = document.querySelectorAll('[data-theme-toggle]'); themeToggleButtons.forEach(button => { button.addEventListener('click', () => { this.toggleTheme(); }); }); // Handle theme selection const themeSelectors = document.querySelectorAll('[data-theme-select]'); themeSelectors.forEach(selector => { selector.addEventListener('change', (e) => { this.setThemePreference(e.target.value); }); }); } /** * Handle theme change */ handleThemeChange(newTheme) { const oldTheme = this.currentTheme; this.currentTheme = newTheme; // Update map tile layers this.updateMapTileLayers(newTheme); // Update marker themes this.updateMarkerThemes(newTheme); // Emit theme change event const event = new CustomEvent('themeChanged', { detail: { oldTheme, newTheme, isSystemPreference: this.getStoredPreference() === 'auto' } }); document.dispatchEvent(event); console.log(`Theme changed from ${oldTheme} to ${newTheme}`); } /** * Update map tile layers for theme */ updateMapTileLayers(theme) { this.mapInstances.forEach(mapInstance => { const currentTileLayer = this.tileLayers.get(mapInstance); if (currentTileLayer) { mapInstance.removeLayer(currentTileLayer); } // Create new tile layer for theme const tileUrl = this.options.tileProviders[theme].osm; const newTileLayer = L.tileLayer(tileUrl, { attribution: '© OpenStreetMap contributors' + (theme === 'dark' ? ', © CARTO' : ''), className: `map-tiles-${theme}` }); newTileLayer.addTo(mapInstance); this.tileLayers.set(mapInstance, newTileLayer); }); } /** * Update marker themes */ updateMarkerThemes(theme) { if (window.mapMarkers) { // Clear marker caches to force re-render with new theme window.mapMarkers.clearIconCache(); window.mapMarkers.clearPopupCache(); } } /** * Toggle between light and dark themes */ toggleTheme() { const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; this.setTheme(newTheme); this.setStoredPreference(newTheme); } /** * Set specific theme */ setTheme(theme) { if (!['light', 'dark'].includes(theme)) return; if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } // Update theme toggle states this.updateThemeToggleStates(theme); } /** * Set theme preference (light, dark, auto) */ setThemePreference(preference) { if (!['light', 'dark', 'auto'].includes(preference)) return; this.setStoredPreference(preference); if (preference === 'auto') { this.applySystemPreference(); } else { this.setTheme(preference); } } /** * Apply system preference */ applySystemPreference() { if (this.mediaQuery) { const systemPrefersDark = this.mediaQuery.matches; this.setTheme(systemPrefersDark ? 'dark' : 'light'); } } /** * Apply stored preference */ applyStoredPreference(preference) { if (preference === 'auto') { this.applySystemPreference(); } else { this.setTheme(preference); } } /** * Get stored theme preference */ getStoredPreference() { return localStorage.getItem(this.options.storageKey) || 'auto'; } /** * Set stored theme preference */ setStoredPreference(preference) { localStorage.setItem(this.options.storageKey, preference); } /** * Update theme toggle button states */ updateThemeToggleStates(theme) { const toggleButtons = document.querySelectorAll('[data-theme-toggle]'); toggleButtons.forEach(button => { button.setAttribute('data-theme', theme); button.setAttribute('aria-label', `Switch to ${theme === 'light' ? 'dark' : 'light'} theme`); }); const themeSelectors = document.querySelectorAll('[data-theme-select]'); themeSelectors.forEach(selector => { selector.value = this.getStoredPreference(); }); } /** * Register map instance for theme management */ registerMapInstance(mapInstance) { this.mapInstances.add(mapInstance); // Apply current theme immediately setTimeout(() => { this.updateMapTileLayers(this.currentTheme); }, 100); } /** * Unregister map instance */ unregisterMapInstance(mapInstance) { this.mapInstances.delete(mapInstance); this.tileLayers.delete(mapInstance); } /** * Create theme toggle button */ createThemeToggle() { const toggle = document.createElement('button'); toggle.className = 'theme-toggle'; toggle.setAttribute('data-theme-toggle', 'true'); toggle.setAttribute('aria-label', 'Toggle theme'); toggle.innerHTML = ` `; toggle.addEventListener('click', () => { this.toggleTheme(); }); return toggle; } /** * Create theme selector dropdown */ createThemeSelector() { const selector = document.createElement('select'); selector.className = 'theme-selector'; selector.setAttribute('data-theme-select', 'true'); selector.innerHTML = ` `; selector.value = this.getStoredPreference(); selector.addEventListener('change', (e) => { this.setThemePreference(e.target.value); }); return selector; } /** * Get current theme */ getCurrentTheme() { return this.currentTheme; } /** * Check if dark mode is active */ isDarkMode() { return this.currentTheme === 'dark'; } /** * Check if system preference is supported */ isSystemPreferenceSupported() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').media !== 'not all'; } /** * Get system preference */ getSystemPreference() { if (this.isSystemPreferenceSupported() && this.mediaQuery) { return this.mediaQuery.matches ? 'dark' : 'light'; } return 'light'; } /** * Add theme transition classes */ addThemeTransitions() { const elements = document.querySelectorAll('.filter-chip, .park-item, .search-result-item, .popup-btn'); elements.forEach(element => { element.classList.add('theme-transition'); }); } /** * Remove theme transition classes */ removeThemeTransitions() { const elements = document.querySelectorAll('.theme-transition'); elements.forEach(element => { element.classList.remove('theme-transition'); }); } /** * Destroy dark mode instance */ destroy() { if (this.observer) { this.observer.disconnect(); } if (this.mediaQuery) { if (this.mediaQuery.removeEventListener) { this.mediaQuery.removeEventListener('change', this.applySystemPreference); } else { this.mediaQuery.removeListener(this.applySystemPreference); } } this.mapInstances.clear(); this.tileLayers.clear(); } } // Auto-initialize dark mode support document.addEventListener('DOMContentLoaded', function() { window.darkModeMaps = new DarkModeMaps(); // Register existing map instances if (window.thrillwikiMap) { window.darkModeMaps.registerMapInstance(window.thrillwikiMap); } // Add theme transitions window.darkModeMaps.addThemeTransitions(); // Add theme toggle to navigation if it doesn't exist const nav = document.querySelector('nav, .navbar, .header-nav'); if (nav && !nav.querySelector('[data-theme-toggle]')) { const themeToggle = window.darkModeMaps.createThemeToggle(); themeToggle.style.marginLeft = 'auto'; nav.appendChild(themeToggle); } }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = DarkModeMaps; } else { window.DarkModeMaps = DarkModeMaps; }