leaflet-theme-control 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -219,19 +219,21 @@ See [examples/api.html](examples/api.html) for a complete example.
219
219
 
220
220
  ### Options
221
221
 
222
- | Option | Type | Default | Description |
223
- | ------------------- | -------- | ---------------------- | ------------------------------------------------------------------- |
224
- | `position` | String | `"topright"` | Position of the control |
225
- | `themes` | Object | `DEFAULT_THEMES` | Theme definitions |
226
- | `defaultTheme` | String | `"light"` | Initial theme |
227
- | `storageKey` | String | `"leaflet-theme"` | localStorage key |
228
- | `detectSystemTheme` | Boolean | `true` | Detect OS dark mode |
229
- | `cssSelector` | String | `".leaflet-tile-pane"` | Elements to apply filter to |
230
- | `addButton` | Boolean | `true` | Add UI button to map (set to `false` for programmatic control only) |
231
- | `enableEditor` | Boolean | `false` | Enable theme editor UI with customization sliders |
232
- | `onChange` | Function | `null` | Callback on theme change: `(themeKey, theme) => {}` |
233
- | `getLabel` | Function | `null` | Function to get translated theme labels: `(themeKey) => string` |
234
- | `getEditorLabels` | Function | `null` | Function to get translated editor UI labels: `(key) => string` |
222
+ | Option | Type | Default | Description |
223
+ | ------------------- | -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
224
+ | `position` | String | `"topright"` | Position of the control |
225
+ | `themes` | Object | `DEFAULT_THEMES` | Theme definitions |
226
+ | `defaultTheme` | String | `"light"` | Initial theme |
227
+ | `storageKey` | String | `"leaflet-theme"` | localStorage key |
228
+ | `detectSystemTheme` | Boolean | `true` | Detect OS dark mode |
229
+ | `cssSelector` | String | `".leaflet-tile-pane"` | Elements to apply filter to |
230
+ | `addButton` | Boolean | `true` | Add UI button to map (set to `false` for programmatic control only) |
231
+ | `enableEditor` | Boolean | `false` | Enable theme editor UI with customization sliders |
232
+ | `onChange` | Function | `null` | Callback on theme change AND editor changes: `(themeKey, theme) => {}` |
233
+ | `getLabel` | Function | `null` | Function to get translated theme labels: `(themeKey) => string` (optional if themes have `label` property) |
234
+ | `getEditorLabels` | Function | `null` | Function to get translated editor UI labels: `(key) => string` |
235
+ | `panelPosition` | String | `"topright"` | Position of editor panel: `"topright"`, `"topleft"`, `"bottomright"`, `"bottomleft"` |
236
+ | `panelZIndex` | Number | `1000` | Z-index for editor panel to avoid conflicts |
235
237
 
236
238
  ### Methods
237
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaflet-theme-control",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A Leaflet control for switching between visual themes (light, dark, grayscale, custom, etc.) using CSS filters",
5
5
  "main": "src/leaflet-theme-control.js",
6
6
  "module": "src/leaflet-theme-control.js",
@@ -1,7 +1,7 @@
1
1
  /* CSS Custom Properties for easy theming */
