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.
145 lines
4.8 KiB
JavaScript
145 lines
4.8 KiB
JavaScript
/* 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;
|
||
})();
|