leaflet-theme-control 0.0.1
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/LICENSE.md +21 -0
- package/README.md +277 -0
- package/package.json +82 -0
- package/src/leaflet-theme-control-themes.js +33 -0
- package/src/leaflet-theme-control.css +503 -0
- package/src/leaflet-theme-control.js +273 -0
- package/src/leaflet-theme-editor.js +621 -0
- package/types/index.d.ts +186 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { DomEvent, DomUtil } from 'leaflet'
|
|
2
|
+
import { DEFAULT_THEMES } from './leaflet-theme-control-themes.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ThemeEditor - UI for selecting and editing themes
|
|
6
|
+
*
|
|
7
|
+
* Provides a theme selector with individual theme editors.
|
|
8
|
+
* Allows users to customize CSS filters for each theme with sliders
|
|
9
|
+
* and save preferences to localStorage.
|
|
10
|
+
*/
|
|
11
|
+
export class ThemeEditor {
|
|
12
|
+
constructor(themeControl) {
|
|
13
|
+
this.themeControl = themeControl
|
|
14
|
+
this.panel = null
|
|
15
|
+
this.isOpen = false
|
|
16
|
+
this.currentView = 'selector' // 'selector' or 'editor'
|
|
17
|
+
this.editingTheme = null
|
|
18
|
+
|
|
19
|
+
// Storage key for custom filters
|
|
20
|
+
this.storageKey = `${themeControl.options.storageKey}-custom-filters`
|
|
21
|
+
|
|
22
|
+
// Load custom filters from localStorage
|
|
23
|
+
this.customFilters = this._loadCustomFilters()
|
|
24
|
+
|
|
25
|
+
// Apply custom filters to themes
|
|
26
|
+
this._applyCustomFilters()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create DOM element with children (like React's createElement)
|
|
31
|
+
* @param {string} tag - HTML tag name
|
|
32
|
+
* @param {object} attrs - Attributes (class, id, data-*, aria-*, etc)
|
|
33
|
+
* @param {...(Node|string)} children - Child nodes or text
|
|
34
|
+
* @returns {HTMLElement} Created element
|
|
35
|
+
*/
|
|
36
|
+
_el(tag, attrs = {}, ...children) {
|
|
37
|
+
const element = document.createElement(tag)
|
|
38
|
+
|
|
39
|
+
// Set attributes
|
|
40
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
41
|
+
if (key === 'className') {
|
|
42
|
+
element.className = value
|
|
43
|
+
}
|
|
44
|
+
else if (key.startsWith('data-')) {
|
|
45
|
+
element.setAttribute(key, value)
|
|
46
|
+
}
|
|
47
|
+
else if (key.startsWith('aria-')) {
|
|
48
|
+
element.setAttribute(key, value)
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
element[key] = value
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Append children
|
|
56
|
+
children.forEach((child) => {
|
|
57
|
+
if (typeof child === 'string') {
|
|
58
|
+
element.appendChild(document.createTextNode(child))
|
|
59
|
+
}
|
|
60
|
+
else if (child instanceof Node) {
|
|
61
|
+
element.appendChild(child)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return element
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_loadCustomFilters() {
|
|
69
|
+
try {
|
|
70
|
+
const stored = localStorage.getItem(this.storageKey)
|
|
71
|
+
return stored ? JSON.parse(stored) : {}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.error('Failed to load custom filters:', e)
|
|
75
|
+
return {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_saveCustomFilters() {
|
|
80
|
+
try {
|
|
81
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.customFilters))
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error('Failed to save custom filters:', e)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_applyCustomFilters() {
|
|
89
|
+
Object.keys(this.customFilters).forEach((themeKey) => {
|
|
90
|
+
if (this.themeControl.options.themes[themeKey]) {
|
|
91
|
+
const custom = this.customFilters[themeKey]
|
|
92
|
+
this.themeControl.options.themes[themeKey].filter = this._buildFilterString(custom)
|
|
93
|
+
|
|
94
|
+
// Apply control style if saved
|
|
95
|
+
if (custom.controlStyle) {
|
|
96
|
+
this.themeControl.options.themes[themeKey].controlStyle = custom.controlStyle
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
createPanel() {
|
|
103
|
+
const panel = DomUtil.create('div', 'leaflet-theme-panel')
|
|
104
|
+
panel.style.display = 'none'
|
|
105
|
+
panel.setAttribute('role', 'dialog')
|
|
106
|
+
panel.setAttribute('aria-modal', 'true')
|
|
107
|
+
panel.setAttribute('aria-labelledby', 'theme-panel-title')
|
|
108
|
+
|
|
109
|
+
// Prevent map interactions when using the panel
|
|
110
|
+
DomEvent.disableClickPropagation(panel)
|
|
111
|
+
DomEvent.disableScrollPropagation(panel)
|
|
112
|
+
|
|
113
|
+
this.panel = panel
|
|
114
|
+
|
|
115
|
+
// Close on ESC key
|
|
116
|
+
this._onKeyDown = (e) => {
|
|
117
|
+
if (e.key === 'Escape' && this.isOpen) {
|
|
118
|
+
this.close()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return panel
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_getLabel(key) {
|
|
126
|
+
// Use custom label function if provided
|
|
127
|
+
if (this.themeControl.options.getEditorLabels) {
|
|
128
|
+
return this.themeControl.options.getEditorLabels(key)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Default labels
|
|
132
|
+
const labels = {
|
|
133
|
+
selectTheme: 'Select Theme',
|
|
134
|
+
customize: 'Customize',
|
|
135
|
+
close: 'Close',
|
|
136
|
+
back: 'Back',
|
|
137
|
+
resetToDefault: 'Reset to Default',
|
|
138
|
+
customizeTheme: 'Customize this theme',
|
|
139
|
+
customBadge: 'Custom',
|
|
140
|
+
controlStyle: 'Control Style',
|
|
141
|
+
lightControls: 'Light',
|
|
142
|
+
darkControls: 'Dark',
|
|
143
|
+
invert: 'Invert',
|
|
144
|
+
hueRotate: 'Hue Rotate',
|
|
145
|
+
saturate: 'Saturate',
|
|
146
|
+
brightness: 'Brightness',
|
|
147
|
+
contrast: 'Contrast',
|
|
148
|
+
sepia: 'Sepia',
|
|
149
|
+
grayscaleFilter: 'Grayscale',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return labels[key] || key
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
openThemeSelector() {
|
|
156
|
+
if (!this.panel) return
|
|
157
|
+
|
|
158
|
+
this.isOpen = true
|
|
159
|
+
this.currentView = 'selector'
|
|
160
|
+
this.panel.style.display = 'block'
|
|
161
|
+
this._renderThemeSelector()
|
|
162
|
+
|
|
163
|
+
// Add keyboard listener
|
|
164
|
+
document.addEventListener('keydown', this._onKeyDown)
|
|
165
|
+
|
|
166
|
+
// Focus first interactive element
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
const firstBtn = this.panel.querySelector('.theme-select-btn')
|
|
169
|
+
if (firstBtn) firstBtn.focus()
|
|
170
|
+
}, 50)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
openThemeEditor(themeKey) {
|
|
174
|
+
if (!this.panel) return
|
|
175
|
+
|
|
176
|
+
this.currentView = 'editor'
|
|
177
|
+
this.editingTheme = themeKey
|
|
178
|
+
this._renderThemeEditor(themeKey)
|
|
179
|
+
|
|
180
|
+
// Focus first slider
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
const firstSlider = this.panel.querySelector('input[type="range"]')
|
|
183
|
+
if (firstSlider) firstSlider.focus()
|
|
184
|
+
}, 50)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Alias for easier API usage
|
|
188
|
+
open() {
|
|
189
|
+
this.openThemeSelector()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
close() {
|
|
193
|
+
if (!this.panel) return
|
|
194
|
+
|
|
195
|
+
this.isOpen = false
|
|
196
|
+
this.panel.style.display = 'none'
|
|
197
|
+
this.currentView = 'selector'
|
|
198
|
+
this.editingTheme = null
|
|
199
|
+
|
|
200
|
+
// Remove keyboard listener
|
|
201
|
+
document.removeEventListener('keydown', this._onKeyDown)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cleanup() {
|
|
205
|
+
// Close panel if open
|
|
206
|
+
if (this.isOpen) {
|
|
207
|
+
this.close()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Remove keyboard listener (in case close() wasn't called)
|
|
211
|
+
if (this._onKeyDown) {
|
|
212
|
+
document.removeEventListener('keydown', this._onKeyDown)
|
|
213
|
+
this._onKeyDown = null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Remove panel from DOM
|
|
217
|
+
if (this.panel && this.panel.parentNode) {
|
|
218
|
+
this.panel.parentNode.removeChild(this.panel)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clear all references
|
|
222
|
+
this.panel = null
|
|
223
|
+
this.themeControl = null
|
|
224
|
+
this.customFilters = null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_renderThemeSelector() {
|
|
228
|
+
const currentTheme = this.themeControl.getCurrentTheme()
|
|
229
|
+
const themes = this.themeControl.options.themes
|
|
230
|
+
|
|
231
|
+
// Build panel header
|
|
232
|
+
const header = this._createPanelHeader(this._getLabel('selectTheme'))
|
|
233
|
+
|
|
234
|
+
// Build theme rows
|
|
235
|
+
const body = this._el('div', {
|
|
236
|
+
'className': 'theme-panel-body',
|
|
237
|
+
'role': 'radiogroup',
|
|
238
|
+
'aria-label': this._getLabel('selectTheme'),
|
|
239
|
+
})
|
|
240
|
+
Object.keys(themes).forEach((themeKey) => {
|
|
241
|
+
const row = this._createThemeRow(themeKey, themes[themeKey], themeKey === currentTheme)
|
|
242
|
+
body.appendChild(row)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Replace panel content
|
|
246
|
+
this.panel.replaceChildren(header, body)
|
|
247
|
+
|
|
248
|
+
// Attach event listeners
|
|
249
|
+
this._attachSelectorListeners()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_createPanelHeader(title, showBackButton = false) {
|
|
253
|
+
const header = this._el('div', { className: 'theme-panel-header' })
|
|
254
|
+
|
|
255
|
+
if (showBackButton) {
|
|
256
|
+
const backBtn = this._el('button', {
|
|
257
|
+
'className': 'theme-panel-back',
|
|
258
|
+
'aria-label': this._getLabel('back'),
|
|
259
|
+
}, '←')
|
|
260
|
+
header.appendChild(backBtn)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const h3 = this._el('h3', { id: 'theme-panel-title' }, title)
|
|
264
|
+
header.appendChild(h3)
|
|
265
|
+
|
|
266
|
+
const closeBtn = this._el('button', {
|
|
267
|
+
'className': 'theme-panel-close',
|
|
268
|
+
'aria-label': this._getLabel('close'),
|
|
269
|
+
}, '×')
|
|
270
|
+
header.appendChild(closeBtn)
|
|
271
|
+
|
|
272
|
+
return header
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_createThemeRow(themeKey, theme, isActive) {
|
|
276
|
+
const hasCustom = !!this.customFilters[themeKey]
|
|
277
|
+
const themeLabel = this.themeControl._getThemeLabel(themeKey)
|
|
278
|
+
|
|
279
|
+
// Create select button with icon, label and badges
|
|
280
|
+
const selectBtn = this._el('button', {
|
|
281
|
+
'className': 'theme-select-btn',
|
|
282
|
+
'data-theme': themeKey,
|
|
283
|
+
'role': 'radio',
|
|
284
|
+
'aria-checked': isActive ? 'true' : 'false',
|
|
285
|
+
'aria-label': themeLabel,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
selectBtn.appendChild(this._el('span', { 'className': 'theme-icon', 'aria-hidden': 'true' }, theme.icon || '🎨'))
|
|
289
|
+
selectBtn.appendChild(this._el('span', { className: 'theme-name' }, themeLabel))
|
|
290
|
+
|
|
291
|
+
if (hasCustom) {
|
|
292
|
+
selectBtn.appendChild(this._el('span', { className: 'theme-custom-badge' }, this._getLabel('customBadge')))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isActive) {
|
|
296
|
+
selectBtn.appendChild(this._el('span', { 'className': 'theme-active-badge', 'aria-hidden': 'true' }, '✓'))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Create edit button
|
|
300
|
+
const editBtn = this._el('button', {
|
|
301
|
+
'className': 'theme-edit-btn',
|
|
302
|
+
'data-theme': themeKey,
|
|
303
|
+
'aria-label': `${this._getLabel('customizeTheme')}: ${themeLabel}`,
|
|
304
|
+
}, '⚙️')
|
|
305
|
+
|
|
306
|
+
// Create row container
|
|
307
|
+
const row = this._el('div', {
|
|
308
|
+
'className': `theme-row ${isActive ? 'active' : ''}`,
|
|
309
|
+
'data-theme': themeKey,
|
|
310
|
+
})
|
|
311
|
+
row.appendChild(selectBtn)
|
|
312
|
+
row.appendChild(editBtn)
|
|
313
|
+
|
|
314
|
+
return row
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_attachSelectorListeners() {
|
|
318
|
+
// Close button
|
|
319
|
+
const closeBtn = this.panel.querySelector('.theme-panel-close')
|
|
320
|
+
DomEvent.on(closeBtn, 'click', () => this.close())
|
|
321
|
+
|
|
322
|
+
// Theme select buttons
|
|
323
|
+
const selectBtns = this.panel.querySelectorAll('.theme-select-btn')
|
|
324
|
+
selectBtns.forEach((btn) => {
|
|
325
|
+
DomEvent.on(btn, 'click', (e) => {
|
|
326
|
+
const themeKey = e.currentTarget.dataset.theme
|
|
327
|
+
|
|
328
|
+
// Update aria-checked states
|
|
329
|
+
selectBtns.forEach(b => b.setAttribute('aria-checked', 'false'))
|
|
330
|
+
e.currentTarget.setAttribute('aria-checked', 'true')
|
|
331
|
+
|
|
332
|
+
this.themeControl.setTheme(themeKey)
|
|
333
|
+
this.close()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Theme edit buttons
|
|
338
|
+
const editBtns = this.panel.querySelectorAll('.theme-edit-btn')
|
|
339
|
+
editBtns.forEach((btn) => {
|
|
340
|
+
DomEvent.on(btn, 'click', (e) => {
|
|
341
|
+
DomEvent.stop(e)
|
|
342
|
+
const themeKey = e.currentTarget.dataset.theme
|
|
343
|
+
this.openThemeEditor(themeKey)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_renderThemeEditor(themeKey) {
|
|
349
|
+
const theme = this.themeControl.options.themes[themeKey]
|
|
350
|
+
const themeLabel = this.themeControl._getThemeLabel(themeKey)
|
|
351
|
+
|
|
352
|
+
// Get current filter values or defaults
|
|
353
|
+
const filterValues = this.customFilters[themeKey] || this._parseFilterString(theme.filter)
|
|
354
|
+
|
|
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
|
+
}
|
|
363
|
+
|
|
364
|
+
// Build panel structure
|
|
365
|
+
const header = this._createPanelHeader(`${this._getLabel('customize')}: ${themeLabel}`, true)
|
|
366
|
+
const body = this._createEditorBody(filterValues, controlStyle)
|
|
367
|
+
const footer = this._createEditorFooter()
|
|
368
|
+
|
|
369
|
+
// Replace panel content
|
|
370
|
+
this.panel.replaceChildren(header, body, footer)
|
|
371
|
+
|
|
372
|
+
// Attach event listeners
|
|
373
|
+
this._attachEditorListeners(themeKey, filterValues, controlStyle)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
_createControlStyleSelector(controlStyle) {
|
|
377
|
+
const selector = this._el('div', { className: 'control-style-selector' })
|
|
378
|
+
|
|
379
|
+
const label = this._el('label', {}, this._getLabel('controlStyle'))
|
|
380
|
+
selector.appendChild(label)
|
|
381
|
+
|
|
382
|
+
const buttonsContainer = this._el('div', { className: 'control-style-buttons' })
|
|
383
|
+
|
|
384
|
+
const lightBtn = this._el('button', {
|
|
385
|
+
'className': `control-style-btn ${controlStyle === 'light' ? 'active' : ''}`,
|
|
386
|
+
'data-style': 'light',
|
|
387
|
+
}, `☀️ ${this._getLabel('lightControls')}`)
|
|
388
|
+
|
|
389
|
+
const darkBtn = this._el('button', {
|
|
390
|
+
'className': `control-style-btn ${controlStyle === 'dark' ? 'active' : ''}`,
|
|
391
|
+
'data-style': 'dark',
|
|
392
|
+
}, `🌙 ${this._getLabel('darkControls')}`)
|
|
393
|
+
|
|
394
|
+
buttonsContainer.appendChild(lightBtn)
|
|
395
|
+
buttonsContainer.appendChild(darkBtn)
|
|
396
|
+
selector.appendChild(buttonsContainer)
|
|
397
|
+
|
|
398
|
+
return selector
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
_createEditorBody(filterValues, controlStyle) {
|
|
402
|
+
const body = this._el('div', { className: 'theme-panel-body theme-editor-sliders' })
|
|
403
|
+
|
|
404
|
+
// Control style selector
|
|
405
|
+
body.appendChild(this._createControlStyleSelector(controlStyle))
|
|
406
|
+
|
|
407
|
+
// Filter sliders
|
|
408
|
+
body.appendChild(this._createSlider('invert', filterValues.invert || 0, 0, 1, 0.1))
|
|
409
|
+
body.appendChild(this._createSlider('hueRotate', filterValues.hueRotate || 0, 0, 360, 1, '°'))
|
|
410
|
+
body.appendChild(this._createSlider('saturate', filterValues.saturate || 1, 0, 2, 0.1))
|
|
411
|
+
body.appendChild(this._createSlider('brightness', filterValues.brightness || 1, 0, 2, 0.1))
|
|
412
|
+
body.appendChild(this._createSlider('contrast', filterValues.contrast || 1, 0, 2, 0.1))
|
|
413
|
+
body.appendChild(this._createSlider('sepia', filterValues.sepia || 0, 0, 1, 0.1))
|
|
414
|
+
body.appendChild(this._createSlider('grayscale', filterValues.grayscale || 0, 0, 1, 0.1))
|
|
415
|
+
|
|
416
|
+
return body
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_createEditorFooter() {
|
|
420
|
+
const footer = this._el('div', { className: 'theme-panel-footer' })
|
|
421
|
+
const resetBtn = this._el('button', { className: 'theme-editor-reset' }, this._getLabel('resetToDefault'))
|
|
422
|
+
footer.appendChild(resetBtn)
|
|
423
|
+
return footer
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
_createSlider(key, value, min, max, step, unit = '') {
|
|
427
|
+
const label = this._getLabel(key)
|
|
428
|
+
|
|
429
|
+
return this._el('div', { className: 'theme-editor-slider' },
|
|
430
|
+
this._el('label', { htmlFor: `slider-${key}` },
|
|
431
|
+
this._el('span', { className: 'slider-label' }, label),
|
|
432
|
+
this._el('span', { 'className': 'slider-value', 'data-key': key }, `${value}${unit}`),
|
|
433
|
+
),
|
|
434
|
+
this._el('input', {
|
|
435
|
+
'type': 'range',
|
|
436
|
+
'id': `slider-${key}`,
|
|
437
|
+
'data-key': key,
|
|
438
|
+
'min': String(min),
|
|
439
|
+
'max': String(max),
|
|
440
|
+
'step': String(step),
|
|
441
|
+
'value': String(value),
|
|
442
|
+
'aria-label': `${label}: ${value}${unit}`,
|
|
443
|
+
'aria-valuemin': String(min),
|
|
444
|
+
'aria-valuemax': String(max),
|
|
445
|
+
'aria-valuenow': String(value),
|
|
446
|
+
}),
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
_attachEditorListeners(themeKey, initialValues, initialControlStyle) {
|
|
451
|
+
// Back button
|
|
452
|
+
const backBtn = this.panel.querySelector('.theme-panel-back')
|
|
453
|
+
DomEvent.on(backBtn, 'click', () => this.openThemeSelector())
|
|
454
|
+
|
|
455
|
+
// Close button
|
|
456
|
+
const closeBtn = this.panel.querySelector('.theme-panel-close')
|
|
457
|
+
DomEvent.on(closeBtn, 'click', () => this.close())
|
|
458
|
+
|
|
459
|
+
// Control style buttons
|
|
460
|
+
let currentControlStyle = initialControlStyle
|
|
461
|
+
const controlStyleBtns = this.panel.querySelectorAll('.control-style-btn')
|
|
462
|
+
controlStyleBtns.forEach((btn) => {
|
|
463
|
+
DomEvent.on(btn, 'click', (e) => {
|
|
464
|
+
const style = e.currentTarget.dataset.style
|
|
465
|
+
currentControlStyle = style
|
|
466
|
+
|
|
467
|
+
// Update active state
|
|
468
|
+
controlStyleBtns.forEach(b => b.classList.remove('active'))
|
|
469
|
+
e.currentTarget.classList.add('active')
|
|
470
|
+
|
|
471
|
+
// Live preview and save
|
|
472
|
+
this.themeControl.root.setAttribute('data-control-style', style)
|
|
473
|
+
this._saveTheme(themeKey, currentValues, currentControlStyle)
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Sliders
|
|
478
|
+
const sliders = this.panel.querySelectorAll('input[type="range"]')
|
|
479
|
+
const currentValues = { ...initialValues }
|
|
480
|
+
|
|
481
|
+
sliders.forEach((slider) => {
|
|
482
|
+
DomEvent.on(slider, 'input', (e) => {
|
|
483
|
+
const key = e.target.dataset.key
|
|
484
|
+
const value = parseFloat(e.target.value)
|
|
485
|
+
currentValues[key] = value
|
|
486
|
+
|
|
487
|
+
// Update display value
|
|
488
|
+
const valueDisplay = this.panel.querySelector(`.slider-value[data-key="${key}"]`)
|
|
489
|
+
const unit = key === 'hueRotate' ? '°' : ''
|
|
490
|
+
valueDisplay.textContent = value + unit
|
|
491
|
+
|
|
492
|
+
// Update aria-valuenow and aria-label
|
|
493
|
+
e.target.setAttribute('aria-valuenow', value)
|
|
494
|
+
e.target.setAttribute('aria-label', `${this._getLabel(key)}: ${value}${unit}`)
|
|
495
|
+
|
|
496
|
+
// Live preview and save
|
|
497
|
+
this._previewFilter(themeKey, currentValues)
|
|
498
|
+
this._saveTheme(themeKey, currentValues, currentControlStyle)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// Reset button
|
|
503
|
+
const resetBtn = this.panel.querySelector('.theme-editor-reset')
|
|
504
|
+
DomEvent.on(resetBtn, 'click', () => {
|
|
505
|
+
this._resetTheme(themeKey)
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_previewFilter(themeKey, values) {
|
|
510
|
+
const filterString = this._buildFilterString(values)
|
|
511
|
+
|
|
512
|
+
// Temporarily update the filter
|
|
513
|
+
const elements = document.querySelectorAll(this.themeControl.options.cssSelector)
|
|
514
|
+
elements.forEach((el) => {
|
|
515
|
+
el.style.filter = filterString
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_saveTheme(themeKey, values, controlStyle) {
|
|
520
|
+
this.customFilters[themeKey] = {
|
|
521
|
+
...values,
|
|
522
|
+
controlStyle: controlStyle,
|
|
523
|
+
}
|
|
524
|
+
this._saveCustomFilters()
|
|
525
|
+
|
|
526
|
+
// Update theme
|
|
527
|
+
const filterString = this._buildFilterString(values)
|
|
528
|
+
this.themeControl.options.themes[themeKey].filter = filterString
|
|
529
|
+
this.themeControl.options.themes[themeKey].controlStyle = controlStyle
|
|
530
|
+
|
|
531
|
+
// Reapply current theme if it's the one being edited
|
|
532
|
+
if (this.themeControl.getCurrentTheme() === themeKey) {
|
|
533
|
+
this.themeControl.setTheme(themeKey)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_resetTheme(themeKey) {
|
|
538
|
+
// Remove custom filter and controlStyle
|
|
539
|
+
delete this.customFilters[themeKey]
|
|
540
|
+
this._saveCustomFilters()
|
|
541
|
+
|
|
542
|
+
// Restore default values from DEFAULT_THEMES
|
|
543
|
+
const defaultTheme = DEFAULT_THEMES[themeKey]
|
|
544
|
+
|
|
545
|
+
if (defaultTheme) {
|
|
546
|
+
// Copy all properties from default theme
|
|
547
|
+
this.themeControl.options.themes[themeKey] = { ...defaultTheme }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Reapply theme (including controlStyle) BEFORE re-rendering editor
|
|
551
|
+
// This ensures the DOM attribute is set correctly first
|
|
552
|
+
if (this.themeControl.getCurrentTheme() === themeKey) {
|
|
553
|
+
this.themeControl._applyTheme(themeKey, false)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Re-render editor panel with default values
|
|
557
|
+
this._renderThemeEditor(themeKey)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
_buildFilterString(values) {
|
|
561
|
+
const parts = []
|
|
562
|
+
|
|
563
|
+
if (values.invert > 0) parts.push(`invert(${values.invert})`)
|
|
564
|
+
if (values.hueRotate !== 0) parts.push(`hue-rotate(${values.hueRotate}deg)`)
|
|
565
|
+
if (values.saturate !== 1) parts.push(`saturate(${values.saturate})`)
|
|
566
|
+
if (values.brightness !== 1) parts.push(`brightness(${values.brightness})`)
|
|
567
|
+
if (values.contrast !== 1) parts.push(`contrast(${values.contrast})`)
|
|
568
|
+
if (values.sepia > 0) parts.push(`sepia(${values.sepia})`)
|
|
569
|
+
if (values.grayscale > 0) parts.push(`grayscale(${values.grayscale})`)
|
|
570
|
+
|
|
571
|
+
return parts.join(' ')
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_parseFilterString(filterString) {
|
|
575
|
+
if (!filterString) {
|
|
576
|
+
return {
|
|
577
|
+
invert: 0,
|
|
578
|
+
hueRotate: 0,
|
|
579
|
+
saturate: 1,
|
|
580
|
+
brightness: 1,
|
|
581
|
+
contrast: 1,
|
|
582
|
+
sepia: 0,
|
|
583
|
+
grayscale: 0,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const values = {
|
|
588
|
+
invert: 0,
|
|
589
|
+
hueRotate: 0,
|
|
590
|
+
saturate: 1,
|
|
591
|
+
brightness: 1,
|
|
592
|
+
contrast: 1,
|
|
593
|
+
sepia: 0,
|
|
594
|
+
grayscale: 0,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Parse each filter function
|
|
598
|
+
const invertMatch = filterString.match(/invert\(([\d.]+)\)/)
|
|
599
|
+
if (invertMatch) values.invert = parseFloat(invertMatch[1])
|
|
600
|
+
|
|
601
|
+
const hueMatch = filterString.match(/hue-rotate\(([\d.]+)deg\)/)
|
|
602
|
+
if (hueMatch) values.hueRotate = parseFloat(hueMatch[1])
|
|
603
|
+
|
|
604
|
+
const saturateMatch = filterString.match(/saturate\(([\d.]+)\)/)
|
|
605
|
+
if (saturateMatch) values.saturate = parseFloat(saturateMatch[1])
|
|
606
|
+
|
|
607
|
+
const brightnessMatch = filterString.match(/brightness\(([\d.]+)\)/)
|
|
608
|
+
if (brightnessMatch) values.brightness = parseFloat(brightnessMatch[1])
|
|
609
|
+
|
|
610
|
+
const contrastMatch = filterString.match(/contrast\(([\d.]+)\)/)
|
|
611
|
+
if (contrastMatch) values.contrast = parseFloat(contrastMatch[1])
|
|
612
|
+
|
|
613
|
+
const sepiaMatch = filterString.match(/sepia\(([\d.]+)\)/)
|
|
614
|
+
if (sepiaMatch) values.sepia = parseFloat(sepiaMatch[1])
|
|
615
|
+
|
|
616
|
+
const grayscaleMatch = filterString.match(/grayscale\(([\d.]+)\)/)
|
|
617
|
+
if (grayscaleMatch) values.grayscale = parseFloat(grayscaleMatch[1])
|
|
618
|
+
|
|
619
|
+
return values
|
|
620
|
+
}
|
|
621
|
+
}
|