leaflet-theme-control 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,273 @@
1
+ import { Control, DomEvent, DomUtil, Util } from 'leaflet'
2
+ import { DEFAULT_THEMES } from './leaflet-theme-control-themes.js'
3
+ import { ThemeEditor } from './leaflet-theme-editor.js'
4
+
5
+ /**
6
+ * ThemeControl - Leaflet control for switching visual themes
7
+ *
8
+ * Supports multiple themes using CSS filters:
9
+ * - Light (default)
10
+ * - Dark (inverted colors)
11
+ * - Grayscale (black & white)
12
+ * - Custom themes via options
13
+ */
14
+ export class ThemeControl extends Control {
15
+ static {
16
+ this.setDefaultOptions({
17
+ position: 'topright',
18
+ themes: null, // Will be set to a copy of DEFAULT_THEMES in initialize
19
+ defaultTheme: 'light',
20
+ storageKey: 'leaflet-theme',
21
+ detectSystemTheme: true,
22
+ cssSelector: '.leaflet-tile-pane',
23
+ addButton: true, // Add UI button to map (set to false for programmatic control only)
24
+ enableEditor: false, // Enable theme editor UI
25
+ onChange: null,
26
+ getLabel: null, // Function to get translated theme labels: (themeKey) => string
27
+ getEditorLabels: null, // Function to get translated editor UI labels: (key) => string
28
+ })
29
+ }
30
+
31
+ initialize(options) {
32
+ Util.setOptions(this, options)
33
+
34
+ // Create a deep copy of DEFAULT_THEMES to avoid mutating the original
35
+ if (!this.options.themes) {
36
+ this.options.themes = {}
37
+ Object.keys(DEFAULT_THEMES).forEach((key) => {
38
+ this.options.themes[key] = { ...DEFAULT_THEMES[key] }
39
+ })
40
+ }
41
+
42
+ this.root = document.documentElement
43
+ this.savedTheme = localStorage.getItem(this.options.storageKey)
44
+
45
+ // AbortController for automatic event cleanup
46
+ this._abortController = new AbortController()
47
+
48
+ // Setup media query listener for system theme changes
49
+ if (this.options.detectSystemTheme) {
50
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
51
+ this.prefersDark = mediaQuery.matches
52
+
53
+ mediaQuery.addEventListener('change', (e) => {
54
+ // Only auto-switch if user hasn't manually selected a theme
55
+ if (!this.savedTheme) {
56
+ this.setTheme(e.matches ? 'dark' : this.options.defaultTheme)
57
+ }
58
+ }, { signal: this._abortController.signal })
59
+ }
60
+ else {
61
+ this.prefersDark = false
62
+ }
63
+
64
+ // Initialize theme editor if enabled
65
+ if (this.options.enableEditor) {
66
+ this.editor = new ThemeEditor(this)
67
+ }
68
+
69
+ // Set initial theme
70
+ const initialTheme = this._determineInitialTheme()
71
+ this.currentTheme = initialTheme
72
+ this._applyTheme(initialTheme, false)
73
+ }
74
+
75
+ _determineInitialTheme() {
76
+ // 1. Check localStorage
77
+ if (this.savedTheme && this.options.themes[this.savedTheme]) {
78
+ return this.savedTheme
79
+ }
80
+
81
+ // 2. Check system preference (if enabled)
82
+ if (this.options.detectSystemTheme && this.prefersDark && this.options.themes.dark) {
83
+ return 'dark'
84
+ }
85
+
86
+ // 3. Use default
87
+ return this.options.defaultTheme
88
+ }
89
+
90
+ onAdd(map) {
91
+ this.map = map
92
+
93
+ // Add theme selector panel to map container if editor enabled
94
+ if (this.options.enableEditor && this.editor) {
95
+ const panel = this.editor.createPanel()
96
+ map.getContainer().appendChild(panel)
97
+ }
98
+
99
+ // Return empty container if no button should be added
100
+ if (!this.options.addButton) {
101
+ const container = DomUtil.create('div', 'leaflet-control-theme-hidden')
102
+ container.style.display = 'none'
103
+ return container
104
+ }
105
+
106
+ const container = DomUtil.create('div', 'leaflet-bar leaflet-control-theme')
107
+ const button = DomUtil.create('button', 'leaflet-control-theme-button', container)
108
+
109
+ button.type = 'button'
110
+ this._updateButton(button, this.currentTheme)
111
+
112
+ // Store button reference for updates
113
+ this.button = button
114
+
115
+ // Event listener - inline arrow function (no need to store reference)
116
+ DomEvent.on(button, 'click', (event) => {
117
+ DomEvent.stop(event)
118
+
119
+ if (this.options.enableEditor && this.editor) {
120
+ this.editor.openThemeSelector()
121
+ }
122
+ else {
123
+ this._cycleTheme()
124
+ }
125
+ })
126
+
127
+ return container
128
+ }
129
+
130
+ onRemove() {
131
+ // Abort all event listeners
132
+ this._abortController.abort()
133
+
134
+ // Cleanup editor
135
+ if (this.editor) {
136
+ this.editor.cleanup()
137
+ }
138
+
139
+ this.button = null
140
+ this.map = null
141
+ }
142
+
143
+ _updateButton(button, themeKey) {
144
+ if (!button) return // No button if addButton: false
145
+
146
+ const theme = this.options.themes[themeKey]
147
+ const label = this._getThemeLabel(themeKey)
148
+
149
+ button.setAttribute('aria-label', 'Theme: ' + label)
150
+ button.title = 'Theme: ' + label
151
+ button.textContent = theme.icon || '🎨'
152
+ }
153
+
154
+ _getThemeLabel(themeKey) {
155
+ // Use custom label function if provided
156
+ if (this.options.getLabel) {
157
+ return this.options.getLabel(themeKey)
158
+ }
159
+
160
+ // Use theme's built-in label
161
+ const theme = this.options.themes[themeKey]
162
+ return theme.label || themeKey
163
+ }
164
+
165
+ _cycleTheme() {
166
+ const themeKeys = Object.keys(this.options.themes)
167
+ const currentIndex = themeKeys.indexOf(this.currentTheme)
168
+ const nextIndex = (currentIndex + 1) % themeKeys.length
169
+ const nextTheme = themeKeys[nextIndex]
170
+
171
+ this.setTheme(nextTheme)
172
+ }
173
+
174
+ setTheme(themeKey) {
175
+ if (!this.options.themes[themeKey]) {
176
+ console.warn(`Theme "${themeKey}" not found`)
177
+ return
178
+ }
179
+
180
+ this.currentTheme = themeKey
181
+ this._applyTheme(themeKey, true)
182
+ this._updateButton(this.button, themeKey)
183
+ }
184
+
185
+ _applyTheme(themeKey, save = true) {
186
+ const theme = this.options.themes[themeKey]
187
+
188
+ // Set data-theme attribute on root
189
+ this.root.setAttribute('data-theme', themeKey)
190
+
191
+ // Set control style (light or dark controls)
192
+ const controlStyle = theme.controlStyle || 'light'
193
+ this.root.setAttribute('data-control-style', controlStyle)
194
+
195
+ // Remove all theme classes from root
196
+ Object.values(this.options.themes).forEach((t) => {
197
+ if (t.className) {
198
+ this.root.classList.remove(t.className)
199
+ }
200
+ })
201
+
202
+ // Add current theme class to root
203
+ if (theme.className) {
204
+ this.root.classList.add(theme.className)
205
+ }
206
+
207
+ // Apply CSS filter to map tiles
208
+ const mapElements = document.querySelectorAll(this.options.cssSelector)
209
+ mapElements.forEach((el) => {
210
+ if (theme.filter) {
211
+ el.style.filter = theme.filter
212
+ }
213
+ else {
214
+ el.style.filter = ''
215
+ }
216
+ })
217
+
218
+ // Collect all selectors from all themes to clear filters first
219
+ const allSelectors = new Set()
220
+ Object.values(this.options.themes).forEach((t) => {
221
+ if (t.applyToSelectors) {
222
+ const selectors = Array.isArray(t.applyToSelectors)
223
+ ? t.applyToSelectors
224
+ : [t.applyToSelectors]
225
+ selectors.forEach(s => allSelectors.add(s))
226
+ }
227
+ })
228
+
229
+ // Clear filters from all possible selectors
230
+ allSelectors.forEach((selector) => {
231
+ const elements = document.querySelectorAll(selector)
232
+ elements.forEach((el) => {
233
+ el.style.filter = ''
234
+ })
235
+ })
236
+
237
+ // Apply CSS filter to additional custom selectors (if specified in current theme)
238
+ if (theme.applyToSelectors) {
239
+ const selectors = Array.isArray(theme.applyToSelectors)
240
+ ? theme.applyToSelectors
241
+ : [theme.applyToSelectors]
242
+
243
+ selectors.forEach((selector) => {
244
+ const elements = document.querySelectorAll(selector)
245
+ elements.forEach((el) => {
246
+ if (theme.filter) {
247
+ el.style.filter = theme.filter
248
+ }
249
+ })
250
+ })
251
+ }
252
+
253
+ // Save to localStorage
254
+ if (save) {
255
+ localStorage.setItem(this.options.storageKey, themeKey)
256
+ }
257
+
258
+ // Trigger onChange callback
259
+ if (this.options.onChange) {
260
+ this.options.onChange(themeKey, theme)
261
+ }
262
+ }
263
+
264
+ getCurrentTheme() {
265
+ return this.currentTheme
266
+ }
267
+
268
+ getThemes() {
269
+ return this.options.themes
270
+ }
271
+ }
272
+
273
+ export { DEFAULT_THEMES }