- Beispieldaten werden nicht mehr eingebettet (data/seed.js entfernt), sondern beim ersten Start per fetch(EventMap.config.SEED_DATA_URL) von einem externen, selbst gehosteten Server geladen (siehe ADR 0001, Revision 2026-07-28). - Neuer UI-Sprachumschalter (Deutsch/Englisch) für die Bedienoberfläche: js/i18n/strings.js + js/i18n/i18n.js, Sprachwahl im Menü, Persistenz über settings.language, Retranslation ohne Reload. - Neuer Menüpunkt "Ausflugsziel übersetzen (KI)": erzeugt einen Prompt, der eine externe KI bittet, Name/Beschreibungen eines bestehenden Ortes ins Englische, Spanische oder Französische zu übersetzen (js/ai/translatePromptBuilder.js, js/ui/translatePromptGenerator.js). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
3.9 KiB
JavaScript
118 lines
3.9 KiB
JavaScript
// Kleine DOM-Helfer. Wichtig: Daten aus Places werden NIE per innerHTML
|
|
// eingefügt, sondern immer über textContent bzw. DOM-APIs (el()).
|
|
//
|
|
// Klassisches Script (kein ES-Modul): Die App wird per file:// direkt aus
|
|
// einem synchronisierten Ordner geöffnet, daher können keine ES-Module
|
|
// (import/export) verwendet werden (Browser blockieren deren Nachladen
|
|
// unter file:// per CORS-Policy). Stattdessen hängen alle Dateien ihre
|
|
// öffentliche API an das globale Namespace-Objekt window.EventMap.
|
|
// Diese Datei wird als erste geladen und legt den Namespace an.
|
|
|
|
window.EventMap = window.EventMap || {};
|
|
window.EventMap.utils = window.EventMap.utils || {};
|
|
window.EventMap.config = window.EventMap.config || {};
|
|
window.EventMap.models = window.EventMap.models || {};
|
|
window.EventMap.store = window.EventMap.store || {};
|
|
window.EventMap.map = window.EventMap.map || {};
|
|
window.EventMap.ui = window.EventMap.ui || {};
|
|
window.EventMap.filters = window.EventMap.filters || {};
|
|
window.EventMap.ai = window.EventMap.ai || {};
|
|
window.EventMap.import = window.EventMap.import || {};
|
|
window.EventMap.i18n = window.EventMap.i18n || {};
|
|
|
|
(function () {
|
|
/**
|
|
* Erzeugt ein DOM-Element.
|
|
* @param {string} tag Tag-Name, z.B. 'div'
|
|
* @param {object} [props] Eigenschaften: className, attrs{}, dataset{}, style{},
|
|
* text (setzt textContent), on{ click: fn, ... } für Event-Listener
|
|
* @param {Array<Node|string>} [children] Kindknoten oder Strings (werden als Text angehängt)
|
|
*/
|
|
function el(tag, props = {}, children = []) {
|
|
const node = document.createElement(tag);
|
|
|
|
if (props.className) {
|
|
node.className = props.className;
|
|
}
|
|
if (props.text !== undefined) {
|
|
node.textContent = props.text;
|
|
}
|
|
if (props.attrs) {
|
|
for (const [key, value] of Object.entries(props.attrs)) {
|
|
if (value !== undefined && value !== null) {
|
|
node.setAttribute(key, value);
|
|
}
|
|
}
|
|
}
|
|
if (props.dataset) {
|
|
for (const [key, value] of Object.entries(props.dataset)) {
|
|
node.dataset[key] = value;
|
|
}
|
|
}
|
|
if (props.style) {
|
|
Object.assign(node.style, props.style);
|
|
}
|
|
if (props.on) {
|
|
for (const [event, handler] of Object.entries(props.on)) {
|
|
node.addEventListener(event, handler);
|
|
}
|
|
}
|
|
|
|
for (const child of children) {
|
|
if (child === null || child === undefined) continue;
|
|
if (typeof child === 'string') {
|
|
node.appendChild(document.createTextNode(child));
|
|
} else {
|
|
node.appendChild(child);
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Escaped HTML-Sonderzeichen. Nur als Sicherheitsnetz gedacht, falls an
|
|
* einzelnen Stellen doch einmal mit HTML-Strings gearbeitet werden muss
|
|
* (z.B. Leaflet-Popups, die intern innerHTML nutzen). Für normales
|
|
* Rendering bitte el()/textContent verwenden.
|
|
*/
|
|
function escapeHtml(value) {
|
|
const str = String(value ?? '');
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
/** Entfernt alle Kindknoten eines Elements. */
|
|
function clearChildren(node) {
|
|
while (node.firstChild) {
|
|
node.removeChild(node.firstChild);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prüft, ob eine URL sicher als Link/Bildquelle verwendet werden darf
|
|
* (Scheme-Whitelist: nur http/https). Schützt vor javascript:-URLs & Co.
|
|
* in Nutzer-/KI-generierten Feldern wie place.website oder place.image.
|
|
*/
|
|
function isSafeHttpUrl(value) {
|
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
return false;
|
|
}
|
|
try {
|
|
const parsed = new URL(value);
|
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
EventMap.utils.el = el;
|
|
EventMap.utils.escapeHtml = escapeHtml;
|
|
EventMap.utils.clearChildren = clearChildren;
|
|
EventMap.utils.isSafeHttpUrl = isSafeHttpUrl;
|
|
})();
|