Phase 3: Formular zum Anlegen und Bearbeiten

Neues Modal (js/ui/form.js) mit allen Feldern aus anforderung.md
Abschnitt 6: Name, Kategorie, Kurz-/Volltext-Beschreibung, Adresse, Ort,
Koordinaten, Altersgruppe, Kosten, Oeffnungszeiten, Aufenthaltsdauer,
Ausstattungs-Checkboxen, Indoor/Outdoor, Bild-URL, Website, Tags.

Validierung: Pflichtfelder (Name, Kategorie, Koordinaten), Koordinaten-
Bereichspruefung, Alter min<=max, Bild-/Website-URL nur http/https.
Fehler erscheinen inline je Feld (aria-invalid/aria-describedby), erstes
fehlerhaftes Feld wird fokussiert.

EventMap.ui.openPlaceForm(place) dient als Einstiegspunkt: ohne Argument
zum Neuanlegen, mit Place-Objekt zum Bearbeiten. Speichern aktualisiert
den Store mit neuer Array-Referenz (Karte/Liste reagieren automatisch)
und persistiert nach LocalStorage.

Verdrahtung: neuer "+ Neues Ausflugsziel hinzufuegen"-Button in der
Karten-Toolbar; der bisherige Bearbeiten-Platzhalter in der
Detailansicht (js/ui/detail.js) oeffnet jetzt das Formular vorausgefuellt
mit dem aktuell angezeigten Ausflugsziel.
This commit is contained in:
2026-07-24 11:43:01 +02:00
parent 2812e03530
commit 10f9170d61
6 changed files with 809 additions and 3 deletions
+175
View File
@@ -0,0 +1,175 @@
/* Formular-Modal zum Anlegen/Bearbeiten eines Ausflugsziels (Phase 3):
Vollbild auf Mobile, zentriertes Panel ab 1024px (siehe Breakpoint in
css/responsive.css). Struktur wird von js/ui/form.js in
#place-form-modal eingehängt. Grundgerüst analog zu css/detail.css. */
.place-form-modal {
position: fixed;
inset: 0;
z-index: 2100;
}
.place-form-modal[hidden] {
display: none;
}
.place-form-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
}
.place-form-modal__panel {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: var(--color-surface);
overflow-y: auto;
}
.place-form-modal__close {
position: absolute;
top: var(--space-sm);
right: var(--space-sm);
z-index: 1;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 1rem;
line-height: 1;
cursor: pointer;
}
.place-form__title {
padding: var(--space-lg) var(--space-lg) 0;
font-size: 1.3rem;
font-weight: 700;
}
.place-form {
padding: var(--space-md) var(--space-lg) var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-lg);
}
.place-form__section {
display: flex;
flex-direction: column;
gap: var(--space-md);
border: none;
border-top: 1px solid var(--color-border);
padding: var(--space-md) 0 0;
margin: 0;
}
.place-form__section:first-of-type {
border-top: none;
padding-top: 0;
}
.place-form__section-label {
padding: 0;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.02em;
color: var(--color-text-muted);
}
.place-form__row {
display: flex;
flex-wrap: wrap;
gap: var(--space-md);
}
.place-form__row .form-field {
flex: 1 1 160px;
}
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.form-field__label {
font-size: 0.85rem;
font-weight: 600;
}
.form-field__required {
color: var(--color-danger);
}
.form-field input,
.form-field select,
.form-field textarea {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: var(--space-sm);
font: inherit;
background: var(--color-surface);
color: var(--color-text);
width: 100%;
}
.form-field textarea {
resize: vertical;
min-height: 80px;
}
.form-field input[aria-invalid='true'],
.form-field select[aria-invalid='true'],
.form-field textarea[aria-invalid='true'] {
border-color: var(--color-danger);
}
.form-field__error {
color: var(--color-danger);
font-size: 0.8rem;
}
.form-field__error[hidden] {
display: none;
}
.place-form__checkbox-group {
display: flex;
flex-wrap: wrap;
gap: var(--space-md);
}
.form-checkbox {
display: flex;
align-items: center;
gap: var(--space-xs);
font-size: 0.9rem;
cursor: pointer;
}
.place-form__actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
padding-top: var(--space-md);
border-top: 1px solid var(--color-border);
}
@media (min-width: 1024px) {
.place-form-modal__panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(720px, 90vw);
height: min(90vh, 960px);
border-radius: var(--radius-md);
box-shadow: var(--shadow-card);
}
}
+3
View File
@@ -39,6 +39,9 @@
top: var(--space-sm);
left: var(--space-sm);
z-index: 1000;
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.map-panel__map {
+6
View File
@@ -14,6 +14,7 @@
<link rel="stylesheet" href="css/layout.css" />
<link rel="stylesheet" href="css/components.css" />
<link rel="stylesheet" href="css/detail.css" />
<link rel="stylesheet" href="css/form.css" />
<link rel="stylesheet" href="css/responsive.css" />
</head>
<body>
@@ -30,6 +31,9 @@
<button id="locate-btn" type="button" class="btn btn--secondary">
Meinen Standort verwenden
</button>
<button id="add-place-btn" type="button" class="btn btn--primary">
+ Neues Ausflugsziel hinzufügen
</button>
</div>
<div id="map" class="map-panel__map">
<p class="map-panel__loading">Karte wird geladen…</p>
@@ -45,6 +49,7 @@
</main>
<div id="detail-modal" class="detail-modal" hidden></div>
<div id="place-form-modal" class="place-form-modal" hidden></div>
<script src="vendor/leaflet/leaflet.js"></script>
<script src="vendor/leaflet.markercluster/leaflet.markercluster.js"></script>
@@ -69,6 +74,7 @@
<script src="js/map/mapController.js"></script>
<script src="js/ui/list.js"></script>
<script src="js/ui/detail.js"></script>
<script src="js/ui/form.js"></script>
<script src="js/main.js"></script>
</body>
</html>
+12 -1
View File
@@ -3,7 +3,7 @@
(function () {
const { initStore } = EventMap.store;
const { initMap, centerOnCoords, requestUserLocation } = EventMap.map;
const { initList, initDetail } = EventMap.ui;
const { initList, initDetail, initForm } = EventMap.ui;
function showFatalError(message) {
const status = document.getElementById('app-status');
@@ -39,12 +39,23 @@
});
}
function setupAddPlaceButton() {
const button = document.getElementById('add-place-btn');
if (!button) return;
button.addEventListener('click', () => {
EventMap.ui.openPlaceForm();
});
}
async function bootstrap() {
await initStore();
initMap('map');
initList('place-list');
initDetail('detail-modal');
initForm('place-form-modal');
setupGeolocationButton();
setupAddPlaceButton();
}
bootstrap().catch((err) => {
+9 -2
View File
@@ -37,6 +37,7 @@
let refs = null;
let miniMap = null;
let lastSelectedPlaceId = null;
let currentPlace = null;
function formatAgeRecommendation(ageRecommendation) {
const { min, max } = ageRecommendation || {};
@@ -115,8 +116,13 @@
attrs: { type: 'button', id: 'detail-edit-btn' },
on: {
click: () => {
// TODO Phase 3: Formular zum Bearbeiten des Ausflugsziels öffnen.
window.alert('Bearbeiten ist ab Phase 3 verfügbar.');
// Detailansicht schließen und das Bearbeiten-Formular öffnen
// (js/ui/form.js, wird nach diesem Modul geladen, daher hier zur
// Aufrufzeit über den globalen Namespace referenziert statt beim
// Datei-Laden destrukturiert).
const place = currentPlace;
setState({ selectedPlaceId: null });
EventMap.ui.openPlaceForm(place);
},
},
},
@@ -298,6 +304,7 @@
}
function renderContent(place) {
currentPlace = place;
const category = getPlaceCategory(place);
refs.name.textContent = place.name;
+604
View File
@@ -0,0 +1,604 @@
// Formular (Modal) zum manuellen Anlegen und Bearbeiten eines
// Ausflugsziels (Phase 3, siehe anforderung.md Abschnitt 6).
//
// API: EventMap.ui.initForm(containerId) initialisiert das Modal einmalig
// (analog zu js/ui/detail.js). EventMap.ui.openPlaceForm(placeOrNull) öffnet
// es: ohne Argument im "Neu anlegen"-Modus, mit einem Place-Objekt im
// "Bearbeiten"-Modus (überschreibt den bestehenden Eintrag statt einen
// neuen anzulegen).
//
// Rendering ausschließlich über textContent/DOM-APIs (el()), niemals
// innerHTML mit Formular-/Place-Daten (siehe js/utils/dom.js).
(function () {
const { getState, setState, persistPlaces } = EventMap.store;
const { normalizePlace } = EventMap.models;
const { COST_TYPES, ENVIRONMENT_TYPES } = EventMap.models;
const { CATEGORIES } = EventMap.config;
const { el, isSafeHttpUrl, isValidCoordinate } = EventMap.utils;
const COST_LABELS = {
free: 'Kostenlos',
paid: 'Kostenpflichtig',
mixed: 'Teilweise kostenpflichtig',
unknown: 'Keine Angabe',
};
const ENVIRONMENT_LABELS = {
indoor: 'Drinnen (Indoor)',
outdoor: 'Draußen (Outdoor)',
mixed: 'Drinnen & draußen',
};
// Felder, für die ein eigenes Fehler-Element existiert (siehe fieldWrapper()).
const ERROR_FIELDS = [
'name',
'category',
'latitude',
'longitude',
'ageMin',
'ageMax',
'duration',
'image',
'website',
];
let container = null;
let refs = null;
let editingPlace = null;
function fieldWrapper(id, labelText, inputNode, { required = false } = {}) {
inputNode.id = id;
if (required) {
inputNode.setAttribute('required', '');
inputNode.setAttribute('aria-required', 'true');
}
const errorEl = el('p', {
className: 'form-field__error',
attrs: { id: `${id}-error`, role: 'alert' },
});
errorEl.hidden = true;
inputNode.setAttribute('aria-describedby', `${id}-error`);
const labelChildren = [labelText];
if (required) {
labelChildren.push(
el('span', { className: 'form-field__required', attrs: { 'aria-hidden': 'true' } }, [' *'])
);
}
const wrapper = el('div', { className: 'form-field' }, [
el('label', { className: 'form-field__label', attrs: { for: id } }, labelChildren),
inputNode,
errorEl,
]);
return { wrapper, input: inputNode, errorEl };
}
function textInput(type = 'text', extraAttrs = {}) {
return el('input', { attrs: { type, ...extraAttrs } });
}
function buildCategorySelect() {
const select = el('select', {}, [
el('option', { attrs: { value: '' }, text: '-- Bitte wählen --' }),
...CATEGORIES.map((cat) =>
el('option', { attrs: { value: cat.id }, text: `${cat.icon} ${cat.label}` })
),
]);
return select;
}
function buildCostTypeSelect() {
return el(
'select',
{},
COST_TYPES.map((type) =>
el('option', { attrs: { value: type }, text: COST_LABELS[type] || type })
)
);
}
function buildEnvironmentSelect() {
return el(
'select',
{},
ENVIRONMENT_TYPES.map((type) =>
el('option', { attrs: { value: type }, text: ENVIRONMENT_LABELS[type] || type })
)
);
}
function checkboxField(id, labelText) {
const input = el('input', { attrs: { type: 'checkbox', id } });
const wrapper = el('label', { className: 'form-checkbox', attrs: { for: id } }, [
input,
labelText,
]);
return { wrapper, input };
}
function section(legendText, children) {
return el('fieldset', { className: 'place-form__section' }, [
el('legend', { className: 'place-form__section-label', text: legendText }),
...children,
]);
}
function row(children) {
return el('div', { className: 'place-form__row' }, children);
}
function buildSkeleton(rootContainer) {
const nodeRefs = {};
nodeRefs.backdrop = el('div', {
className: 'place-form-modal__backdrop',
on: { click: () => closeForm() },
});
nodeRefs.closeBtn = el(
'button',
{
className: 'place-form-modal__close',
attrs: { type: 'button', 'aria-label': 'Formular schließen' },
on: { click: () => closeForm() },
},
['✕']
);
nodeRefs.title = el('h2', {
className: 'place-form__title',
attrs: { id: 'place-form-title' },
});
// Basisdaten
const nameField = fieldWrapper('pf-name', 'Name', textInput('text'), { required: true });
const categoryField = fieldWrapper('pf-category', 'Kategorie', buildCategorySelect(), {
required: true,
});
const shortDescriptionField = fieldWrapper(
'pf-short-description',
'Kurzbeschreibung',
textInput('text')
);
const descriptionField = fieldWrapper(
'pf-description',
'Ausführliche Beschreibung',
el('textarea', {})
);
// Adresse / Position
const addressField = fieldWrapper('pf-address', 'Adresse', textInput('text'));
const cityField = fieldWrapper('pf-city', 'Ort', textInput('text'));
const latitudeField = fieldWrapper(
'pf-latitude',
'Breitengrad',
textInput('number', { step: 'any' }),
{ required: true }
);
const longitudeField = fieldWrapper(
'pf-longitude',
'Längengrad',
textInput('number', { step: 'any' }),
{ required: true }
);
// Altersgruppe / Kosten / Öffnungszeiten / Dauer
const ageMinField = fieldWrapper(
'pf-age-min',
'Altersgruppe ab (Jahre)',
textInput('number', { step: '1', min: '0' })
);
const ageMaxField = fieldWrapper(
'pf-age-max',
'Altersgruppe bis (Jahre)',
textInput('number', { step: '1', min: '0' })
);
const costTypeField = fieldWrapper('pf-cost-type', 'Kosten', buildCostTypeSelect());
const costDescriptionField = fieldWrapper(
'pf-cost-description',
'Kosten Details',
textInput('text')
);
const openingHoursField = fieldWrapper('pf-opening-hours', 'Öffnungszeiten', textInput('text'));
const durationField = fieldWrapper(
'pf-duration',
'Aufenthaltsdauer (Minuten)',
textInput('number', { step: '1', min: '0' })
);
// Ausstattung
const strollerCheckbox = checkboxField('pf-facility-stroller', 'Kinderwagen geeignet');
const wheelchairCheckbox = checkboxField('pf-facility-wheelchair', 'Barrierefrei');
const toiletsCheckbox = checkboxField('pf-facility-toilets', 'Toiletten vorhanden');
const parkingCheckbox = checkboxField('pf-facility-parking', 'Parkplatz vorhanden');
const restaurantCheckbox = checkboxField('pf-facility-restaurant', 'Gastronomie vorhanden');
const environmentField = fieldWrapper('pf-environment', 'Indoor / Outdoor', buildEnvironmentSelect());
// Weitere Informationen
const imageField = fieldWrapper('pf-image', 'Bild-URL', textInput('url'));
const websiteField = fieldWrapper('pf-website', 'Website', textInput('url'));
const tagsField = fieldWrapper(
'pf-tags',
'Tags (kommagetrennt)',
textInput('text')
);
nodeRefs.cancelBtn = el(
'button',
{ className: 'btn btn--secondary', attrs: { type: 'button' }, on: { click: () => closeForm() } },
['Abbrechen']
);
nodeRefs.submitBtn = el(
'button',
{ className: 'btn btn--primary', attrs: { type: 'submit' } },
['Ausflugsziel speichern']
);
nodeRefs.form = el(
'form',
{ className: 'place-form', attrs: { id: 'place-form', novalidate: '' }, on: { submit: handleSubmit } },
[
section('Basisdaten', [
nameField.wrapper,
categoryField.wrapper,
shortDescriptionField.wrapper,
descriptionField.wrapper,
]),
section('Adresse & Position', [
addressField.wrapper,
cityField.wrapper,
row([latitudeField.wrapper, longitudeField.wrapper]),
]),
section('Altersgruppe, Kosten & Zeiten', [
row([ageMinField.wrapper, ageMaxField.wrapper]),
row([costTypeField.wrapper, costDescriptionField.wrapper]),
openingHoursField.wrapper,
durationField.wrapper,
]),
section('Ausstattung', [
el('div', { className: 'place-form__checkbox-group' }, [
strollerCheckbox.wrapper,
wheelchairCheckbox.wrapper,
toiletsCheckbox.wrapper,
parkingCheckbox.wrapper,
restaurantCheckbox.wrapper,
]),
environmentField.wrapper,
]),
section('Weitere Informationen', [
imageField.wrapper,
websiteField.wrapper,
tagsField.wrapper,
]),
el('div', { className: 'place-form__actions' }, [nodeRefs.cancelBtn, nodeRefs.submitBtn]),
]
);
nodeRefs.panel = el(
'div',
{
className: 'place-form-modal__panel',
attrs: { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'place-form-title' },
},
[nodeRefs.closeBtn, nodeRefs.title, nodeRefs.form]
);
rootContainer.appendChild(nodeRefs.backdrop);
rootContainer.appendChild(nodeRefs.panel);
// Flache Referenzen für Lesen/Schreiben/Fehleranzeige der Feldwerte.
nodeRefs.name = nameField.input;
nodeRefs.nameError = nameField.errorEl;
nodeRefs.category = categoryField.input;
nodeRefs.categoryError = categoryField.errorEl;
nodeRefs.shortDescription = shortDescriptionField.input;
nodeRefs.description = descriptionField.input;
nodeRefs.address = addressField.input;
nodeRefs.city = cityField.input;
nodeRefs.latitude = latitudeField.input;
nodeRefs.latitudeError = latitudeField.errorEl;
nodeRefs.longitude = longitudeField.input;
nodeRefs.longitudeError = longitudeField.errorEl;
nodeRefs.ageMin = ageMinField.input;
nodeRefs.ageMinError = ageMinField.errorEl;
nodeRefs.ageMax = ageMaxField.input;
nodeRefs.ageMaxError = ageMaxField.errorEl;
nodeRefs.costType = costTypeField.input;
nodeRefs.costDescription = costDescriptionField.input;
nodeRefs.openingHours = openingHoursField.input;
nodeRefs.duration = durationField.input;
nodeRefs.durationError = durationField.errorEl;
nodeRefs.stroller = strollerCheckbox.input;
nodeRefs.wheelchair = wheelchairCheckbox.input;
nodeRefs.toilets = toiletsCheckbox.input;
nodeRefs.parking = parkingCheckbox.input;
nodeRefs.restaurant = restaurantCheckbox.input;
nodeRefs.environment = environmentField.input;
nodeRefs.image = imageField.input;
nodeRefs.imageError = imageField.errorEl;
nodeRefs.website = websiteField.input;
nodeRefs.websiteError = websiteField.errorEl;
nodeRefs.tags = tagsField.input;
return nodeRefs;
}
function parseOptionalNumber(raw) {
const trimmed = (raw || '').trim();
if (trimmed === '') return null;
const num = Number(trimmed);
return Number.isFinite(num) ? num : NaN;
}
function readValues() {
return {
name: refs.name.value.trim(),
category: refs.category.value,
shortDescription: refs.shortDescription.value.trim(),
description: refs.description.value.trim(),
address: refs.address.value.trim(),
city: refs.city.value.trim(),
latitude: parseOptionalNumber(refs.latitude.value),
longitude: parseOptionalNumber(refs.longitude.value),
ageMin: parseOptionalNumber(refs.ageMin.value),
ageMax: parseOptionalNumber(refs.ageMax.value),
costType: refs.costType.value,
costDescription: refs.costDescription.value.trim(),
openingHours: refs.openingHours.value.trim(),
duration: parseOptionalNumber(refs.duration.value),
stroller: refs.stroller.checked,
wheelchair: refs.wheelchair.checked,
toilets: refs.toilets.checked,
parking: refs.parking.checked,
restaurant: refs.restaurant.checked,
environment: refs.environment.value,
image: refs.image.value.trim(),
website: refs.website.value.trim(),
tags: refs.tags.value,
};
}
/** Validiert die gelesenen Formularwerte. Gibt ein Objekt {feldname: Fehlermeldung} zurück. */
function validate(values) {
const errors = {};
if (!values.name) {
errors.name = 'Bitte einen Namen angeben.';
}
if (!values.category) {
errors.category = 'Bitte eine Kategorie auswählen.';
}
if (values.latitude === null) {
errors.latitude = 'Bitte einen Breitengrad angeben.';
} else if (Number.isNaN(values.latitude) || values.latitude < -90 || values.latitude > 90) {
errors.latitude = 'Breitengrad muss eine Zahl zwischen -90 und 90 sein.';
}
if (values.longitude === null) {
errors.longitude = 'Bitte einen Längengrad angeben.';
} else if (Number.isNaN(values.longitude) || values.longitude < -180 || values.longitude > 180) {
errors.longitude = 'Längengrad muss eine Zahl zwischen -180 und 180 sein.';
}
if (!errors.latitude && !errors.longitude && !isValidCoordinate(values.latitude, values.longitude)) {
errors.latitude = 'Koordinaten sind ungültig.';
}
if (values.ageMin !== null && (Number.isNaN(values.ageMin) || values.ageMin < 0)) {
errors.ageMin = 'Bitte ein gültiges Alter (mindestens 0) angeben.';
}
if (values.ageMax !== null && (Number.isNaN(values.ageMax) || values.ageMax < 0)) {
errors.ageMax = 'Bitte ein gültiges Alter (mindestens 0) angeben.';
}
if (
!errors.ageMin &&
!errors.ageMax &&
values.ageMin !== null &&
values.ageMax !== null &&
values.ageMin > values.ageMax
) {
errors.ageMax = 'Der Wert "bis" darf nicht kleiner als "ab" sein.';
}
if (values.duration !== null && (Number.isNaN(values.duration) || values.duration < 0)) {
errors.duration = 'Bitte eine gültige Aufenthaltsdauer in Minuten (mindestens 0) angeben.';
}
if (values.image && !isSafeHttpUrl(values.image)) {
errors.image = 'Bitte eine gültige Bild-URL (http/https) angeben.';
}
if (values.website && !isSafeHttpUrl(values.website)) {
errors.website = 'Bitte eine gültige Website-URL (http/https) angeben.';
}
return errors;
}
function clearErrors() {
for (const key of ERROR_FIELDS) {
const input = refs[key];
const errorEl = refs[`${key}Error`];
if (!input || !errorEl) continue;
input.removeAttribute('aria-invalid');
errorEl.textContent = '';
errorEl.hidden = true;
}
}
function showErrors(errors) {
clearErrors();
for (const [key, message] of Object.entries(errors)) {
const input = refs[key];
const errorEl = refs[`${key}Error`];
if (!input || !errorEl) continue;
input.setAttribute('aria-invalid', 'true');
errorEl.textContent = message;
errorEl.hidden = false;
}
}
function focusFirstError(errors) {
const firstKey = ERROR_FIELDS.find((key) => errors[key]);
if (firstKey && refs[firstKey]) refs[firstKey].focus();
}
/** Baut aus Formularwerten + optionaler Basis (bestehendes Place-Objekt) ein Rohobjekt für normalizePlace(). */
function buildRawPlace(values, base) {
return {
...base,
name: values.name,
category: values.category,
shortDescription: values.shortDescription,
description: values.description,
address: values.address,
city: values.city,
latitude: values.latitude,
longitude: values.longitude,
ageRecommendation: { min: values.ageMin, max: values.ageMax },
cost: { type: values.costType, description: values.costDescription },
openingHours: values.openingHours,
durationMinutes: values.duration,
facilities: {
strollerAccessible: values.stroller,
wheelchairAccessible: values.wheelchair,
toilets: values.toilets,
parking: values.parking,
restaurant: values.restaurant,
},
environment: values.environment,
image: values.image,
website: values.website,
tags: values.tags
.split(',')
.map((tag) => tag.trim())
.filter(Boolean),
// Explizit zurücksetzen: normalizePlace() setzt bei Bearbeiten dadurch
// einen neuen Zeitstempel, statt den aus `base` übernommenen zu behalten.
updatedAt: undefined,
};
}
function savePlace(normalized) {
const state = getState();
const places = editingPlace
? state.places.map((p) => (p.id === normalized.id ? normalized : p))
: [...state.places, normalized];
setState({ places });
persistPlaces();
}
function handleSubmit(event) {
event.preventDefault();
const values = readValues();
const errors = validate(values);
showErrors(errors);
if (Object.keys(errors).length > 0) {
focusFirstError(errors);
return;
}
const base = editingPlace ? { ...editingPlace } : {};
const normalized = normalizePlace(buildRawPlace(values, base));
savePlace(normalized);
closeForm();
}
function prefillForm(place) {
refs.name.value = place.name || '';
refs.category.value = place.category || '';
refs.shortDescription.value = place.shortDescription || '';
refs.description.value = place.description || '';
refs.address.value = place.address || '';
refs.city.value = place.city || '';
refs.latitude.value = place.latitude ?? '';
refs.longitude.value = place.longitude ?? '';
const age = place.ageRecommendation || {};
refs.ageMin.value = age.min ?? '';
refs.ageMax.value = age.max ?? '';
const cost = place.cost || {};
refs.costType.value = cost.type || 'unknown';
refs.costDescription.value = cost.description || '';
refs.openingHours.value = place.openingHours || '';
refs.duration.value = place.durationMinutes ?? '';
const facilities = place.facilities || {};
refs.stroller.checked = Boolean(facilities.strollerAccessible);
refs.wheelchair.checked = Boolean(facilities.wheelchairAccessible);
refs.toilets.checked = Boolean(facilities.toilets);
refs.parking.checked = Boolean(facilities.parking);
refs.restaurant.checked = Boolean(facilities.restaurant);
refs.environment.value = place.environment || 'outdoor';
refs.image.value = place.image || '';
refs.website.value = place.website || '';
refs.tags.value = Array.isArray(place.tags) ? place.tags.join(', ') : '';
}
/** Öffnet das Formular. Ohne Argument (bzw. mit null) im "Neu anlegen"-Modus, sonst im Bearbeiten-Modus. */
function openPlaceForm(place) {
if (!container || !refs) {
console.error('EventMap: Formular wurde noch nicht initialisiert (initForm()).');
return;
}
editingPlace = place || null;
clearErrors();
refs.form.reset();
if (editingPlace) {
refs.title.textContent = 'Ausflugsziel bearbeiten';
refs.submitBtn.textContent = 'Änderungen speichern';
prefillForm(editingPlace);
} else {
refs.title.textContent = 'Neues Ausflugsziel hinzufügen';
refs.submitBtn.textContent = 'Ausflugsziel speichern';
refs.category.value = '';
refs.costType.value = 'unknown';
refs.environment.value = 'outdoor';
}
container.hidden = false;
document.body.classList.add('place-form-modal-open');
refs.name.focus();
}
function closeForm() {
if (!container || container.hidden) return;
container.hidden = true;
document.body.classList.remove('place-form-modal-open');
editingPlace = null;
}
/** Initialisiert das Formular-Modal im Element mit der übergebenen id. */
function initForm(containerId) {
const containerEl = document.getElementById(containerId);
if (!containerEl) {
console.error(`EventMap: Formular-Container #${containerId} nicht gefunden.`);
return;
}
container = containerEl;
refs = buildSkeleton(container);
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !container.hidden) {
closeForm();
}
});
}
EventMap.ui.initForm = initForm;
EventMap.ui.openPlaceForm = openPlaceForm;
})();