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

@@ -5,14 +5,33 @@
import {
kcTestAutoRefresh, setKcTestAutoRefresh,
kcTestTargetId, setKcTestTargetId,
kcEditorInitialValues, setKcEditorInitialValues,
_kcNameManuallyEdited, set_kcNameManuallyEdited,
kcWebSockets,
PATTERN_RECT_BORDERS,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm, setupBackdropClose } from '../core/ui.js';
import { lockBody, showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
class KCEditorModal extends Modal {
constructor() {
super('kc-editor-modal');
}
snapshotValues() {
return {
name: document.getElementById('kc-editor-name').value,
source: document.getElementById('kc-editor-source').value,
fps: document.getElementById('kc-editor-fps').value,
interpolation: document.getElementById('kc-editor-interpolation').value,
smoothing: document.getElementById('kc-editor-smoothing').value,
patternTemplateId: document.getElementById('kc-editor-pattern-template').value,
};
}
}
const kcEditorModal = new KCEditorModal();
export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
const state = target.state || {};
@@ -391,19 +410,8 @@ export async function showKCEditor(targetId = null) {
patSelect.onchange = () => _autoGenerateKCName();
if (!targetId) _autoGenerateKCName();
setKcEditorInitialValues({
name: document.getElementById('kc-editor-name').value,
source: sourceSelect.value,
fps: document.getElementById('kc-editor-fps').value,
interpolation: document.getElementById('kc-editor-interpolation').value,
smoothing: document.getElementById('kc-editor-smoothing').value,
patternTemplateId: patSelect.value,
});
const modal = document.getElementById('kc-editor-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeKCEditorModal);
kcEditorModal.snapshot();
kcEditorModal.open();
document.getElementById('kc-editor-error').style.display = 'none';
setTimeout(() => document.getElementById('kc-editor-name').focus(), 100);
@@ -414,29 +422,15 @@ export async function showKCEditor(targetId = null) {
}
export function isKCEditorDirty() {
return (
document.getElementById('kc-editor-name').value !== kcEditorInitialValues.name ||
document.getElementById('kc-editor-source').value !== kcEditorInitialValues.source ||
document.getElementById('kc-editor-fps').value !== kcEditorInitialValues.fps ||
document.getElementById('kc-editor-interpolation').value !== kcEditorInitialValues.interpolation ||
document.getElementById('kc-editor-smoothing').value !== kcEditorInitialValues.smoothing ||
document.getElementById('kc-editor-pattern-template').value !== kcEditorInitialValues.patternTemplateId
);
return kcEditorModal.isDirty();
}
export async function closeKCEditorModal() {
if (isKCEditorDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseKCEditorModal();
await kcEditorModal.close();
}
export function forceCloseKCEditorModal() {
document.getElementById('kc-editor-modal').style.display = 'none';
document.getElementById('kc-editor-error').style.display = 'none';
unlockBody();
setKcEditorInitialValues({});
kcEditorModal.forceClose();
}
export async function saveKCEditor() {
@@ -447,17 +441,14 @@ export async function saveKCEditor() {
const interpolation = document.getElementById('kc-editor-interpolation').value;
const smoothing = parseFloat(document.getElementById('kc-editor-smoothing').value);
const patternTemplateId = document.getElementById('kc-editor-pattern-template').value;
const errorEl = document.getElementById('kc-editor-error');
if (!name) {
errorEl.textContent = t('kc.error.required');
errorEl.style.display = 'block';
kcEditorModal.showError(t('kc.error.required'));
return;
}
if (!patternTemplateId) {
errorEl.textContent = t('kc.error.no_pattern');
errorEl.style.display = 'block';
kcEditorModal.showError(t('kc.error.no_pattern'));
return;
}
@@ -497,13 +488,12 @@ export async function saveKCEditor() {
}
showToast(targetId ? t('kc.updated') : t('kc.created'), 'success');
forceCloseKCEditorModal();
kcEditorModal.forceClose();
// Use window.* to avoid circular import with targets.js
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} catch (error) {
console.error('Error saving KC target:', error);
errorEl.textContent = error.message;
errorEl.style.display = 'block';
kcEditorModal.showError(error.message);
}
}