leaflet-theme-control 0.1.4 → 0.1.5
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/package.json +15 -15
- package/src/leaflet-theme-control.js +30 -40
- package/src/leaflet-theme-editor.js +75 -93
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leaflet-theme-control",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -38,31 +38,31 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
41
|
-
"url": "git+https://github.com/
|
|
41
|
+
"url": "git+https://github.com/KristjanESPERANTO/leaflet-theme-control.git"
|
|
42
42
|
},
|
|
43
43
|
"bugs": {
|
|
44
|
-
"url": "https://github.com/
|
|
44
|
+
"url": "https://github.com/KristjanESPERANTO/leaflet-theme-control/issues"
|
|
45
45
|
},
|
|
46
|
-
"homepage": "https://
|
|
46
|
+
"homepage": "https://KristjanESPERANTO.github.io/leaflet-theme-control/",
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"leaflet": ">=2.0.0-alpha.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@eslint/css": "^0.
|
|
52
|
-
"@eslint/js": "^
|
|
51
|
+
"@eslint/css": "^1.0.0",
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
53
53
|
"@eslint/markdown": "^7.5.1",
|
|
54
|
-
"@stylistic/eslint-plugin": "^5.
|
|
54
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
55
55
|
"commit-and-tag-version": "^12.6.1",
|
|
56
|
-
"eslint": "^
|
|
57
|
-
"eslint-plugin-import-x": "^4.16.
|
|
58
|
-
"eslint-plugin-jsdoc": "^
|
|
59
|
-
"globals": "^
|
|
60
|
-
"happy-dom": "^20.
|
|
56
|
+
"eslint": "^10.0.3",
|
|
57
|
+
"eslint-plugin-import-x": "^4.16.2",
|
|
58
|
+
"eslint-plugin-jsdoc": "^62.8.0",
|
|
59
|
+
"globals": "^17.4.0",
|
|
60
|
+
"happy-dom": "^20.8.4",
|
|
61
61
|
"leaflet": "^2.0.0-alpha.1",
|
|
62
|
-
"lint-staged": "^16.
|
|
63
|
-
"prettier": "^3.
|
|
62
|
+
"lint-staged": "^16.4.0",
|
|
63
|
+
"prettier": "^3.8.1",
|
|
64
64
|
"simple-git-hooks": "^2.13.1",
|
|
65
|
-
"vitest": "^4.0
|
|
65
|
+
"vitest": "^4.1.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"test": "npm run lint && vitest run",
|
|
@@ -33,23 +33,10 @@ export class ThemeControl extends Control {
|
|
|
33
33
|
initialize(options) {
|
|
34
34
|
Util.setOptions(this, options)
|
|
35
35
|
|
|
36
|
-
// Create
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
48
|
-
this.options.themes = {}
|
|
49
|
-
Object.keys(DEFAULT_THEMES).forEach((key) => {
|
|
50
|
-
this.options.themes[key] = { ...DEFAULT_THEMES[key] }
|
|
51
|
-
})
|
|
52
|
-
}
|
|
36
|
+
// Create shallow copies of themes to avoid mutating the source objects
|
|
37
|
+
this.options.themes = this._shallowCopyThemes(
|
|
38
|
+
this.options.themes || DEFAULT_THEMES,
|
|
39
|
+
)
|
|
53
40
|
|
|
54
41
|
// Store original themes for reset functionality in editor
|
|
55
42
|
// This ensures reset uses user-provided values, not DEFAULT_THEMES
|
|
@@ -260,32 +247,12 @@ export class ThemeControl extends Control {
|
|
|
260
247
|
const controlStyle = theme.controlStyle || 'light'
|
|
261
248
|
this.root.setAttribute('data-control-style', controlStyle)
|
|
262
249
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
if (t.className) {
|
|
266
|
-
this.root.classList.remove(t.className)
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
// Add current theme class to root
|
|
271
|
-
if (theme.className) {
|
|
272
|
-
this.root.classList.add(theme.className)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Also apply theme class to control container if map exists
|
|
250
|
+
// Apply theme classes to root and control container
|
|
251
|
+
this._applyThemeClasses(this.root, theme)
|
|
276
252
|
if (this.map) {
|
|
277
253
|
const controlContainer = this.map.getContainer().querySelector('.leaflet-control-container')
|
|
278
254
|
if (controlContainer) {
|
|
279
|
-
|
|
280
|
-
Object.values(this.options.themes).forEach((t) => {
|
|
281
|
-
if (t.className) {
|
|
282
|
-
controlContainer.classList.remove(t.className)
|
|
283
|
-
}
|
|
284
|
-
})
|
|
285
|
-
// Add current theme class
|
|
286
|
-
if (theme.className) {
|
|
287
|
-
controlContainer.classList.add(theme.className)
|
|
288
|
-
}
|
|
255
|
+
this._applyThemeClasses(controlContainer, theme)
|
|
289
256
|
}
|
|
290
257
|
}
|
|
291
258
|
|
|
@@ -346,6 +313,29 @@ export class ThemeControl extends Control {
|
|
|
346
313
|
}
|
|
347
314
|
}
|
|
348
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Shallow-copy each theme object from the given source.
|
|
318
|
+
* @param {object} source - Theme map to copy
|
|
319
|
+
* @returns {object} New object with copied themes
|
|
320
|
+
*/
|
|
321
|
+
_shallowCopyThemes(source) {
|
|
322
|
+
return Object.fromEntries(
|
|
323
|
+
Object.entries(source).map(([key, theme]) => [key, { ...theme }]),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Remove all theme classes from an element, then add the active one.
|
|
329
|
+
* @param {HTMLElement} el - Target element
|
|
330
|
+
* @param {object} theme - Currently active theme config
|
|
331
|
+
*/
|
|
332
|
+
_applyThemeClasses(el, theme) {
|
|
333
|
+
for (const t of Object.values(this.options.themes)) {
|
|
334
|
+
if (t.className) el.classList.remove(t.className)
|
|
335
|
+
}
|
|
336
|
+
if (theme.className) el.classList.add(theme.className)
|
|
337
|
+
}
|
|
338
|
+
|
|
349
339
|
getCurrentTheme() {
|
|
350
340
|
return this.currentTheme
|
|
351
341
|
}
|
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import { DomEvent, DomUtil } from 'leaflet'
|
|
2
2
|
import { DEFAULT_THEMES } from './leaflet-theme-control-themes.js'
|
|
3
3
|
|
|
4
|
+
/** Default editor UI labels (allocated once, reused on every _getLabel call) */
|
|
5
|
+
const DEFAULT_LABELS = {
|
|
6
|
+
selectTheme: 'Select Theme',
|
|
7
|
+
customize: 'Customize',
|
|
8
|
+
close: 'Close',
|
|
9
|
+
back: 'Back',
|
|
10
|
+
resetToDefault: 'Reset to Default',
|
|
11
|
+
customizeTheme: 'Customize this theme',
|
|
12
|
+
customBadge: 'Custom',
|
|
13
|
+
controlStyle: 'Control Style',
|
|
14
|
+
lightControls: 'Light',
|
|
15
|
+
darkControls: 'Dark',
|
|
16
|
+
invert: 'Invert',
|
|
17
|
+
hueRotate: 'Hue Rotate',
|
|
18
|
+
saturate: 'Saturate',
|
|
19
|
+
brightness: 'Brightness',
|
|
20
|
+
contrast: 'Contrast',
|
|
21
|
+
sepia: 'Sepia',
|
|
22
|
+
grayscaleFilter: 'Grayscale',
|
|
23
|
+
themeButton: 'Theme',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Filter definitions: key → { cssName, regex, defaultValue }
|
|
28
|
+
* Used by both _parseFilterString and _isThemeModified.
|
|
29
|
+
*/
|
|
30
|
+
const FILTER_DEFS = [
|
|
31
|
+
{ key: 'invert', css: 'invert', default: 0 },
|
|
32
|
+
{ key: 'hueRotate', css: 'hue-rotate', default: 0, unit: 'deg' },
|
|
33
|
+
{ key: 'saturate', css: 'saturate', default: 1 },
|
|
34
|
+
{ key: 'brightness', css: 'brightness', default: 1 },
|
|
35
|
+
{ key: 'contrast', css: 'contrast', default: 1 },
|
|
36
|
+
{ key: 'sepia', css: 'sepia', default: 0 },
|
|
37
|
+
{ key: 'grayscale', css: 'grayscale', default: 0 },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
// Pre-compiled regexes (one per filter, built from FILTER_DEFS)
|
|
41
|
+
const FILTER_REGEXES = FILTER_DEFS.map(({ css, unit }) => {
|
|
42
|
+
const unitPattern = unit ? unit : ''
|
|
43
|
+
return new RegExp(`${css}\\(([\\d.]+)${unitPattern}\\)`)
|
|
44
|
+
})
|
|
45
|
+
|
|
4
46
|
/**
|
|
5
47
|
* ThemeEditor - UI for selecting and editing themes
|
|
6
48
|
*
|
|
@@ -16,6 +58,9 @@ export class ThemeEditor {
|
|
|
16
58
|
this.currentView = 'selector' // 'selector' or 'editor'
|
|
17
59
|
this.editingTheme = null
|
|
18
60
|
|
|
61
|
+
// AbortController for automatic event cleanup
|
|
62
|
+
this._abortController = new AbortController()
|
|
63
|
+
|
|
19
64
|
// Storage key for custom filters
|
|
20
65
|
this.storageKey = `${themeControl.options.storageKey}-custom-filters`
|
|
21
66
|
|
|
@@ -117,12 +162,12 @@ export class ThemeEditor {
|
|
|
117
162
|
|
|
118
163
|
this.panel = panel
|
|
119
164
|
|
|
120
|
-
// Close on ESC key
|
|
121
|
-
|
|
165
|
+
// Close on ESC key (listener lives for the lifetime of the panel)
|
|
166
|
+
document.addEventListener('keydown', (e) => {
|
|
122
167
|
if (e.key === 'Escape' && this.isOpen) {
|
|
123
168
|
this.close()
|
|
124
169
|
}
|
|
125
|
-
}
|
|
170
|
+
}, { signal: this._abortController.signal })
|
|
126
171
|
|
|
127
172
|
return panel
|
|
128
173
|
}
|
|
@@ -133,29 +178,7 @@ export class ThemeEditor {
|
|
|
133
178
|
return this.themeControl.options.getEditorLabels(key)
|
|
134
179
|
}
|
|
135
180
|
|
|
136
|
-
|
|
137
|
-
const labels = {
|
|
138
|
-
selectTheme: 'Select Theme',
|
|
139
|
-
customize: 'Customize',
|
|
140
|
-
close: 'Close',
|
|
141
|
-
back: 'Back',
|
|
142
|
-
resetToDefault: 'Reset to Default',
|
|
143
|
-
customizeTheme: 'Customize this theme',
|
|
144
|
-
customBadge: 'Custom',
|
|
145
|
-
controlStyle: 'Control Style',
|
|
146
|
-
lightControls: 'Light',
|
|
147
|
-
darkControls: 'Dark',
|
|
148
|
-
invert: 'Invert',
|
|
149
|
-
hueRotate: 'Hue Rotate',
|
|
150
|
-
saturate: 'Saturate',
|
|
151
|
-
brightness: 'Brightness',
|
|
152
|
-
contrast: 'Contrast',
|
|
153
|
-
sepia: 'Sepia',
|
|
154
|
-
grayscaleFilter: 'Grayscale',
|
|
155
|
-
themeButton: 'Theme',
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return labels[key] || key
|
|
181
|
+
return DEFAULT_LABELS[key] || key
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
openThemeSelector() {
|
|
@@ -166,9 +189,6 @@ export class ThemeEditor {
|
|
|
166
189
|
this.panel.style.display = 'block'
|
|
167
190
|
this._renderThemeSelector()
|
|
168
191
|
|
|
169
|
-
// Add keyboard listener
|
|
170
|
-
document.addEventListener('keydown', this._onKeyDown)
|
|
171
|
-
|
|
172
192
|
// Focus first interactive element
|
|
173
193
|
setTimeout(() => {
|
|
174
194
|
const firstBtn = this.panel.querySelector('.theme-select-btn')
|
|
@@ -202,9 +222,6 @@ export class ThemeEditor {
|
|
|
202
222
|
this.panel.style.display = 'none'
|
|
203
223
|
this.currentView = 'selector'
|
|
204
224
|
this.editingTheme = null
|
|
205
|
-
|
|
206
|
-
// Remove keyboard listener
|
|
207
|
-
document.removeEventListener('keydown', this._onKeyDown)
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
_isThemeModified(themeKey) {
|
|
@@ -244,11 +261,8 @@ export class ThemeEditor {
|
|
|
244
261
|
this.close()
|
|
245
262
|
}
|
|
246
263
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
document.removeEventListener('keydown', this._onKeyDown)
|
|
250
|
-
this._onKeyDown = null
|
|
251
|
-
}
|
|
264
|
+
// Abort all event listeners
|
|
265
|
+
this._abortController.abort()
|
|
252
266
|
|
|
253
267
|
// Remove panel from DOM
|
|
254
268
|
if (this.panel && this.panel.parentNode) {
|
|
@@ -454,13 +468,13 @@ export class ThemeEditor {
|
|
|
454
468
|
body.appendChild(this._createControlStyleSelector(controlStyle))
|
|
455
469
|
|
|
456
470
|
// Filter sliders
|
|
457
|
-
body.appendChild(this._createSlider('invert', filterValues.invert
|
|
458
|
-
body.appendChild(this._createSlider('hueRotate', filterValues.hueRotate
|
|
459
|
-
body.appendChild(this._createSlider('saturate', filterValues.saturate
|
|
460
|
-
body.appendChild(this._createSlider('brightness', filterValues.brightness
|
|
461
|
-
body.appendChild(this._createSlider('contrast', filterValues.contrast
|
|
462
|
-
body.appendChild(this._createSlider('sepia', filterValues.sepia
|
|
463
|
-
body.appendChild(this._createSlider('grayscale', filterValues.grayscale
|
|
471
|
+
body.appendChild(this._createSlider('invert', filterValues.invert ?? 0, 0, 1, 0.1))
|
|
472
|
+
body.appendChild(this._createSlider('hueRotate', filterValues.hueRotate ?? 0, 0, 360, 1, '°'))
|
|
473
|
+
body.appendChild(this._createSlider('saturate', filterValues.saturate ?? 1, 0, 2, 0.1))
|
|
474
|
+
body.appendChild(this._createSlider('brightness', filterValues.brightness ?? 1, 0, 2, 0.1))
|
|
475
|
+
body.appendChild(this._createSlider('contrast', filterValues.contrast ?? 1, 0, 2, 0.1))
|
|
476
|
+
body.appendChild(this._createSlider('sepia', filterValues.sepia ?? 0, 0, 1, 0.1))
|
|
477
|
+
body.appendChild(this._createSlider('grayscale', filterValues.grayscale ?? 0, 0, 1, 0.1))
|
|
464
478
|
|
|
465
479
|
return body
|
|
466
480
|
}
|
|
@@ -631,64 +645,32 @@ export class ThemeEditor {
|
|
|
631
645
|
}
|
|
632
646
|
|
|
633
647
|
_buildFilterString(values) {
|
|
648
|
+
const EPS = 0.01
|
|
634
649
|
const parts = []
|
|
635
650
|
|
|
636
|
-
if (values.invert >
|
|
637
|
-
if (values.hueRotate
|
|
638
|
-
if (values.saturate
|
|
639
|
-
if (values.brightness
|
|
640
|
-
if (values.contrast
|
|
641
|
-
if (values.sepia >
|
|
642
|
-
if (values.grayscale >
|
|
651
|
+
if (values.invert > EPS) parts.push(`invert(${values.invert})`)
|
|
652
|
+
if (Math.abs(values.hueRotate) > EPS) parts.push(`hue-rotate(${values.hueRotate}deg)`)
|
|
653
|
+
if (Math.abs(values.saturate - 1) > EPS) parts.push(`saturate(${values.saturate})`)
|
|
654
|
+
if (Math.abs(values.brightness - 1) > EPS) parts.push(`brightness(${values.brightness})`)
|
|
655
|
+
if (Math.abs(values.contrast - 1) > EPS) parts.push(`contrast(${values.contrast})`)
|
|
656
|
+
if (values.sepia > EPS) parts.push(`sepia(${values.sepia})`)
|
|
657
|
+
if (values.grayscale > EPS) parts.push(`grayscale(${values.grayscale})`)
|
|
643
658
|
|
|
644
659
|
return parts.join(' ')
|
|
645
660
|
}
|
|
646
661
|
|
|
647
662
|
_parseFilterString(filterString) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
663
|
+
const values = {}
|
|
664
|
+
for (let i = 0; i < FILTER_DEFS.length; i++) {
|
|
665
|
+
const { key, default: def } = FILTER_DEFS[i]
|
|
666
|
+
if (filterString) {
|
|
667
|
+
const match = filterString.match(FILTER_REGEXES[i])
|
|
668
|
+
values[key] = match ? parseFloat(match[1]) : def
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
values[key] = def
|
|
657
672
|
}
|
|
658
673
|
}
|
|
659
|
-
|
|
660
|
-
const values = {
|
|
661
|
-
invert: 0,
|
|
662
|
-
hueRotate: 0,
|
|
663
|
-
saturate: 1,
|
|
664
|
-
brightness: 1,
|
|
665
|
-
contrast: 1,
|
|
666
|
-
sepia: 0,
|
|
667
|
-
grayscale: 0,
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Parse each filter function
|
|
671
|
-
const invertMatch = filterString.match(/invert\(([\d.]+)\)/)
|
|
672
|
-
if (invertMatch) values.invert = parseFloat(invertMatch[1])
|
|
673
|
-
|
|
674
|
-
const hueMatch = filterString.match(/hue-rotate\(([\d.]+)deg\)/)
|
|
675
|
-
if (hueMatch) values.hueRotate = parseFloat(hueMatch[1])
|
|
676
|
-
|
|
677
|
-
const saturateMatch = filterString.match(/saturate\(([\d.]+)\)/)
|
|
678
|
-
if (saturateMatch) values.saturate = parseFloat(saturateMatch[1])
|
|
679
|
-
|
|
680
|
-
const brightnessMatch = filterString.match(/brightness\(([\d.]+)\)/)
|
|
681
|
-
if (brightnessMatch) values.brightness = parseFloat(brightnessMatch[1])
|
|
682
|
-
|
|
683
|
-
const contrastMatch = filterString.match(/contrast\(([\d.]+)\)/)
|
|
684
|
-
if (contrastMatch) values.contrast = parseFloat(contrastMatch[1])
|
|
685
|
-
|
|
686
|
-
const sepiaMatch = filterString.match(/sepia\(([\d.]+)\)/)
|
|
687
|
-
if (sepiaMatch) values.sepia = parseFloat(sepiaMatch[1])
|
|
688
|
-
|
|
689
|
-
const grayscaleMatch = filterString.match(/grayscale\(([\d.]+)\)/)
|
|
690
|
-
if (grayscaleMatch) values.grayscale = parseFloat(grayscaleMatch[1])
|
|
691
|
-
|
|
692
674
|
return values
|
|
693
675
|
}
|
|
694
676
|
}
|