Extract Modal base class and fix target editor defaults

Add core/modal.js with reusable Modal class that handles open/close,
body locking, backdrop close, dirty checking, error display, and a
static stack for ESC key management. Migrate all 13 modals across 8
feature files to use the base class, eliminating ~200 lines of
duplicated boilerplate. Replace manual ESC handler list in app.js
with Modal.closeTopmost(), fixing 3 modals that were previously
unreachable via ESC. Remove 5 unused initialValues variables from
state.js. Fix target editor to auto-select first device/source and
auto-generate name like the KC editor does.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 17:49:42 +03:00
parent 20d5a42e47
commit ed220a97e7
11 changed files with 341 additions and 329 deletions

View File

@@ -3,14 +3,50 @@
*/
import {
calibrationInitialValues, setCalibrationInitialValues,
calibrationTestState, EDGE_TEST_COLORS,
} from '../core/state.js';
import { API_BASE, getHeaders, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm } from '../core/ui.js';
import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { closeTutorial, startCalibrationTutorial } from './tutorials.js';
/* ── CalibrationModal subclass ────────────────────────────────── */
class CalibrationModal extends Modal {
constructor() {
super('calibration-modal');
}
snapshotValues() {
return {
start_position: this.$('cal-start-position').value,
layout: this.$('cal-layout').value,
offset: this.$('cal-offset').value,
top: this.$('cal-top-leds').value,
right: this.$('cal-right-leds').value,
bottom: this.$('cal-bottom-leds').value,
left: this.$('cal-left-leds').value,
spans: JSON.stringify(window.edgeSpans),
skip_start: this.$('cal-skip-start').value,
skip_end: this.$('cal-skip-end').value,
border_width: this.$('cal-border-width').value,
};
}
onForceClose() {
closeTutorial();
const deviceId = this.$('calibration-device-id').value;
if (deviceId) clearTestMode(deviceId);
if (window._calibrationResizeObserver) window._calibrationResizeObserver.disconnect();
const error = this.$('calibration-error');
if (error) error.style.display = 'none';
}
}
const calibModal = new CalibrationModal();
/* ── Public API (exported names unchanged) ────────────────────── */
export async function showCalibration(deviceId) {
try {
const [response, displaysResponse] = await Promise.all([
@@ -63,27 +99,12 @@ export async function showCalibration(deviceId) {
left: { start: calibration.span_left_start ?? 0, end: calibration.span_left_end ?? 1 },
};
setCalibrationInitialValues({
start_position: calibration.start_position,
layout: calibration.layout,
offset: String(calibration.offset || 0),
top: String(calibration.leds_top || 0),
right: String(calibration.leds_right || 0),
bottom: String(calibration.leds_bottom || 0),
left: String(calibration.leds_left || 0),
spans: JSON.stringify(window.edgeSpans),
skip_start: String(calibration.skip_leds_start || 0),
skip_end: String(calibration.skip_leds_end || 0),
border_width: String(calibration.border_width || 10),
});
calibrationTestState[device.id] = new Set();
updateCalibrationPreview();
const modal = document.getElementById('calibration-modal');
modal.style.display = 'flex';
lockBody();
calibModal.snapshot();
calibModal.open();
initSpanDrag();
requestAnimationFrame(() => {
@@ -109,40 +130,15 @@ export async function showCalibration(deviceId) {
}
function isCalibrationDirty() {
return (
document.getElementById('cal-start-position').value !== calibrationInitialValues.start_position ||
document.getElementById('cal-layout').value !== calibrationInitialValues.layout ||
document.getElementById('cal-offset').value !== calibrationInitialValues.offset ||
document.getElementById('cal-top-leds').value !== calibrationInitialValues.top ||
document.getElementById('cal-right-leds').value !== calibrationInitialValues.right ||
document.getElementById('cal-bottom-leds').value !== calibrationInitialValues.bottom ||
document.getElementById('cal-left-leds').value !== calibrationInitialValues.left ||
JSON.stringify(window.edgeSpans) !== calibrationInitialValues.spans ||
document.getElementById('cal-skip-start').value !== calibrationInitialValues.skip_start ||
document.getElementById('cal-skip-end').value !== calibrationInitialValues.skip_end ||
document.getElementById('cal-border-width').value !== calibrationInitialValues.border_width
);
return calibModal.isDirty();
}
export function forceCloseCalibrationModal() {
closeTutorial();
const deviceId = document.getElementById('calibration-device-id').value;
if (deviceId) clearTestMode(deviceId);
if (window._calibrationResizeObserver) window._calibrationResizeObserver.disconnect();
const modal = document.getElementById('calibration-modal');
const error = document.getElementById('calibration-error');
modal.style.display = 'none';
error.style.display = 'none';
unlockBody();
setCalibrationInitialValues({});
calibModal.forceClose();
}
export async function closeCalibrationModal() {
if (isCalibrationDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseCalibrationModal();
calibModal.close();
}
export function updateOffsetSkipLock() {
@@ -681,7 +677,7 @@ export async function saveCalibration() {
if (response.status === 401) { handle401Error(); return; }
if (response.ok) {
showToast('Calibration saved', 'success');
forceCloseCalibrationModal();
calibModal.forceClose();
window.loadDevices();
} else {
const errorData = await response.json();