2
2
  :root {
3
- /* Primary colors */
4
- --ltc-primary: #2196f3;
3
+ /* Primary colors - fallback to Leaflet defaults if available */
4
+ --ltc-primary: var(--leaflet-control-bg-color, #2196f3);
5
5
  --ltc-primary-dark: #1976d2;
6
6
  --ltc-primary-light: #64b5f6;
7
7
  --ltc-primary-bg: #e3f2fd;
@@ -9,15 +9,15 @@
9
9
  /* Success/Custom badge */
10
10
  --ltc-success: #4caf50;
11
11
 
12
- /* Neutral colors - Light mode */
13
- --ltc-bg: white;
14
- --ltc-bg-hover: #f4f4f4;
12
+ /* Neutral colors - Light mode - with Leaflet fallbacks */
13
+ --ltc-bg: var(--leaflet-control-bg-color, white);
14
+ --ltc-bg-hover: var(--leaflet-control-hover-bg-color, #f4f4f4);
15
15
  --ltc-bg-secondary: #f5f5f5;
16
16
  --ltc-bg-tertiary: #e0e0e0;
17
- --ltc-text: #333;
17
+ --ltc-text: var(--leaflet-control-text-color, #333);
18
18
  --ltc-text-secondary: #666;
19
19
  --ltc-text-tertiary: #aaa;
20
- --ltc-border: #e0e0e0;
20
+ --ltc-border: var(--leaflet-control-border-color, #e0e0e0);
21
21
  --ltc-border-dark: #bbb;
22
22
 
23
23
  /* Component specific */
@@ -68,8 +68,6 @@
68
68
 
69
69
  .leaflet-theme-panel {
70
70
  position: absolute;
71
- top: 60px;
72
- right: 10px;
73
71
  width: 320px;
74
72
  max-width: calc(100vw - 20px);
75
73
  max-height: calc(100vh - 80px);
@@ -77,12 +75,46 @@
77
75
  background: var(--ltc-bg);
78
76
  border-radius: 8px;
79
77
  box-shadow: 0 4px 12px var(--ltc-shadow);
80
- z-index: 1000;
81
78
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
82
79
  pointer-events: all;
83
80
  color: var(--ltc-text);
84
81
  }
85
82
 
83
+ /* Position variants */
84
+ .leaflet-theme-panel[data-position="topright"],
85
+ .leaflet-theme-panel[data-position="topleft"] {
86
+ top: 60px;
87
+ }
88
+
89
+ .leaflet-theme-panel[data-position="bottomright"],
90
+ .leaflet-theme-panel[data-position="bottomleft"] {
91
+ bottom: 20px;
92
+ }
93
+
94
+ .leaflet-theme-panel[data-position="topright"],
95
+ .leaflet-theme-panel[data-position="bottomright"] {
96
+ right: 10px;
97
+ }
98
+
99
+ .leaflet-theme-panel[data-position="topleft"],
100
+ .leaflet-theme-panel[data-position="bottomleft"] {
101
+ left: 10px;
102
+ }
103
+
104
+ /* Responsive positioning for small screens */
105
+ /* Use higher specificity to override position classes without !important */
106
+ @media (max-width: 480px) {
107
+ .leaflet-theme-panel.leaflet-theme-panel {
108
+ top: 50%;
109
+ left: 50%;
110
+ right: auto;
111
+ bottom: auto;
112
+ transform: translate(-50%, -50%);
113
+ width: calc(100vw - 40px);
114
+ max-height: calc(100vh - 100px);
115
+ }
116
+ }
117
+
86
118
  /* Panel Header */
87
119
  .theme-panel-header {
88
120
  display: flex;
@@ -388,18 +420,37 @@
388
420
  /* Touch-friendly larger hit area */
389
421
  @media (hover: none) and (pointer: coarse) {
390
422
  .theme-editor-slider input[type="range"] {
391
- height: 32px;
423
+ height: 40px; /* Größere Touch-Area */
424
+ }
425
+
426
+ .theme-editor-slider input[type="range"]::-webkit-slider-runnable-track {
427
+ height: 10px; /* Größere Track */
392
428
  }
393
429
 
394
430
  .theme-editor-slider input[type="range"]::-webkit-slider-thumb {
395
- width: 28px;
396
- height: 28px;
397
- margin-top: -10px;
431
+ width: 32px; /* Größerer Thumb */
432
+ height: 32px;
433
+ margin-top: -11px;
434
+ }
435
+
436
+ .theme-editor-slider input[type="range"]::-moz-range-track {
437
+ height: 10px;
398
438
  }
399
439
 
400
440
  .theme-editor-slider input[type="range"]::-moz-range-thumb {
401
- width: 28px;
402
- height: 28px;
441
+ width: 32px;
442
+ height: 32px;
443
+ }
444
+
445
+ /* Größere Buttons für Touch */
446
+ .theme-select-btn,
447
+ .theme-edit-btn {
448
+ min-height: 48px;
449
+ }
450
+
451
+ .control-style-btn {
452
+ min-height: 44px;
453
+ font-size: 14px;
403
454
  }
404
455
  }
405
456
 
@@ -25,14 +25,26 @@ export class ThemeControl extends Control {
25
25
  onChange: null,
26
26
  getLabel: null, // Function to get translated theme labels: (themeKey) => string
27
27
  getEditorLabels: null, // Function to get translated editor UI labels: (key) => string
28
+ panelPosition: 'topright', // Position of the editor panel: 'topright', 'topleft', 'bottomright', 'bottomleft'
29
+ panelZIndex: 1000, // Z-index for editor panel
28
30
  })
29
31
  }
30
32
 
31
33
  initialize(options) {
32
34
  Util.setOptions(this, options)
33
35
 
34
- // Create a deep copy of DEFAULT_THEMES to avoid mutating the original
35
- if (!this.options.themes) {
36
+ // Create deep copies of themes to avoid mutating DEFAULT_THEMES
37
+ // This is necessary because spread operator only does shallow copies
38
+ if (this.options.themes) {
39
+ // User provided custom themes - create deep copies of each theme
40
+ const themesCopy = {}
41
+ Object.keys(this.options.themes).forEach((key) => {
42
+ themesCopy[key] = { ...this.options.themes[key] }
43
+ })
44
+ this.options.themes = themesCopy
45
+ }
46
+ else {
47
+ // No themes provided - use copies of DEFAULT_THEMES
36
48
  this.options.themes = {}
37
49
  Object.keys(DEFAULT_THEMES).forEach((key) => {
38
50
  this.options.themes[key] = { ...DEFAULT_THEMES[key] }
@@ -204,6 +216,23 @@ export class ThemeControl extends Control {
204
216
  this.root.classList.add(theme.className)
205
217
  }
206
218
 
219
+ // Also apply theme class to control container if map exists
220
+ if (this.map) {
221
+ const controlContainer = this.map.getContainer().querySelector('.leaflet-control-container')
222
+ if (controlContainer) {
223
+ // Remove all theme classes from control container
224
+ Object.values(this.options.themes).forEach((t) => {
225
+ if (t.className) {
226
+ controlContainer.classList.remove(t.className)
227
+ }
228
+ })
229
+ // Add current theme class
230
+ if (theme.className) {
231
+ controlContainer.classList.add(theme.className)
232
+ }
233
+ }
234
+ }
235
+
207
236
  // Apply CSS filter to map tiles
208
237
  const mapElements = document.querySelectorAll(this.options.cssSelector)
209
238
  mapElements.forEach((el) => {
@@ -106,6 +106,11 @@ export class ThemeEditor {
106
106
  panel.setAttribute('aria-modal', 'true')
107
107
  panel.setAttribute('aria-labelledby', 'theme-panel-title')
108
108
 
109
+ // Apply position and z-index from options
110
+ const position = this.themeControl.options.panelPosition || 'topright'
111
+ panel.setAttribute('data-position', position)
112
+ panel.style.zIndex = this.themeControl.options.panelZIndex || 1000
113
+
109
114
  // Prevent map interactions when using the panel
110
115
  DomEvent.disableClickPropagation(panel)
111
116
  DomEvent.disableScrollPropagation(panel)
@@ -201,6 +206,37 @@ export class ThemeEditor {
201
206
  document.removeEventListener('keydown', this._onKeyDown)
202
207
  }
203
208
 
209
+ _isThemeModified(themeKey) {
210
+ // Check if theme has custom filters saved
211
+ if (!this.customFilters[themeKey]) {
212
+ return false
213
+ }
214
+
215
+ // Get default theme from DEFAULT_THEMES
216
+ const defaultTheme = DEFAULT_THEMES[themeKey]
217
+ if (!defaultTheme) {
218
+ // Custom theme not in defaults - consider it modified
219
+ return true
220
+ }
221
+
222
+ // Parse default filter and compare with custom
223
+ const defaultValues = this._parseFilterString(defaultTheme.filter)
224
+ const customValues = this.customFilters[themeKey]
225
+
226
+ // Check if any filter value differs from default
227
+ const filterKeys = ['invert', 'hueRotate', 'saturate', 'brightness', 'contrast', 'sepia', 'grayscale']
228
+ const filtersModified = filterKeys.some((key) => {
229
+ const defaultVal = defaultValues[key]
230
+ const customVal = customValues[key] !== undefined ? customValues[key] : defaultVal
231
+ return Math.abs(customVal - defaultVal) > 0.01 // Use small epsilon for float comparison
232
+ })
233
+
234
+ // Check if control style differs
235
+ const controlStyleModified = customValues.controlStyle && customValues.controlStyle !== defaultTheme.controlStyle
236
+
237
+ return filtersModified || controlStyleModified
238
+ }
239
+
204
240
  cleanup() {
205
241
  // Close panel if open
206
242
  if (this.isOpen) {
@@ -273,7 +309,7 @@ export class ThemeEditor {
273
309
  }
274
310
 
275
311
  _createThemeRow(themeKey, theme, isActive) {
276
- const hasCustom = !!this.customFilters[themeKey]
312
+ const hasCustom = this._isThemeModified(themeKey)
277
313
  const themeLabel = this.themeControl._getThemeLabel(themeKey)
278
314
 
279
315
  // Create select button with icon, label and badges
@@ -321,7 +357,7 @@ export class ThemeEditor {
321
357
 
322
358
  // Theme select buttons
323
359
  const selectBtns = this.panel.querySelectorAll('.theme-select-btn')
324
- selectBtns.forEach((btn) => {
360
+ selectBtns.forEach((btn, index) => {
325
361
  DomEvent.on(btn, 'click', (e) => {
326
362
  const themeKey = e.currentTarget.dataset.theme
327
363
 
@@ -332,6 +368,20 @@ export class ThemeEditor {
332
368
  this.themeControl.setTheme(themeKey)
333
369
  this.close()
334
370
  })
371
+
372
+ // Keyboard navigation for theme buttons
373
+ DomEvent.on(btn, 'keydown', (e) => {
374
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
375
+ e.preventDefault()
376
+ const nextIndex = (index + 1) % selectBtns.length
377
+ selectBtns[nextIndex].focus()
378
+ }
379
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
380
+ e.preventDefault()
381
+ const prevIndex = (index - 1 + selectBtns.length) % selectBtns.length
382
+ selectBtns[prevIndex].focus()
383
+ }
384
+ })
335
385
  })
336
386
 
337
387
  // Theme edit buttons
@@ -352,14 +402,9 @@ export class ThemeEditor {
352
402
  // Get current filter values or defaults
353
403
  const filterValues = this.customFilters[themeKey] || this._parseFilterString(theme.filter)
354
404
 
355
- // Get control style preference
356
- let controlStyle = 'light'
357
- if (this.customFilters[themeKey]?.controlStyle) {
358
- controlStyle = this.customFilters[themeKey].controlStyle
359
- }
360
- else if (theme.controlStyle) {
361
- controlStyle = theme.controlStyle
362
- }
405
+ // Get control style preference - always start fresh from theme
406
+ // Don't check customFilters first, as it may have stale data
407
+ const controlStyle = theme.controlStyle || 'light'
363
408
 
364
409
  // Build panel structure
365
410
  const header = this._createPanelHeader(`${this._getLabel('customize')}: ${themeLabel}`, true)
@@ -532,6 +577,11 @@ export class ThemeEditor {
532
577
  if (this.themeControl.getCurrentTheme() === themeKey) {
533
578
  this.themeControl.setTheme(themeKey)
534
579
  }
580
+
581
+ // Fire onChange callback for editor changes
582
+ if (this.themeControl.options.onChange) {
583
+ this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
584
+ }
535
585
  }
536
586
 
537
587
  _resetTheme(themeKey) {
@@ -539,18 +589,37 @@ export class ThemeEditor {
539
589
  delete this.customFilters[themeKey]
540
590
  this._saveCustomFilters()
541
591
 
542
- // Restore default values from DEFAULT_THEMES
592
+ // Restore default filter and controlStyle from DEFAULT_THEMES
593
+ // but KEEP user-defined properties like applyToSelectors
543
594
  const defaultTheme = DEFAULT_THEMES[themeKey]
595
+ const currentTheme = this.themeControl.options.themes[themeKey]
544
596
 
545
- if (defaultTheme) {
546
- // Copy all properties from default theme
547
- this.themeControl.options.themes[themeKey] = { ...defaultTheme }
597
+ if (defaultTheme && currentTheme) {
598
+ // Only reset the editable properties (filter, controlStyle)
599
+ // Keep other properties like applyToSelectors, icon, label, className
600
+ currentTheme.filter = defaultTheme.filter
601
+ currentTheme.controlStyle = defaultTheme.controlStyle
602
+ }
603
+ else if (!currentTheme) {
604
+ // Theme doesn't exist - this shouldn't happen
605
+ console.warn(`Theme "${themeKey}" not found`)
606
+ return
607
+ }
608
+ else {
609
+ // For custom themes not in DEFAULT_THEMES, we can't reset
610
+ console.warn(`Theme "${themeKey}" has no default in DEFAULT_THEMES, cannot reset filter values`)
548
611
  }
549
612
 
550
- // Reapply theme (including controlStyle) BEFORE re-rendering editor
551
- // This ensures the DOM attribute is set correctly first
613
+ // Reapply theme if it's currently active
614
+ // Use setTheme to ensure proper application
552
615
  if (this.themeControl.getCurrentTheme() === themeKey) {
553
- this.themeControl._applyTheme(themeKey, false)
616
+ this.themeControl.setTheme(themeKey)
617
+ }
618
+ else {
619
+ // Fire onChange callback even if theme is not currently active
620
+ if (this.themeControl.options.onChange) {
621
+ this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
622
+ }
554
623
  }
555
624
 
556
625
  // Re-render editor panel with default values