Files
dapelza 1b26cfda7a Feature: Hover-Vorschau auf Kartenmarkern
Marker zeigen jetzt beim Mouseover zusaetzlich zum bestehenden
Klick-Popup eine kompakte Vorschau (Leaflet-Tooltip) mit Name,
Kategorie, Kurzbeschreibung sowie Kurzfakten (Altersempfehlung,
Kostenart, Bewertung, falls vorhanden) - ohne dass geklickt werden muss.
Inhalt wird wie ueberall sonst als DOM-Node ueber el()/textContent
erzeugt, kein innerHTML.
2026-07-24 13:01:35 +02:00

145 lines
4.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global L */
// Erzeugt Leaflet-Marker für Places inkl. Kategorie-Icon (L.divIcon) und
// Popup-Inhalt. Popup-Inhalte werden als DOM-Nodes (nicht als HTML-String)
// erzeugt, damit Place-Daten niemals ungesichert als innerHTML landen.
(function () {
const { getCategoryById } = EventMap.config;
const { getPlaceCategory, getPlaceLatLng } = EventMap.models;
const { el } = EventMap.utils;
const iconCache = new Map();
function buildDivIcon(category) {
return L.divIcon({
className: 'eventmap-marker',
html: `<span class="eventmap-marker__glyph" style="background:${category.color}">${category.icon}</span>`,
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16],
});
}
function getIconForCategory(categoryId) {
if (!iconCache.has(categoryId)) {
iconCache.set(categoryId, buildDivIcon(getCategoryById(categoryId)));
}
return iconCache.get(categoryId);
}
function buildPopupContent(place) {
const category = getPlaceCategory(place);
return el('div', { className: 'eventmap-popup' }, [
el('strong', { className: 'eventmap-popup__title', text: place.name }),
el('div', {
className: 'eventmap-popup__category',
style: { color: category.color },
text: `${category.icon} ${category.label}`,
}),
el('p', { className: 'eventmap-popup__desc', text: place.shortDescription }),
]);
}
const COST_LABELS = { free: 'Kostenlos', paid: 'Kostenpflichtig', mixed: 'Teils kostenpflichtig', unknown: null };
function formatTooltipAge(ageRecommendation) {
const { min, max } = ageRecommendation || {};
if (min == null && max == null) return null;
if (min != null && max != null) return `${min}${max} Jahre`;
if (min != null) return `Ab ${min} Jahren`;
return `Bis ${max} Jahre`;
}
/** Kompakte Hover-Vorschau (Leaflet-Tooltip), zusätzlich zum Klick-Popup. */
function buildTooltipContent(place) {
const category = getPlaceCategory(place);
const facts = [];
const age = formatTooltipAge(place.ageRecommendation);
if (age) facts.push(age);
const costLabel = COST_LABELS[(place.cost || {}).type];
if (costLabel) facts.push(costLabel);
if (typeof place.rating === 'number') facts.push(`★ ${place.rating.toFixed(1)}`);
const children = [
el('strong', { className: 'eventmap-tooltip__title', text: place.name }),
el('div', {
className: 'eventmap-tooltip__category',
style: { color: category.color },
text: `${category.icon} ${category.label}`,
}),
];
if (place.shortDescription) {
children.push(el('p', { className: 'eventmap-tooltip__desc', text: place.shortDescription }));
}
if (facts.length > 0) {
children.push(el('p', { className: 'eventmap-tooltip__facts', text: facts.join(' · ') }));
}
return el('div', { className: 'eventmap-tooltip' }, children);
}
/**
* Erzeugt einen Leaflet-Marker für ein Place-Objekt.
* @param {object} place
* @param {{onSelect?: (placeId: string) => void}} [handlers]
*/
function createMarkerForPlace(place, { onSelect } = {}) {
const { lat, lng } = getPlaceLatLng(place);
const marker = L.marker([lat, lng], { icon: getIconForCategory(place.category) });
marker.bindPopup(buildPopupContent(place));
// Hover-Vorschau zusätzlich zum Klick-Popup: sticky folgt dem Mauszeiger,
// damit die Vorschau nicht die Position wechselt, während man über den
// Marker fährt. Schließt sich automatisch bei mouseout bzw. wenn der
// Klick-Popup geöffnet wird.
marker.bindTooltip(buildTooltipContent(place), {
direction: 'top',
offset: [0, -18],
opacity: 0.97,
sticky: false,
className: 'eventmap-tooltip-wrapper',
});
marker.on('click', () => {
if (onSelect) {
onSelect(place.id);
}
});
return marker;
}
let userLocationIcon = null;
function getUserLocationIcon() {
if (!userLocationIcon) {
userLocationIcon = L.divIcon({
className: 'eventmap-user-marker',
html: '<span class="eventmap-user-marker__pulse"></span><span class="eventmap-user-marker__dot"></span>',
iconSize: [22, 22],
iconAnchor: [11, 11],
});
}
return userLocationIcon;
}
/**
* Erzeugt einen (nicht klickbaren) Marker für den übermittelten/manuell
* eingegebenen Nutzerstandort (Gitea Issue #2 "Standort auf Karte
* anzeigen"). Bewusst kein Popup/Interaktion, da es kein Ausflugsziel ist.
*/
function createUserLocationMarker(lat, lng) {
return L.marker([lat, lng], {
icon: getUserLocationIcon(),
interactive: false,
keyboard: false,
zIndexOffset: 1000,
});
}
EventMap.map.createMarkerForPlace = createMarkerForPlace;
EventMap.map.createUserLocationMarker = createUserLocationMarker;
})();