leaflet-theme-control 0.0.1 → 0.0.2
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 +15 -13
- package/package.json +1 -1
- package/src/leaflet-theme-control.css +67 -16
- package/src/leaflet-theme-control.js +19 -0
- package/src/leaflet-theme-editor.js +72 -2
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.
|
|
3
|
+
"version": "0.0.2",
|
|
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:
|
|
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:
|
|
396
|
-
height:
|
|
397
|
-
margin-top: -
|
|
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:
|
|
402
|
-
height:
|
|
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,6 +25,8 @@ 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
|
|
|
@@ -204,6 +206,23 @@ export class ThemeControl extends Control {
|
|
|
204
206
|
this.root.classList.add(theme.className)
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
// Also apply theme class to control container if map exists
|
|
210
|
+
if (this.map) {
|
|
211
|
+
const controlContainer = this.map.getContainer().querySelector('.leaflet-control-container')
|
|
212
|
+
if (controlContainer) {
|
|
213
|
+
// Remove all theme classes from control container
|
|
214
|
+
Object.values(this.options.themes).forEach((t) => {
|
|
215
|
+
if (t.className) {
|
|
216
|
+
controlContainer.classList.remove(t.className)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
// Add current theme class
|
|
220
|
+
if (theme.className) {
|
|
221
|
+
controlContainer.classList.add(theme.className)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
207
226
|
// Apply CSS filter to map tiles
|
|
208
227
|
const mapElements = document.querySelectorAll(this.options.cssSelector)
|
|
209
228
|
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 =
|
|
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
|
|
@@ -532,6 +582,11 @@ export class ThemeEditor {
|
|
|
532
582
|
if (this.themeControl.getCurrentTheme() === themeKey) {
|
|
533
583
|
this.themeControl.setTheme(themeKey)
|
|
534
584
|
}
|
|
585
|
+
|
|
586
|
+
// Fire onChange callback for editor changes
|
|
587
|
+
if (this.themeControl.options.onChange) {
|
|
588
|
+
this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
|
|
589
|
+
}
|
|
535
590
|
}
|
|
536
591
|
|
|
537
592
|
_resetTheme(themeKey) {
|
|
@@ -546,6 +601,16 @@ export class ThemeEditor {
|
|
|
546
601
|
// Copy all properties from default theme
|
|
547
602
|
this.themeControl.options.themes[themeKey] = { ...defaultTheme }
|
|
548
603
|
}
|
|
604
|
+
else {
|
|
605
|
+
// For custom themes not in DEFAULT_THEMES, reset to parsed filter values
|
|
606
|
+
// This handles user-defined themes that don't have a default
|
|
607
|
+
const theme = this.themeControl.options.themes[themeKey]
|
|
608
|
+
if (theme) {
|
|
609
|
+
// Keep the theme but clear any custom modifications by re-parsing
|
|
610
|
+
// the original filter (which may still be modified, so this is a no-op for custom themes)
|
|
611
|
+
console.warn(`Theme "${themeKey}" has no default in DEFAULT_THEMES, cannot fully reset`)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
549
614
|
|
|
550
615
|
// Reapply theme (including controlStyle) BEFORE re-rendering editor
|
|
551
616
|
// This ensures the DOM attribute is set correctly first
|
|
@@ -553,6 +618,11 @@ export class ThemeEditor {
|
|
|
553
618
|
this.themeControl._applyTheme(themeKey, false)
|
|
554
619
|
}
|
|
555
620
|
|
|
621
|
+
// Fire onChange callback for reset
|
|
622
|
+
if (this.themeControl.options.onChange) {
|
|
623
|
+
this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
|
|
624
|
+
}
|
|
625
|
+
|
|
556
626
|
// Re-render editor panel with default values
|
|
557
627
|
this._renderThemeEditor(themeKey)
|
|
558
628
|
}
|