// Modal zur manuellen Eingabe des Nutzerstandorts (Bugfix, Gitea Issue #1 // "Eigenen Standort verwenden"): Bislang gab es keine Möglichkeit, // state.userLocation zu setzen, wenn die Browser-Geolocation fehlschlägt // oder gar nicht verfügbar ist (fehlende Berechtigung, kein HTTPS, Desktop // ohne GPS, ...). // // Aufbau/Muster analog zu js/ui/form.js (Modal mit Backdrop, Escape schließt, // Fokus auf erstes Feld beim Öffnen, Validierung + Inline-Fehlermeldungen // über dieselben .form-field-Klassen aus css/form.css). Rendering // ausschließlich über textContent/DOM-APIs (el()), niemals innerHTML. // // API: EventMap.ui.initManualLocationForm(containerId) initialisiert das // Modal einmalig. EventMap.ui.openManualLocationForm() öffnet es, vorbefüllt // mit dem aktuellen state.userLocation (falls vorhanden). (function () { const { getState, setState, updateSettings } = EventMap.store; const { el, isValidCoordinate } = EventMap.utils; const { centerOnCoords } = EventMap.map; const ERROR_FIELDS = ['latitude', 'longitude']; let container = null; let refs = 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 buildSkeleton(rootContainer) { const nodeRefs = {}; nodeRefs.backdrop = el('div', { className: 'manual-location-modal__backdrop', on: { click: () => closeManualLocationForm() }, }); nodeRefs.closeBtn = el( 'button', { className: 'manual-location-modal__close', attrs: { type: 'button', 'aria-label': 'Formular schließen' }, on: { click: () => closeManualLocationForm() }, }, ['✕'] ); nodeRefs.title = el('h2', { className: 'manual-location-form__title', attrs: { id: 'manual-location-title' }, text: 'Standort manuell eingeben', }); nodeRefs.intro = el('p', { className: 'manual-location-form__intro', text: 'Falls dein Browser den Standort nicht ermitteln kann oder du die Freigabe nicht ' + 'erteilen möchtest, gib hier Breiten- und Längengrad direkt ein.', }); const latitudeField = fieldWrapper( 'ml-latitude', 'Breitengrad', el('input', { attrs: { type: 'number', step: 'any', min: '-90', max: '90', placeholder: 'z.B. 48.1374' } }), { required: true } ); const longitudeField = fieldWrapper( 'ml-longitude', 'Längengrad', el('input', { attrs: { type: 'number', step: 'any', min: '-180', max: '180', placeholder: 'z.B. 11.5755' } }), { required: true } ); nodeRefs.cancelBtn = el( 'button', { className: 'btn btn--secondary', attrs: { type: 'button' }, on: { click: () => closeManualLocationForm() } }, ['Abbrechen'] ); nodeRefs.submitBtn = el( 'button', { className: 'btn btn--primary', attrs: { type: 'submit' } }, ['Übernehmen'] ); nodeRefs.form = el( 'form', { className: 'manual-location-form', attrs: { id: 'manual-location-form', novalidate: '' }, on: { submit: handleSubmit }, }, [ latitudeField.wrapper, longitudeField.wrapper, el('div', { className: 'manual-location-form__actions' }, [nodeRefs.cancelBtn, nodeRefs.submitBtn]), ] ); nodeRefs.panel = el( 'div', { className: 'manual-location-modal__panel', attrs: { role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'manual-location-title' }, }, [nodeRefs.closeBtn, nodeRefs.title, nodeRefs.intro, nodeRefs.form] ); rootContainer.appendChild(nodeRefs.backdrop); rootContainer.appendChild(nodeRefs.panel); nodeRefs.latitude = latitudeField.input; nodeRefs.latitudeError = latitudeField.errorEl; nodeRefs.longitude = longitudeField.input; nodeRefs.longitudeError = longitudeField.errorEl; return nodeRefs; } function parseNumber(raw) { const trimmed = (raw || '').trim(); if (trimmed === '') return null; const num = Number(trimmed); return Number.isFinite(num) ? num : NaN; } /** Validiert die gelesenen Koordinaten. Gibt ein Objekt {feldname: Fehlermeldung} zurück. */ function validate(lat, lng) { const errors = {}; if (lat === null) { errors.latitude = 'Bitte einen Breitengrad angeben.'; } else if (Number.isNaN(lat) || lat < -90 || lat > 90) { errors.latitude = 'Breitengrad muss eine Zahl zwischen -90 und 90 sein.'; } if (lng === null) { errors.longitude = 'Bitte einen Längengrad angeben.'; } else if (Number.isNaN(lng) || lng < -180 || lng > 180) { errors.longitude = 'Längengrad muss eine Zahl zwischen -180 und 180 sein.'; } if (!errors.latitude && !errors.longitude && !isValidCoordinate(lat, lng)) { errors.latitude = 'Koordinaten sind ungültig.'; } 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(); } function handleSubmit(event) { event.preventDefault(); const lat = parseNumber(refs.latitude.value); const lng = parseNumber(refs.longitude.value); const errors = validate(lat, lng); showErrors(errors); if (Object.keys(errors).length > 0) { focusFirstError(errors); return; } setState({ userLocation: { lat, lng } }); updateSettings({ userConsentGeo: true }); centerOnCoords(lat, lng, 13); closeManualLocationForm(); } /** Öffnet das Formular, vorbefüllt mit dem aktuellen Nutzerstandort (falls vorhanden). */ function openManualLocationForm() { if (!container || !refs) { console.error( 'EventMap: Formular für manuellen Standort wurde noch nicht initialisiert (initManualLocationForm()).' ); return; } clearErrors(); refs.form.reset(); const { userLocation } = getState(); if (userLocation) { refs.latitude.value = userLocation.lat; refs.longitude.value = userLocation.lng; } container.hidden = false; document.body.classList.add('manual-location-modal-open'); refs.latitude.focus(); } function closeManualLocationForm() { if (!container || container.hidden) return; container.hidden = true; document.body.classList.remove('manual-location-modal-open'); } /** Initialisiert das Modal im Element mit der übergebenen id. */ function initManualLocationForm(containerId) { const containerEl = document.getElementById(containerId); if (!containerEl) { console.error(`EventMap: Container #${containerId} für manuellen Standort nicht gefunden.`); return; } container = containerEl; refs = buildSkeleton(container); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !container.hidden) { closeManualLocationForm(); } }); } EventMap.ui.initManualLocationForm = initManualLocationForm; EventMap.ui.openManualLocationForm = openManualLocationForm; })();