diff --git a/css/form.css b/css/form.css new file mode 100644 index 0000000..4a46ff9 --- /dev/null +++ b/css/form.css @@ -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); + } +} diff --git a/css/layout.css b/css/layout.css index 844c451..9b65d7d 100644 --- a/css/layout.css +++ b/css/layout.css @@ -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 { diff --git a/index.html b/index.html index 9579a88..b9e6d27 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@ + @@ -30,6 +31,9 @@ +

Karte wird geladen…

@@ -45,6 +49,7 @@ + @@ -69,6 +74,7 @@ + diff --git a/js/main.js b/js/main.js index e78c082..aa9d34d 100644 --- a/js/main.js +++ b/js/main.js @@ -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) => { diff --git a/js/ui/detail.js b/js/ui/detail.js index df54983..a249958 100644 --- a/js/ui/detail.js +++ b/js/ui/detail.js @@ -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; diff --git a/js/ui/form.js b/js/ui/form.js new file mode 100644 index 0000000..2b96e42 --- /dev/null +++ b/js/ui/form.js @@ -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; +})();