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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaflet-theme-control",
3
- "version": "0.1.4",
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/kristjanesperanto/leaflet-theme-control.git"
41
+ "url": "git+https://github.com/KristjanESPERANTO/leaflet-theme-control.git"
42
42
  },
43
43
  "bugs": {
44
- "url": "https://github.com/kristjanesperanto/leaflet-theme-control/issues"
44
+ "url": "https://github.com/KristjanESPERANTO/leaflet-theme-control/issues"
45
45
  },
46
- "homepage": "https://kristjanesperanto.github.io/leaflet-theme-control/",
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.14.1",
52
- "@eslint/js": "^9.39.2",
51
+ "@eslint/css": "^1.0.0",
52
+ "@eslint/js": "^10.0.1",
53
53
  "@eslint/markdown": "^7.5.1",
54
- "@stylistic/eslint-plugin": "^5.6.1",
54
+ "@stylistic/eslint-plugin": "^5.10.0",
55
55
  "commit-and-tag-version": "^12.6.1",
56
- "eslint": "^9.39.2",
57
- "eslint-plugin-import-x": "^4.16.1",
58
- "eslint-plugin-jsdoc": "^61.5.0",
59
- "globals": "^16.5.0",
60
- "happy-dom": "^20.0.11",
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.2.7",
63
- "prettier": "^3.7.4",
62
+ "lint-staged": "^16.4.0",
63
+ "prettier": "^3.8.1",
64
64
  "simple-git-hooks": "^2.13.1",
65
- "vitest": "^4.0.16"
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 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
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
- // Remove all theme classes from root
264
- Object.values(this.options.themes).forEach((t) => {
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
- // Remove all theme classes from control container
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
- this._onKeyDown = (e) => {
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
- // Default labels
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
- // Remove keyboard listener (in case close() wasn't called)
248
- if (this._onKeyDown) {
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 || 0, 0, 1, 0.1))
458
- body.appendChild(this._createSlider('hueRotate', filterValues.hueRotate || 0, 0, 360, 1, '°'))
459
- body.appendChild(this._createSlider('saturate', filterValues.saturate || 1, 0, 2, 0.1))
460
- body.appendChild(this._createSlider('brightness', filterValues.brightness || 1, 0, 2, 0.1))
461
- body.appendChild(this._createSlider('contrast', filterValues.contrast || 1, 0, 2, 0.1))
462
- body.appendChild(this._createSlider('sepia', filterValues.sepia || 0, 0, 1, 0.1))
463
- body.appendChild(this._createSlider('grayscale', filterValues.grayscale || 0, 0, 1, 0.1))
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 > 0) parts.push(`invert(${values.invert})`)
637
- if (values.hueRotate !== 0) parts.push(`hue-rotate(${values.hueRotate}deg)`)
638
- if (values.saturate !== 1) parts.push(`saturate(${values.saturate})`)
639
- if (values.brightness !== 1) parts.push(`brightness(${values.brightness})`)
640
- if (values.contrast !== 1) parts.push(`contrast(${values.contrast})`)
641
- if (values.sepia > 0) parts.push(`sepia(${values.sepia})`)
642
- if (values.grayscale > 0) parts.push(`grayscale(${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
- if (!filterString) {
649
- return {
650
- invert: 0,
651
- hueRotate: 0,
652
- saturate: 1,
653
- brightness: 1,
654
- contrast: 1,
655
- sepia: 0,
656
- grayscale: 0,
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
  }