fix(csp): wire display sliders and accent picker without inline on*
Lint & Test / test (push) Successful in 10s

The display brightness/contrast sliders and the accent color picker
rendered dynamic HTML with inline oninput/onchange/onclick attributes,
which are blocked by the script-src 'self' CSP — so display settings
were silently un-clickable from the WebUI.

Replace the inline attributes with data-* markers, then attach proper
event listeners after innerHTML (delegated on the container for the
slider rows, direct for the accent dropdown).
This commit is contained in:
2026-05-19 01:17:47 +03:00
parent fe82836f4d
commit e1c8474271
2 changed files with 57 additions and 11 deletions
+33 -6
View File
@@ -182,8 +182,7 @@ export async function loadDisplayMonitors() {
<span class="display-slider-label" data-i18n="display.contrast">${t('display.contrast')}</span> <span class="display-slider-label" data-i18n="display.contrast">${t('display.contrast')}</span>
<input type="range" class="display-slider display-contrast-slider" <input type="range" class="display-slider display-contrast-slider"
min="0" max="100" value="${contrastValue}" min="0" max="100" value="${contrastValue}"
oninput="onDisplayContrastInput(${monitor.id}, this.value)" data-display-slider="contrast" data-monitor-id="${monitor.id}">
onchange="onDisplayContrastChange(${monitor.id}, this.value)">
<span class="display-slider-value" id="contrast-val-${monitor.id}">${contrastValue}%</span> <span class="display-slider-value" id="contrast-val-${monitor.id}">${contrastValue}%</span>
</div>`; </div>`;
} }
@@ -296,8 +295,7 @@ export async function loadDisplayMonitors() {
</svg> </svg>
<span class="display-slider-label" data-i18n="display.brightness">${t('display.brightness')}</span> <span class="display-slider-label" data-i18n="display.brightness">${t('display.brightness')}</span>
<input type="range" class="display-slider display-brightness-slider" min="0" max="100" value="${brightnessValue}" ${brightnessDisabled} <input type="range" class="display-slider display-brightness-slider" min="0" max="100" value="${brightnessValue}" ${brightnessDisabled}
oninput="onDisplayBrightnessInput(${monitor.id}, this.value)" data-display-slider="brightness" data-monitor-id="${monitor.id}">
onchange="onDisplayBrightnessChange(${monitor.id}, this.value)">
<span class="display-slider-value display-brightness-value" id="brightness-val-${monitor.id}">${brightnessValue}%</span> <span class="display-slider-value display-brightness-value" id="brightness-val-${monitor.id}">${brightnessValue}%</span>
</div> </div>
${contrastRow} ${contrastRow}
@@ -306,10 +304,15 @@ export async function loadDisplayMonitors() {
container.appendChild(card); container.appendChild(card);
}); });
// Bind a single delegated click handler for the power buttons. // Bind a single delegated click handler for the power buttons,
// Avoids inline onclick="..." with interpolated monitor data. // plus input/change handlers for the brightness & contrast sliders.
// Avoids inline on* attributes (blocked by script-src 'self' CSP).
container.removeEventListener('click', _onPowerButtonClick); container.removeEventListener('click', _onPowerButtonClick);
container.addEventListener('click', _onPowerButtonClick); container.addEventListener('click', _onPowerButtonClick);
container.removeEventListener('input', _onDisplaySliderInput);
container.addEventListener('input', _onDisplaySliderInput);
container.removeEventListener('change', _onDisplaySliderChange);
container.addEventListener('change', _onDisplaySliderChange);
// Enhance every tuning <select> with an IconSelect now that the // Enhance every tuning <select> with an IconSelect now that the
// cards are in the DOM (IconSelect needs offsetParent + sibling). // cards are in the DOM (IconSelect needs offsetParent + sibling).
@@ -456,6 +459,30 @@ function _onPowerButtonClick(event) {
if (Number.isFinite(id)) toggleDisplayPower(id); if (Number.isFinite(id)) toggleDisplayPower(id);
} }
function _onDisplaySliderInput(event) {
const el = event.target.closest('input[data-display-slider]');
if (!el) return;
const id = Number(el.dataset.monitorId);
if (!Number.isFinite(id)) return;
if (el.dataset.displaySlider === 'brightness') {
onDisplayBrightnessInput(id, el.value);
} else if (el.dataset.displaySlider === 'contrast') {
onDisplayContrastInput(id, el.value);
}
}
function _onDisplaySliderChange(event) {
const el = event.target.closest('input[data-display-slider]');
if (!el) return;
const id = Number(el.dataset.monitorId);
if (!Number.isFinite(id)) return;
if (el.dataset.displaySlider === 'brightness') {
onDisplayBrightnessChange(id, el.value);
} else if (el.dataset.displaySlider === 'contrast') {
onDisplayContrastChange(id, el.value);
}
}
export async function toggleDisplayPower(monitorId) { export async function toggleDisplayPower(monitorId) {
const btn = document.getElementById(`power-btn-${monitorId}`); const btn = document.getElementById(`power-btn-${monitorId}`);
const isOn = btn && btn.classList.contains('on'); const isOn = btn && btn.classList.contains('on');
+24 -5
View File
@@ -207,20 +207,39 @@ export function renderAccentSwatches() {
const swatches = accentPresets.map(p => const swatches = accentPresets.map(p =>
`<div class="accent-swatch ${p.color === current ? 'active' : ''}" `<div class="accent-swatch ${p.color === current ? 'active' : ''}"
style="background: ${p.color}" style="background: ${p.color}"
onclick="selectAccentColor('${p.color}', '${p.hover}')" data-accent-color="${p.color}" data-accent-hover="${p.hover}"
title="${p.name}"></div>` title="${p.name}"></div>`
).join(''); ).join('');
const customRow = ` const customRow = `
<div class="accent-custom-row ${isCustom ? 'active' : ''}" onclick="document.getElementById('accentCustomInput').click()"> <div class="accent-custom-row ${isCustom ? 'active' : ''}" data-accent-custom-row>
<span class="accent-custom-swatch" style="background: ${isCustom ? current : '#888'}"></span> <span class="accent-custom-swatch" style="background: ${isCustom ? current : '#888'}"></span>
<span class="accent-custom-label">${t('accent.custom')}</span> <span class="accent-custom-label">${t('accent.custom')}</span>
<input type="color" id="accentCustomInput" value="${current}" <input type="color" id="accentCustomInput" value="${current}">
onclick="event.stopPropagation()"
onchange="selectAccentColor(this.value, lightenColor(this.value, 15))">
</div>`; </div>`;
dropdown.innerHTML = swatches + customRow; dropdown.innerHTML = swatches + customRow;
// Wire CSP-safe handlers (script-src 'self' blocks inline on* attributes).
dropdown.querySelectorAll('.accent-swatch[data-accent-color]').forEach(el => {
el.addEventListener('click', () => {
selectAccentColor(el.dataset.accentColor, el.dataset.accentHover);
});
});
const customRowEl = dropdown.querySelector('[data-accent-custom-row]');
const customInput = dropdown.querySelector('#accentCustomInput');
if (customRowEl && customInput) {
customRowEl.addEventListener('click', (e) => {
// The native color popup only opens from a user-initiated click on
// the <input>. Forward clicks on the row to the input — except when
// the input itself was the source (avoids re-entry).
if (e.target !== customInput) customInput.click();
});
customInput.addEventListener('click', (e) => e.stopPropagation());
customInput.addEventListener('change', () => {
selectAccentColor(customInput.value, lightenColor(customInput.value, 15));
});
}
} }
export function selectAccentColor(color, hover) { export function selectAccentColor(color, hover) {