diff --git a/contao/dca/tl_module.php b/contao/dca/tl_module.php index ff403f1..f77c951 100644 --- a/contao/dca/tl_module.php +++ b/contao/dca/tl_module.php @@ -13,6 +13,39 @@ $GLOBALS['TL_DCA']['tl_module']['palettes']['eventmanager_map'] = '{title_legend $GLOBALS['TL_DCA']['tl_module']['palettes']['organization_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,logoFolder,organizationTypeTags;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; $GLOBALS['TL_DCA']['tl_module']['palettes']['event_edit'] = '{title_legend},name,headline,type;{eventmanager_legend},listPage,eventFolder,termsPage,frontendAuthorId,frontendArchiveId,eventTypeTags;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; +$getUsedOrganizationTagOptions = static function (): array { + $database = Database::getInstance(); + $labelExpression = $database->fieldExists('title', 'tl_tags') + ? "COALESCE(NULLIF(t.title, ''), t.tag)" + : 't.tag'; + + $rows = $database + ->prepare(sprintf( + 'SELECT DISTINCT t.id, %1$s AS label + FROM tl_tags_rel r + INNER JOIN tl_tags t ON t.id=r.tag_id + WHERE r.ptable=? AND (r.field=? OR r.field IS NULL OR r.field = \'\') + ORDER BY label ASC', + $labelExpression, + )) + ->execute('tl_organization', 'tags') + ->fetchAllAssoc(); + + $options = []; + + foreach ($rows as $row) { + $label = trim((string) ($row['label'] ?? '')); + + if ('' === $label) { + continue; + } + + $options[(int) $row['id']] = $label; + } + + return $options; +}; + $GLOBALS['TL_DCA']['tl_module']['fields']['editPage'] = [ 'label' => &$GLOBALS['TL_LANG']['tl_module']['editPage'], 'exclude' => true, @@ -73,38 +106,7 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['organizationTypeTags'] = [ 'label' => &$GLOBALS['TL_LANG']['tl_module']['organizationTypeTags'], 'exclude' => true, 'inputType' => 'checkbox', - 'options_callback' => static function () { - $database = Database::getInstance(); - $labelExpression = $database->fieldExists('title', 'tl_tags') - ? "COALESCE(NULLIF(t.title, ''), t.tag)" - : 't.tag'; - - $rows = Database::getInstance() - ->prepare(sprintf( - 'SELECT DISTINCT t.id, %1$s AS label - FROM tl_tags_rel r - INNER JOIN tl_tags t ON t.id=r.tag_id - WHERE r.ptable=? AND r.field=? - ORDER BY label ASC', - $labelExpression, - )) - ->execute('tl_organization', 'tags') - ->fetchAllAssoc(); - - $options = []; - - foreach ($rows as $row) { - $label = trim((string) ($row['label'] ?? '')); - - if ('' === $label) { - continue; - } - - $options[(int) $row['id']] = $label; - } - - return $options; - }, + 'options_callback' => $getUsedOrganizationTagOptions, 'eval' => ['multiple' => true, 'tl_class' => 'clr'], 'sql' => ['type' => 'blob', 'notnull' => false], 'save_callback' => [ @@ -229,38 +231,7 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['mapInitialOrganizationTagId'] = [ 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapInitialOrganizationTagId'], 'exclude' => true, 'inputType' => 'select', - 'options_callback' => static function (): array { - $database = Database::getInstance(); - $labelExpression = $database->fieldExists('title', 'tl_tags') - ? "COALESCE(NULLIF(t.title, ''), t.tag)" - : 't.tag'; - - $rows = $database - ->prepare(sprintf( - 'SELECT DISTINCT t.id, %1$s AS label - FROM tl_tags_rel r - INNER JOIN tl_tags t ON t.id=r.tag_id - WHERE r.ptable=? AND r.field=? - ORDER BY label ASC', - $labelExpression, - )) - ->execute('tl_organization', 'tags') - ->fetchAllAssoc(); - - $options = []; - - foreach ($rows as $row) { - $label = trim((string) ($row['label'] ?? '')); - - if ('' === $label) { - continue; - } - - $options[(int) $row['id']] = $label; - } - - return $options; - }, + 'options_callback' => $getUsedOrganizationTagOptions, 'eval' => ['includeBlankOption' => true, 'chosen' => true, 'tl_class' => 'w50'], 'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0], ]; diff --git a/public/assets/map-module.js b/public/assets/map-module.js index b515581..c81a3cf 100644 --- a/public/assets/map-module.js +++ b/public/assets/map-module.js @@ -10,7 +10,6 @@ const DEFAULT_ZOOM = 6; const DEFAULT_EVENT_COLOR = '#BC5067'; const EVENT_FADE_DURATION_MS = 200; const EVENT_CLUSTER_LAYER_ID = 'eventmanager-events-clusters'; -const EVENT_CLUSTER_SPIDERFY_LAYER_ID = 'eventmanager-events-clusters-spiderfy'; const EVENT_UNCLUSTERED_LAYER_ID = 'eventmanager-events-unclustered'; const SATELLITE_SOURCE_ID = 'eventmanager-satellite-source'; const SATELLITE_LAYER_ID = 'eventmanager-satellite-layer'; @@ -162,7 +161,7 @@ const parseItems = (container) => { const unique = new Map(); parsed.forEach((item) => { - if (!item || !['event', 'location', 'organisation'].includes(item.type)) { + if (!item || !['event', 'organisation'].includes(item.type)) { return; } @@ -297,9 +296,7 @@ const ensureSatelliteLayer = (map) => { const markerLayerCandidates = [ EVENT_CLUSTER_LAYER_ID, - EVENT_CLUSTER_SPIDERFY_LAYER_ID, EVENT_UNCLUSTERED_LAYER_ID, - 'eventmanager-non-events', ]; const beforeId = markerLayerCandidates.find((layerId) => map.getLayer(layerId)); @@ -526,7 +523,7 @@ const toFeature = (item) => ({ }, }); -const initOrganizationMarkers = (map, organizationItems, organizationColor) => { +const initOrganizationMarkers = (map, organizationItems, organizationColor, openPopup) => { if (!organizationItems.length) { return { setOnlyTagId: () => {}, @@ -542,16 +539,21 @@ const initOrganizationMarkers = (map, organizationItems, organizationColor) => { const marker = new maplibregl.Marker(organizationColor ? { color: organizationColor } : undefined) .setLngLat([item.longitude, item.latitude]); - if (popupHtml) { - marker.setPopup(new maplibregl.Popup({ offset: 15 }).setHTML(String(popupHtml))); - } - marker.addTo(map); const markerElement = marker.getElement(); if (markerElement) { markerElement.style.zIndex = String(Math.round((90 - item.latitude) * 1000)); + markerElement.style.cursor = 'pointer'; + + if (popupHtml && typeof openPopup === 'function') { + markerElement.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + openPopup([item.longitude, item.latitude], String(popupHtml)); + }); + } } return { @@ -605,7 +607,7 @@ const initOrganizationMarkers = (map, organizationItems, organizationColor) => { }; }; -const bindExternalTagFilters = (container, map, organizationMarkerManager, eventLayerManager) => { +const bindExternalTagFilters = (container, map, organizationMarkerManager, eventLayerManager, closePopup) => { const wrapperId = container.dataset.mapFilterWrapperId || ''; if (!wrapperId || !organizationMarkerManager) { @@ -720,6 +722,9 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event setEventButtonState(false); updateExclusiveButtonAccessibility(); + if (typeof closePopup === 'function') { + closePopup(); + } applyFilter(); }); }); @@ -761,6 +766,9 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event tagButtons.forEach((button) => setButtonState(button, false)); setEventButtonState(eventsOnly); updateExclusiveButtonAccessibility(); + if (typeof closePopup === 'function') { + closePopup(); + } applyFilter(); }); } else if (eventToggleButton) { @@ -847,119 +855,7 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event applyMapStyleMode(); }; -const initLocationLayers = (map, locationItems, markerImageId) => { - if (!locationItems.length) { - return { - setVisible: () => {}, - }; - } - - const sourceId = 'eventmanager-non-events-source'; - const layerId = 'eventmanager-non-events'; - map.addSource(sourceId, { - type: 'geojson', - data: { - type: 'FeatureCollection', - features: locationItems.map(toFeature), - }, - }); - - map.addLayer({ - id: layerId, - type: 'symbol', - source: sourceId, - layout: { - 'icon-image': markerImageId, - 'icon-size': 1, - 'icon-anchor': 'bottom', - 'icon-allow-overlap': true, - 'icon-ignore-placement': true, - 'symbol-sort-key': ['*', -1, ['to-number', ['get', 'latitude'], 0]], - }, - paint: { - 'icon-opacity': 1, - 'icon-translate': [0, 4], - }, - }); - - map.on('click', layerId, (event) => { - const blockingLayers = [ - EVENT_CLUSTER_LAYER_ID, - EVENT_CLUSTER_SPIDERFY_LAYER_ID, - EVENT_UNCLUSTERED_LAYER_ID, - ].filter((id) => map.getLayer(id)); - - if (blockingLayers.length) { - const topEventFeatures = map.queryRenderedFeatures(event.point, { layers: blockingLayers }); - - if (topEventFeatures.length) { - return; - } - } - - const feature = event.features && event.features[0] ? event.features[0] : null; - - if (!feature) { - return; - } - - const popupHtml = feature?.properties?.popupHtml || ''; - const coordinates = event.lngLat ? [event.lngLat.lng, event.lngLat.lat] : null; - - if (!coordinates) { - return; - } - - new maplibregl.Popup({ offset: 15 }) - .setLngLat(coordinates) - .setHTML(String(popupHtml)) - .addTo(map); - }); - - map.on('mouseenter', layerId, () => { - map.getCanvas().style.cursor = 'pointer'; - }); - - map.on('mouseleave', layerId, () => { - map.getCanvas().style.cursor = ''; - }); - - const setLayerLayoutVisibility = (isVisible) => { - if (!map.getLayer(layerId)) { - return; - } - - map.setLayoutProperty(layerId, 'visibility', isVisible ? 'visible' : 'none'); - }; - - const fadeLayerOpacity = (value) => { - if (!map.getLayer(layerId)) { - return; - } - - map.setPaintProperty(layerId, 'icon-opacity-transition', { duration: EVENT_FADE_DURATION_MS, delay: 0 }); - map.setPaintProperty(layerId, 'icon-opacity', value); - }; - - return { - setVisible: (isVisible) => { - if (isVisible) { - setLayerLayoutVisibility(true); - fadeLayerOpacity(1); - - return; - } - - fadeLayerOpacity(0); - - window.setTimeout(() => { - setLayerLayoutVisibility(false); - }, EVENT_FADE_DURATION_MS + 10); - }, - }; -}; - -const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMarkerImageId) => { +const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMarkerImageId, openPopup, closePopup) => { const sourceId = 'eventmanager-events-source'; const clusterLayerId = EVENT_CLUSTER_LAYER_ID; const unclusteredLayerId = EVENT_UNCLUSTERED_LAYER_ID; @@ -1036,10 +932,9 @@ const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMark return; } - new maplibregl.Popup({ offset: 15 }) - .setLngLat([lng, lat]) - .setHTML(String(popupHtml)) - .addTo(map); + if (typeof openPopup === 'function') { + openPopup([lng, lat], String(popupHtml)); + } }; map.on('click', unclusteredLayerId, (event) => { @@ -1124,7 +1019,7 @@ const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMark }, spiderLeavesPaint: { 'icon-opacity': 1, - 'icon-translate': [0, 9], + 'icon-translate': [-3, 9], }, onLeafClick: (feature, spiderEvent) => { const clickCoordinates = spiderEvent?.lngLat @@ -1208,6 +1103,10 @@ const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMark } } + if (typeof closePopup === 'function') { + closePopup(); + } + fadeLayerOpacity(clusterLayerId, 'icon-opacity', 0); fadeLayerOpacity(clusterLayerId, 'text-opacity', 0); fadeLayerOpacity(unclusteredLayerId, 'icon-opacity', 0); @@ -1285,6 +1184,44 @@ const initSingleMap = (container) => { }, }); + let activePopup = null; + + const closePopup = () => { + if (!activePopup) { + return; + } + + activePopup.remove(); + activePopup = null; + }; + + const openPopup = (coordinates, popupHtml) => { + if (!Array.isArray(coordinates) || coordinates.length !== 2 || !popupHtml) { + return; + } + + const [lng, lat] = coordinates.map((value) => Number(value)); + + if (!Number.isFinite(lng) || !Number.isFinite(lat)) { + return; + } + + closePopup(); + + const popup = new maplibregl.Popup({ offset: 15 }) + .setLngLat([lng, lat]) + .setHTML(String(popupHtml)) + .addTo(map); + + popup.on('close', () => { + if (activePopup === popup) { + activePopup = null; + } + }); + + activePopup = popup; + }; + map.on('style.load', () => { applyDefaultProjection(map); }); @@ -1334,9 +1271,6 @@ const initSingleMap = (container) => { const organizationItems = items .filter((item) => item.type === 'organisation') .sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); - const locationItems = items - .filter((item) => item.type === 'location') - .sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); const eventItems = items .filter((item) => item.type === 'event') .sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude); @@ -1357,25 +1291,14 @@ const initSingleMap = (container) => { return; } - const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationColor); - const locationLayerManager = initLocationLayers(map, locationItems, eventMarkerImageId); + const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationColor, openPopup); let eventLayerManager = null; if (eventItems.length) { - eventLayerManager = initEventLayers(map, eventItems, eventColor, eventMarkerImageId, clusterMarkerImageId); + eventLayerManager = initEventLayers(map, eventItems, eventColor, eventMarkerImageId, clusterMarkerImageId, openPopup, closePopup); } - const combinedEventLayerManager = { - setVisible: (isVisible) => { - locationLayerManager.setVisible(isVisible); - - if (eventLayerManager) { - eventLayerManager.setVisible(isVisible); - } - }, - }; - - bindExternalTagFilters(container, map, organizationMarkerManager, combinedEventLayerManager); + bindExternalTagFilters(container, map, organizationMarkerManager, eventLayerManager, closePopup); if ('markers' === centerMode) { fitBounds(allCoordinates); diff --git a/src/Service/MapModuleDataProvider.php b/src/Service/MapModuleDataProvider.php index 6d58e11..68d8ebf 100644 --- a/src/Service/MapModuleDataProvider.php +++ b/src/Service/MapModuleDataProvider.php @@ -347,24 +347,6 @@ class MapModuleDataProvider return $qb->executeQuery()->fetchAllAssociative(); } - /** - * @return list> - */ - private function fetchLocationRows(string $locationTable, array $locationGeoColumns): array - { - $qb = $this->connection->createQueryBuilder(); - $qb - ->select('l.id', 'l.title') - ->addSelect(sprintf('l.%s AS latitude', $locationGeoColumns['latitude'])) - ->addSelect(sprintf('l.%s AS longitude', $locationGeoColumns['longitude'])) - ->from($locationTable, 'l') - ->orderBy('l.id', 'ASC'); - - $this->applyPublicationConstraints($qb, 'l', $locationTable); - - return $qb->executeQuery()->fetchAllAssociative(); - } - /** * @return list> */