Der Backup-Export/Import (Phase 7) war bislang nur ueber den zweiten Tab
im "KI-Ausflugsziele importieren"-Fenster erreichbar - der Toolbar-Button
nannte an keiner Stelle "Export" oder "Backup", weshalb die Funktion
praktisch nicht auffindbar war.
Neuer eigener Toolbar-Button "Daten sichern/wiederherstellen" oeffnet
das bestehende Import-Fenster jetzt direkt auf dem Backup-Tab
(openImport('backup')). Der bisherige Button bleibt fuer den
KI-JSON-Import zustaendig (openImport('ai')), keine Funktionsaenderung
an der eigentlichen Export-/Import-Logik.
578 lines
20 KiB
JavaScript
578 lines
20 KiB
JavaScript
// KI-JSON-Import + Backup-Export/Import (Phase 7, siehe anforderung.md
|
|
// Abschnitt "Vorgabe für das KI-JSON"). Ein Modal mit zwei Bereichen
|
|
// ("Tabs"): "KI-Ausflugsziele importieren" (Validierung/Vorschau/Übernahme
|
|
// von KI-generierten Ausflugszielen über js/import/importValidator.js) und
|
|
// "Backup sichern/wiederherstellen" (Export/Import des kompletten
|
|
// Datenbestands über js/import/backup.js). Beides zusammen in einem Modal,
|
|
// da beide Funktionen denselben Baustein "JSON einfügen ODER Datei
|
|
// hochladen" benötigen und ein zweites, fast identisches Modal unnötig
|
|
// redundant wäre.
|
|
//
|
|
// WICHTIG (Sicherheit): Diese Datei zeigt UNGEPRÜFTE FREMDDATEN an (rohe,
|
|
// von einer KI erzeugte oder aus einer Datei geladene Objekte, BEVOR sie
|
|
// über normalizePlace() übernommen wurden). Rendering deshalb ausschließlich
|
|
// über textContent/DOM-APIs (el()), niemals innerHTML - siehe js/utils/dom.js.
|
|
//
|
|
// Aufbau/Muster analog zu js/ui/form.js und js/ui/promptGenerator.js (Modal
|
|
// mit Backdrop, Escape/Backdrop-Klick schließt, Fokus-Management).
|
|
//
|
|
// API: EventMap.ui.initImport(containerId) initialisiert das Modal einmalig.
|
|
// EventMap.ui.openImport() öffnet es (immer im zurückgesetzten Zustand,
|
|
// startend auf dem KI-Import-Tab).
|
|
|
|
(function () {
|
|
const { el, clearChildren } = EventMap.utils;
|
|
|
|
let container = null;
|
|
let refs = null;
|
|
|
|
// Ergebnis der letzten Validierung/des letzten Parse-Vorgangs, damit die
|
|
// Bestätigungs-Buttons ohne erneutes Parsen darauf zugreifen können.
|
|
let lastAiValidation = null;
|
|
let lastBackupParse = null;
|
|
|
|
function readFileAsText(file, callback) {
|
|
const reader = new FileReader();
|
|
reader.onload = () => callback(String(reader.result || ''));
|
|
reader.onerror = () => callback(null);
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
/** Baut den gemeinsamen Eingabe-Baustein "JSON einfügen ODER Datei hochladen". */
|
|
function buildJsonInputGroup({ idPrefix, textareaPlaceholder }) {
|
|
const textarea = el('textarea', {
|
|
className: 'import-input__textarea',
|
|
attrs: { id: `${idPrefix}-textarea`, rows: '8', placeholder: textareaPlaceholder },
|
|
});
|
|
|
|
const fileInput = el('input', {
|
|
className: 'import-input__file',
|
|
attrs: { type: 'file', id: `${idPrefix}-file`, accept: '.json,application/json' },
|
|
on: {
|
|
change: (event) => {
|
|
const file = event.target.files && event.target.files[0];
|
|
if (!file) return;
|
|
readFileAsText(file, (text) => {
|
|
if (text !== null) {
|
|
textarea.value = text;
|
|
}
|
|
});
|
|
},
|
|
},
|
|
});
|
|
|
|
const wrapper = el('div', { className: 'import-input' }, [
|
|
el('label', { className: 'form-field__label', attrs: { for: `${idPrefix}-textarea` } }, ['JSON einfügen']),
|
|
textarea,
|
|
el('label', { className: 'form-field__label', attrs: { for: `${idPrefix}-file` } }, [
|
|
'… oder Datei hochladen',
|
|
]),
|
|
fileInput,
|
|
]);
|
|
|
|
return { wrapper, textarea, fileInput };
|
|
}
|
|
|
|
function resolveCategoryLabel(rawCategory) {
|
|
const { CATEGORIES, getCategoryById, getCategoryByLabel } = EventMap.config;
|
|
if (typeof rawCategory === 'string' && CATEGORIES.some((cat) => cat.id === rawCategory)) {
|
|
return getCategoryById(rawCategory).label;
|
|
}
|
|
return getCategoryByLabel(rawCategory).label;
|
|
}
|
|
|
|
function buildAiValidItem(entry, index, existingPlaces) {
|
|
const hasName = typeof entry.raw.name === 'string' && entry.raw.name.trim() !== '';
|
|
const name = hasName ? entry.raw.name.trim() : `Eintrag ${index + 1}`;
|
|
const categoryLabel = resolveCategoryLabel(entry.raw.category);
|
|
const duplicate = EventMap.import.findDuplicate(entry.raw, existingPlaces);
|
|
|
|
const detailNodes = [];
|
|
if (duplicate) {
|
|
detailNodes.push(
|
|
el('p', {
|
|
className: 'import-preview__duplicate-note',
|
|
text: `Mögliches Duplikat des bestehenden Eintrags "${duplicate.name}" - wird bei aktivem Duplikat-Filter übersprungen.`,
|
|
})
|
|
);
|
|
}
|
|
if (entry.warnings.length > 0) {
|
|
detailNodes.push(
|
|
el(
|
|
'ul',
|
|
{ className: 'import-preview__warnings' },
|
|
entry.warnings.map((warning) => el('li', { text: warning }))
|
|
)
|
|
);
|
|
}
|
|
|
|
return el('li', { className: 'import-preview__item import-preview__item--valid' }, [
|
|
el('div', { className: 'import-preview__item-header' }, [
|
|
el('span', { className: 'import-preview__name', text: name }),
|
|
el('span', { className: 'import-preview__category', text: categoryLabel }),
|
|
]),
|
|
...detailNodes,
|
|
]);
|
|
}
|
|
|
|
function buildAiRejectedItem(entry, index) {
|
|
const hasName =
|
|
entry.raw && typeof entry.raw === 'object' && typeof entry.raw.name === 'string' && entry.raw.name.trim() !== '';
|
|
const name = hasName ? entry.raw.name.trim() : `Eintrag ${index + 1}`;
|
|
|
|
return el('li', { className: 'import-preview__item import-preview__item--rejected' }, [
|
|
el('div', { className: 'import-preview__item-header' }, [
|
|
el('span', { className: 'import-preview__name', text: name }),
|
|
el('span', { className: 'import-preview__badge', text: 'Abgelehnt' }),
|
|
]),
|
|
el(
|
|
'ul',
|
|
{ className: 'import-preview__reasons' },
|
|
entry.reasons.map((reason) => el('li', { text: reason }))
|
|
),
|
|
]);
|
|
}
|
|
|
|
function showAiParseError(message) {
|
|
refs.aiParseError.textContent = message;
|
|
refs.aiParseError.hidden = false;
|
|
}
|
|
|
|
function showBackupParseError(message) {
|
|
refs.backupParseError.textContent = message;
|
|
refs.backupParseError.hidden = false;
|
|
}
|
|
|
|
function computeAiImportableCount() {
|
|
if (!lastAiValidation) return 0;
|
|
const existingPlaces = EventMap.store.getState().places;
|
|
const skipDuplicates = refs.aiSkipDuplicatesCheckbox.checked;
|
|
|
|
return lastAiValidation.validPlaces.filter((entry) => {
|
|
if (!skipDuplicates) return true;
|
|
return !EventMap.import.findDuplicate(entry.raw, existingPlaces);
|
|
}).length;
|
|
}
|
|
|
|
function updateAiImportButton() {
|
|
const count = computeAiImportableCount();
|
|
refs.aiImportBtn.textContent = `${count} Ausflugsziel${count === 1 ? '' : 'e'} importieren`;
|
|
refs.aiImportBtn.disabled = count === 0;
|
|
}
|
|
|
|
function renderAiPreview(result) {
|
|
const { warnings, validPlaces, rejectedPlaces } = result;
|
|
const withWarningsCount = validPlaces.filter((entry) => entry.warnings.length > 0).length;
|
|
|
|
refs.aiSummary.textContent =
|
|
`${validPlaces.length} gültig (davon ${withWarningsCount} mit Warnung), ${rejectedPlaces.length} abgelehnt.`;
|
|
|
|
clearChildren(refs.aiGlobalWarnings);
|
|
warnings.forEach((warning) => refs.aiGlobalWarnings.appendChild(el('li', { text: warning })));
|
|
refs.aiGlobalWarnings.hidden = warnings.length === 0;
|
|
|
|
const existingPlaces = EventMap.store.getState().places;
|
|
|
|
clearChildren(refs.aiEntriesList);
|
|
validPlaces.forEach((entry, index) => {
|
|
refs.aiEntriesList.appendChild(buildAiValidItem(entry, index, existingPlaces));
|
|
});
|
|
rejectedPlaces.forEach((entry, index) => {
|
|
refs.aiEntriesList.appendChild(buildAiRejectedItem(entry, index));
|
|
});
|
|
|
|
updateAiImportButton();
|
|
refs.aiPreview.hidden = false;
|
|
}
|
|
|
|
function handleAiValidateClick() {
|
|
refs.aiParseError.hidden = true;
|
|
refs.aiParseError.textContent = '';
|
|
refs.aiPreview.hidden = true;
|
|
refs.aiStatus.textContent = '';
|
|
lastAiValidation = null;
|
|
|
|
const text = refs.aiTextarea.value.trim();
|
|
if (!text) {
|
|
showAiParseError('Bitte JSON einfügen oder eine Datei hochladen.');
|
|
return;
|
|
}
|
|
|
|
const result = EventMap.import.parseAndValidate(text);
|
|
if (!result.success) {
|
|
showAiParseError(result.error);
|
|
return;
|
|
}
|
|
|
|
lastAiValidation = result;
|
|
renderAiPreview(result);
|
|
}
|
|
|
|
function handleAiImportClick() {
|
|
if (!lastAiValidation) return;
|
|
|
|
const state = EventMap.store.getState();
|
|
const skipDuplicates = refs.aiSkipDuplicatesCheckbox.checked;
|
|
|
|
const toImport = [];
|
|
for (const entry of lastAiValidation.validPlaces) {
|
|
if (skipDuplicates && EventMap.import.findDuplicate(entry.raw, state.places)) {
|
|
continue;
|
|
}
|
|
toImport.push(EventMap.models.normalizePlace(entry.raw));
|
|
}
|
|
|
|
if (toImport.length === 0) {
|
|
refs.aiStatus.textContent =
|
|
'Keine Ausflugsziele übernommen (alle gültigen Einträge wurden als Duplikat übersprungen).';
|
|
return;
|
|
}
|
|
|
|
EventMap.store.setState({ places: [...state.places, ...toImport] });
|
|
EventMap.store.persistPlaces();
|
|
|
|
refs.aiStatus.textContent = `${toImport.length} Ausflugsziel${toImport.length === 1 ? '' : 'e'} erfolgreich importiert.`;
|
|
refs.aiPreview.hidden = true;
|
|
refs.aiTextarea.value = '';
|
|
refs.aiFileInput.value = '';
|
|
lastAiValidation = null;
|
|
}
|
|
|
|
function handleBackupCheckClick() {
|
|
refs.backupParseError.hidden = true;
|
|
refs.backupParseError.textContent = '';
|
|
refs.backupPreview.hidden = true;
|
|
refs.backupStatus.textContent = '';
|
|
lastBackupParse = null;
|
|
|
|
const text = refs.backupTextarea.value.trim();
|
|
if (!text) {
|
|
showBackupParseError('Bitte JSON einfügen oder eine Backup-Datei hochladen.');
|
|
return;
|
|
}
|
|
|
|
const result = EventMap.import.parseBackup(text);
|
|
if (!result.success) {
|
|
showBackupParseError(result.error);
|
|
return;
|
|
}
|
|
|
|
lastBackupParse = result;
|
|
const existingCount = EventMap.store.getState().places.length;
|
|
refs.backupSummary.textContent =
|
|
`Backup enthält ${result.places.length} Ausflugsziel(e). Aktuell gespeichert: ${existingCount}.`;
|
|
refs.backupPreview.hidden = false;
|
|
}
|
|
|
|
function finishBackupImport(message) {
|
|
refs.backupStatus.textContent = message;
|
|
refs.backupPreview.hidden = true;
|
|
refs.backupTextarea.value = '';
|
|
refs.backupFileInput.value = '';
|
|
lastBackupParse = null;
|
|
}
|
|
|
|
function handleBackupMergeClick() {
|
|
if (!lastBackupParse) return;
|
|
const state = EventMap.store.getState();
|
|
const merged = EventMap.import.mergeBackupPlaces(state.places, lastBackupParse.places);
|
|
|
|
EventMap.store.setState({ places: merged });
|
|
EventMap.store.persistPlaces();
|
|
finishBackupImport(`${lastBackupParse.places.length} Ausflugsziel(e) ergänzt. Gesamtbestand: ${merged.length}.`);
|
|
}
|
|
|
|
function handleBackupReplaceClick() {
|
|
if (!lastBackupParse) return;
|
|
const existingCount = EventMap.store.getState().places.length;
|
|
const confirmed = window.confirm(
|
|
`Wirklich alle ${existingCount} bestehenden Ausflugsziele durch die ${lastBackupParse.places.length} ` +
|
|
'Ausflugsziele aus dem Backup ersetzen? Dies kann nicht rückgängig gemacht werden.'
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
EventMap.store.setState({ places: lastBackupParse.places });
|
|
EventMap.store.persistPlaces();
|
|
finishBackupImport(`${lastBackupParse.places.length} Ausflugsziel(e) übernommen (bestehende Daten ersetzt).`);
|
|
}
|
|
|
|
function setActiveTab(tab) {
|
|
const isAi = tab === 'ai';
|
|
refs.aiPanel.hidden = !isAi;
|
|
refs.backupPanel.hidden = isAi;
|
|
refs.tabAiBtn.setAttribute('aria-selected', String(isAi));
|
|
refs.tabBackupBtn.setAttribute('aria-selected', String(!isAi));
|
|
refs.tabAiBtn.classList.toggle('import-modal__tab--active', isAi);
|
|
refs.tabBackupBtn.classList.toggle('import-modal__tab--active', !isAi);
|
|
}
|
|
|
|
function buildSkeleton(rootContainer) {
|
|
refs = {};
|
|
|
|
refs.backdrop = el('div', {
|
|
className: 'import-modal__backdrop',
|
|
on: { click: () => closeImport() },
|
|
});
|
|
|
|
refs.closeBtn = el(
|
|
'button',
|
|
{
|
|
className: 'import-modal__close',
|
|
attrs: { type: 'button', 'aria-label': 'Import-Fenster schließen' },
|
|
on: { click: () => closeImport() },
|
|
},
|
|
['✕']
|
|
);
|
|
|
|
refs.title = el('h2', {
|
|
className: 'import-modal__title',
|
|
attrs: { id: 'import-modal-title' },
|
|
text: 'Ausflugsziele importieren',
|
|
});
|
|
|
|
refs.tabAiBtn = el(
|
|
'button',
|
|
{
|
|
className: 'import-modal__tab',
|
|
attrs: { type: 'button', role: 'tab', id: 'import-tab-ai', 'aria-controls': 'import-panel-ai' },
|
|
on: { click: () => setActiveTab('ai') },
|
|
},
|
|
['KI-Ausflugsziele importieren']
|
|
);
|
|
|
|
refs.tabBackupBtn = el(
|
|
'button',
|
|
{
|
|
className: 'import-modal__tab',
|
|
attrs: { type: 'button', role: 'tab', id: 'import-tab-backup', 'aria-controls': 'import-panel-backup' },
|
|
on: { click: () => setActiveTab('backup') },
|
|
},
|
|
['Backup sichern/wiederherstellen']
|
|
);
|
|
|
|
refs.tabList = el(
|
|
'div',
|
|
{ className: 'import-modal__tabs', attrs: { role: 'tablist', 'aria-label': 'Import-Bereich wählen' } },
|
|
[refs.tabAiBtn, refs.tabBackupBtn]
|
|
);
|
|
|
|
// --- KI-Import-Tab ---
|
|
|
|
const aiInput = buildJsonInputGroup({
|
|
idPrefix: 'import-ai',
|
|
textareaPlaceholder: 'Hier das von der KI erzeugte JSON einfügen …',
|
|
});
|
|
refs.aiTextarea = aiInput.textarea;
|
|
refs.aiFileInput = aiInput.fileInput;
|
|
|
|
refs.aiParseError = el('p', { className: 'import-error', attrs: { role: 'alert' } });
|
|
refs.aiParseError.hidden = true;
|
|
|
|
refs.aiValidateBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--primary', attrs: { type: 'button' }, on: { click: handleAiValidateClick } },
|
|
['Prüfen']
|
|
);
|
|
|
|
refs.aiSummary = el('p', { className: 'import-preview__summary' });
|
|
refs.aiGlobalWarnings = el('ul', { className: 'import-preview__global-warnings' });
|
|
refs.aiEntriesList = el('ul', { className: 'import-preview__list' });
|
|
|
|
refs.aiSkipDuplicatesCheckbox = el('input', {
|
|
attrs: { type: 'checkbox', id: 'import-ai-skip-duplicates' },
|
|
on: { change: updateAiImportButton },
|
|
});
|
|
refs.aiSkipDuplicatesCheckbox.checked = true;
|
|
|
|
refs.aiImportBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--primary', attrs: { type: 'button' }, on: { click: handleAiImportClick } },
|
|
['0 Ausflugsziele importieren']
|
|
);
|
|
refs.aiStatus = el('p', { className: 'import-status', attrs: { role: 'status' } });
|
|
|
|
refs.aiPreview = el('div', { className: 'import-preview' }, [
|
|
refs.aiSummary,
|
|
refs.aiGlobalWarnings,
|
|
refs.aiEntriesList,
|
|
el('label', { className: 'form-checkbox import-preview__skip-duplicates' }, [
|
|
refs.aiSkipDuplicatesCheckbox,
|
|
'Erkannte Duplikate (Name + Standort stimmen mit einem bestehenden Eintrag überein) überspringen',
|
|
]),
|
|
refs.aiImportBtn,
|
|
]);
|
|
refs.aiPreview.hidden = true;
|
|
|
|
refs.aiPanel = el(
|
|
'div',
|
|
{ className: 'import-panel', attrs: { id: 'import-panel-ai', role: 'tabpanel', 'aria-labelledby': 'import-tab-ai' } },
|
|
[
|
|
el('p', {
|
|
className: 'import-panel__intro',
|
|
text:
|
|
'Füge das JSON aus der KI-Antwort ein oder lade eine gespeicherte Datei hoch. Jeder Eintrag wird ' +
|
|
'einzeln geprüft, bevor er übernommen wird - ungültige Einträge werden abgelehnt und angezeigt.',
|
|
}),
|
|
aiInput.wrapper,
|
|
refs.aiParseError,
|
|
refs.aiValidateBtn,
|
|
refs.aiPreview,
|
|
refs.aiStatus,
|
|
]
|
|
);
|
|
|
|
// --- Backup-Tab ---
|
|
|
|
refs.backupExportBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--secondary', attrs: { type: 'button' }, on: { click: () => EventMap.import.exportBackup() } },
|
|
['💾 Backup exportieren']
|
|
);
|
|
|
|
const backupInput = buildJsonInputGroup({
|
|
idPrefix: 'import-backup',
|
|
textareaPlaceholder: 'Hier den Inhalt einer eventmap-backup-*.json-Datei einfügen …',
|
|
});
|
|
refs.backupTextarea = backupInput.textarea;
|
|
refs.backupFileInput = backupInput.fileInput;
|
|
|
|
refs.backupParseError = el('p', { className: 'import-error', attrs: { role: 'alert' } });
|
|
refs.backupParseError.hidden = true;
|
|
|
|
refs.backupCheckBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--primary', attrs: { type: 'button' }, on: { click: handleBackupCheckClick } },
|
|
['Backup prüfen']
|
|
);
|
|
|
|
refs.backupSummary = el('p', { className: 'import-preview__summary' });
|
|
refs.backupMergeBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--primary', attrs: { type: 'button' }, on: { click: handleBackupMergeClick } },
|
|
['Bestehende Daten ergänzen']
|
|
);
|
|
refs.backupReplaceBtn = el(
|
|
'button',
|
|
{ className: 'btn btn--danger', attrs: { type: 'button' }, on: { click: handleBackupReplaceClick } },
|
|
['Bestehende Daten ersetzen']
|
|
);
|
|
refs.backupStatus = el('p', { className: 'import-status', attrs: { role: 'status' } });
|
|
|
|
refs.backupPreview = el('div', { className: 'import-preview' }, [
|
|
refs.backupSummary,
|
|
el('div', { className: 'import-preview__actions' }, [refs.backupMergeBtn, refs.backupReplaceBtn]),
|
|
]);
|
|
refs.backupPreview.hidden = true;
|
|
|
|
refs.backupPanel = el(
|
|
'div',
|
|
{ className: 'import-panel', attrs: { id: 'import-panel-backup', role: 'tabpanel', 'aria-labelledby': 'import-tab-backup' } },
|
|
[
|
|
el('section', { className: 'import-panel__section' }, [
|
|
el('h3', { className: 'place-form__section-label', text: 'Backup exportieren' }),
|
|
el('p', {
|
|
className: 'import-panel__intro',
|
|
text: 'Speichert den gesamten aktuellen Datenbestand als JSON-Datei, z.B. zur Sicherung oder zur Übertragung auf ein anderes Gerät.',
|
|
}),
|
|
refs.backupExportBtn,
|
|
]),
|
|
el('section', { className: 'import-panel__section' }, [
|
|
el('h3', { className: 'place-form__section-label', text: 'Backup wiederherstellen' }),
|
|
el('p', {
|
|
className: 'import-panel__intro',
|
|
text: 'Fügt ein zuvor exportiertes Backup ein oder lädt es hoch und wählt danach, ob die bestehenden Daten ersetzt oder ergänzt werden sollen.',
|
|
}),
|
|
backupInput.wrapper,
|
|
refs.backupParseError,
|
|
refs.backupCheckBtn,
|
|
refs.backupPreview,
|
|
refs.backupStatus,
|
|
]),
|
|
]
|
|
);
|
|
|
|
refs.panel = el(
|
|
'div',
|
|
{
|
|
className: 'import-modal__panel',
|
|
attrs: { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'import-modal-title' },
|
|
},
|
|
[refs.closeBtn, refs.title, refs.tabList, refs.aiPanel, refs.backupPanel]
|
|
);
|
|
|
|
rootContainer.appendChild(refs.backdrop);
|
|
rootContainer.appendChild(refs.panel);
|
|
}
|
|
|
|
function resetModalState() {
|
|
lastAiValidation = null;
|
|
lastBackupParse = null;
|
|
|
|
refs.aiTextarea.value = '';
|
|
refs.aiFileInput.value = '';
|
|
refs.aiParseError.hidden = true;
|
|
refs.aiPreview.hidden = true;
|
|
refs.aiStatus.textContent = '';
|
|
refs.aiSkipDuplicatesCheckbox.checked = true;
|
|
|
|
refs.backupTextarea.value = '';
|
|
refs.backupFileInput.value = '';
|
|
refs.backupParseError.hidden = true;
|
|
refs.backupPreview.hidden = true;
|
|
refs.backupStatus.textContent = '';
|
|
}
|
|
|
|
/**
|
|
* Öffnet das Import-/Backup-Fenster im zurückgesetzten Zustand.
|
|
* @param {'ai'|'backup'} [initialTab] Welcher Tab beim Öffnen aktiv sein
|
|
* soll - 'backup' für den direkten Einstieg über den Sichern/
|
|
* Wiederherstellen-Toolbar-Button (Export war sonst innerhalb des
|
|
* KI-Import-Fensters schwer auffindbar).
|
|
*/
|
|
function openImport(initialTab = 'ai') {
|
|
if (!container || !refs) {
|
|
console.error('EventMap: Import-Fenster wurde noch nicht initialisiert (initImport()).');
|
|
return;
|
|
}
|
|
|
|
resetModalState();
|
|
setActiveTab(initialTab === 'backup' ? 'backup' : 'ai');
|
|
container.hidden = false;
|
|
document.body.classList.add('import-modal-open');
|
|
if (initialTab === 'backup') {
|
|
refs.backupTextarea.focus();
|
|
} else {
|
|
refs.aiTextarea.focus();
|
|
}
|
|
}
|
|
|
|
function closeImport() {
|
|
if (!container || container.hidden) return;
|
|
container.hidden = true;
|
|
document.body.classList.remove('import-modal-open');
|
|
}
|
|
|
|
/** Initialisiert das Import-/Backup-Fenster im Element mit der übergebenen id. */
|
|
function initImport(containerId) {
|
|
const containerEl = document.getElementById(containerId);
|
|
if (!containerEl) {
|
|
console.error(`EventMap: Import-Container #${containerId} nicht gefunden.`);
|
|
return;
|
|
}
|
|
|
|
container = containerEl;
|
|
clearChildren(container);
|
|
buildSkeleton(container);
|
|
setActiveTab('ai');
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape' && !container.hidden) {
|
|
closeImport();
|
|
}
|
|
});
|
|
}
|
|
|
|
EventMap.ui.initImport = initImport;
|
|
EventMap.ui.openImport = openImport;
|
|
})();
|