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 +15 -13
- package/package.json +1 -1
- package/src/leaflet-theme-control.css +67 -16
- package/src/leaflet-theme-control.js +31 -2
- package/src/leaflet-theme-editor.js +86 -17
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.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:
|
|
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,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
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
357
|
-
|
|
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
|
|
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
|
-
//
|
|
547
|
-
|
|
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
|
|
551
|
-
//
|
|
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.
|
|
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
|