From 621ce8dc8bf67d42399c1938d890e0258fa9513a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Mummert?= Date: Thu, 26 Feb 2026 18:26:20 +0100 Subject: [PATCH] fix(map): simplify filters and restore style toggle behavior --- contao/dca/tl_module.php | 10 +- contao/languages/de/tl_module.php | 2 +- contao/languages/en/tl_module.php | 2 +- contao/templates/frontend/event_map.html.twig | 46 ++-- public/assets/map-module.js | 211 +++++++++--------- .../Frontend/EventMapController.php | 8 +- 6 files changed, 141 insertions(+), 138 deletions(-) diff --git a/contao/dca/tl_module.php b/contao/dca/tl_module.php index 3aaa269..b5a3abf 100644 --- a/contao/dca/tl_module.php +++ b/contao/dca/tl_module.php @@ -9,7 +9,7 @@ use Contao\StringUtil; $GLOBALS['TL_DCA']['tl_module']['palettes']['member_organizations'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; $GLOBALS['TL_DCA']['tl_module']['palettes']['member_events'] = '{title_legend},name,headline,type;{eventmanager_legend},editPage;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; $GLOBALS['TL_DCA']['tl_module']['palettes']['event_filter'] = '{title_legend},name,headline,type;{eventmanager_legend},cal_calendar,eventListDomId;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; -$GLOBALS['TL_DCA']['tl_module']['palettes']['eventmanager_map'] = '{title_legend},name,headline,type;{eventmanager_legend},mapShowOrganizations,mapShowExternalOrganizations,mapShowEvents,mapEventColor,mapOrganizationColorScheme,mapCenterMode;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; +$GLOBALS['TL_DCA']['tl_module']['palettes']['eventmanager_map'] = '{title_legend},name,headline,type;{eventmanager_legend},mapShowOrganizations,mapShowExternalOrganizations,mapShowEvents,mapEventColor,mapOrganizationColor,mapCenterMode;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID'; $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'; @@ -195,12 +195,12 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['mapEventColor'] = [ 'sql' => ['type' => 'string', 'length' => 7, 'default' => '#BC5067'], ]; -$GLOBALS['TL_DCA']['tl_module']['fields']['mapOrganizationColorScheme'] = [ - 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'], +$GLOBALS['TL_DCA']['tl_module']['fields']['mapOrganizationColor'] = [ + 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColor'], 'exclude' => true, 'inputType' => 'text', - 'eval' => ['maxlength' => 1024, 'tl_class' => 'clr long'], - 'sql' => ['type' => 'string', 'length' => 1024, 'default' => ''], + 'eval' => ['maxlength' => 7, 'rgxp' => 'hexcolor', 'colorpicker' => true, 'tl_class' => 'w50'], + 'sql' => ['type' => 'string', 'length' => 7, 'default' => '#BC5067'], ]; $GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterMode'] = [ diff --git a/contao/languages/de/tl_module.php b/contao/languages/de/tl_module.php index 6d9dedd..17729e3 100644 --- a/contao/languages/de/tl_module.php +++ b/contao/languages/de/tl_module.php @@ -17,7 +17,7 @@ $GLOBALS['TL_LANG']['tl_module']['mapShowOrganizations'] = ['Organisationen anze $GLOBALS['TL_LANG']['tl_module']['mapShowExternalOrganizations'] = ['Externe Organisationen anzeigen', 'Wenn aktiviert, werden externe Organisationen (isExternal=1) zusätzlich auf der Karte dargestellt. Standard: nein.']; $GLOBALS['TL_LANG']['tl_module']['mapShowEvents'] = ['Veranstaltungen anzeigen', 'Wenn aktiviert, werden Event- (inkl. Orts-) Marker auf der Karte dargestellt.']; $GLOBALS['TL_LANG']['tl_module']['mapEventColor'] = ['Event-Farbe (Kreise/Linien)', 'Farbe für Event-Cluster, Event-Punkte und Spiderfy-Verbindungslinien (Hex, z. B. #BC5067).']; -$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'] = ['Farbschema (Organisationstypen)', 'Kommagetrennte Farben für Organisationstypen/Tags, z. B. ff6600,77dd33,ff99bb. Markerfarbe richtet sich nach dem ersten Tag.']; +$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColor'] = ['Organisationsfarbe', 'Einheitliche Farbe für alle Organisations-Marker (Hex, z. B. #BC5067).']; $GLOBALS['TL_LANG']['tl_module']['mapCenterMode'] = ['Karten-Zentrierung', 'Wählen Sie, ob die Karte anhand der Marker oder mit festen Koordinaten zentriert werden soll.']; $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [ 'markers' => 'Anhand der Marker (alle Marker sichtbar)', diff --git a/contao/languages/en/tl_module.php b/contao/languages/en/tl_module.php index 4a69d86..cfdb33e 100644 --- a/contao/languages/en/tl_module.php +++ b/contao/languages/en/tl_module.php @@ -17,7 +17,7 @@ $GLOBALS['TL_LANG']['tl_module']['mapShowOrganizations'] = ['Show organizations' $GLOBALS['TL_LANG']['tl_module']['mapShowExternalOrganizations'] = ['Show external organizations', 'If enabled, external organizations (isExternal=1) are additionally rendered on the map. Default: no.']; $GLOBALS['TL_LANG']['tl_module']['mapShowEvents'] = ['Show events', 'If enabled, event markers (including related locations) are rendered on the map.']; $GLOBALS['TL_LANG']['tl_module']['mapEventColor'] = ['Event color (circles/lines)', 'Color for event clusters, event points and spiderfy connector lines (hex, e.g. #BC5067).']; -$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'] = ['Color scheme (organization types)', 'Comma-separated colors for organization type tags, e.g. ff6600,77dd33,ff99bb. Marker color follows the first tag.']; +$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColor'] = ['Organization color', 'Unified color for all organization markers (hex, e.g. #BC5067).']; $GLOBALS['TL_LANG']['tl_module']['mapCenterMode'] = ['Map centering', 'Choose whether the map should center by markers or fixed coordinates.']; $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [ 'markers' => 'By markers (fit all visible markers)', diff --git a/contao/templates/frontend/event_map.html.twig b/contao/templates/frontend/event_map.html.twig index 5749aa7..e426462 100644 --- a/contao/templates/frontend/event_map.html.twig +++ b/contao/templates/frontend/event_map.html.twig @@ -9,12 +9,13 @@ >
+ + {% if tags is iterable and tags|length > 0 %} {% for tag in tags %} {% endfor %} {% endif %} -
-
- - - - + + +
@@ -57,7 +72,7 @@ data-map-style="{{ mapStyleUrl|e('html_attr') }}" data-map-data-id="{{ mapDataElementId|e('html_attr') }}" data-map-event-color="{{ mapEventColor|default('#BC5067')|e('html_attr') }}" - data-map-organization-colors="{{ mapOrganizationColorScheme|default('')|e('html_attr') }}" + data-map-organization-color="{{ mapOrganizationColor|default('#BC5067')|e('html_attr') }}" data-map-center-mode="{{ mapCenterMode|default('markers')|e('html_attr') }}" data-map-center-lat="{{ mapCenterLat|default('')|e('html_attr') }}" data-map-center-lng="{{ mapCenterLng|default('')|e('html_attr') }}" @@ -71,8 +86,7 @@ margin-bottom: .75rem; } - .eventmanager-map-filter__group, - .eventmanager-map-filter__actions { + .eventmanager-map-filter__group { display: flex; flex-wrap: wrap; gap: .5rem; @@ -89,4 +103,4 @@ - + diff --git a/public/assets/map-module.js b/public/assets/map-module.js index f51c1c2..bed0213 100644 --- a/public/assets/map-module.js +++ b/public/assets/map-module.js @@ -199,70 +199,6 @@ const normalizeHexColor = (value, fallback = DEFAULT_EVENT_COLOR) => { return fallback; }; -const normalizeHexColorOrNull = (value) => { - const fallback = '__INVALID__'; - const normalized = normalizeHexColor(value, fallback); - - return normalized === fallback ? null : normalized; -}; - -const parseOrganizationColorScheme = (value) => { - const raw = String(value || '').trim(); - - if ('' === raw) { - return []; - } - - return raw - .split(',') - .map((part) => normalizeHexColorOrNull(part)) - .filter((part) => null !== part); -}; - -const buildOrganizationTagColorMap = (container, colors, organizationItems) => { - if (!colors.length) { - return { - byTagId: {}, - fallbackColor: null, - }; - } - - const fallbackColor = colors[0]; - const wrapperId = container.dataset.mapFilterWrapperId || ''; - const wrapper = wrapperId ? document.getElementById(wrapperId) : null; - let orderedTagIds = []; - - if (wrapper) { - orderedTagIds = Array.from(wrapper.querySelectorAll('[data-map-tag-filter]')) - .map((button) => String(button.dataset.mapTagFilter || '').trim()) - .filter((value) => /^\d+$/.test(value)); - } - - if (!orderedTagIds.length) { - const seen = new Set(); - - organizationItems.forEach((item) => { - const firstTagId = String(item?.extra?.organizationTagIds?.[0] || '').trim(); - - if (/^\d+$/.test(firstTagId) && !seen.has(firstTagId)) { - seen.add(firstTagId); - orderedTagIds.push(firstTagId); - } - }); - } - - const byTagId = {}; - - orderedTagIds.forEach((tagId, index) => { - byTagId[tagId] = colors[index] || fallbackColor; - }); - - return { - byTagId, - fallbackColor, - }; -}; - const applyDefaultProjection = (map) => { if (!map || typeof map.setProjection !== 'function') { return; @@ -441,21 +377,18 @@ const toFeature = (item) => ({ }, }); -const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap) => { +const initOrganizationMarkers = (map, organizationItems, organizationColor) => { if (!organizationItems.length) { return { - setActiveTagIds: () => {}, + setOnlyTagId: () => {}, setAllVisible: () => {}, + setVisible: () => {}, }; } const markerEntries = organizationItems.map((item) => { const popupHtml = popupHtmlFor(item); - const primaryTagId = String(item?.extra?.organizationTagIds?.[0] || '').trim(); - const configuredColor = /^\d+$/.test(primaryTagId) - ? (organizationTagColorMap?.byTagId?.[primaryTagId] || organizationTagColorMap?.fallbackColor || null) - : null; - const marker = new maplibregl.Marker(configuredColor ? { color: configuredColor } : undefined) + const marker = new maplibregl.Marker(organizationColor ? { color: organizationColor } : undefined) .setLngLat([item.longitude, item.latitude]); if (popupHtml) { @@ -471,8 +404,9 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap }); return { - setActiveTagIds: (activeTagIds) => { - const activeSet = new Set(normalizeTagIds(activeTagIds)); + setOnlyTagId: (activeTagId) => { + const normalizedTagId = String(activeTagId ?? '').trim(); + const hasActiveTag = /^\d+$/.test(normalizedTagId); markerEntries.forEach((entry) => { const markerElement = entry.marker.getElement(); @@ -482,8 +416,9 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap } const hasTags = entry.tagIds.length > 0; - const isVisible = !hasTags - || entry.tagIds.some((tagId) => activeSet.has(tagId)); + const isVisible = !hasActiveTag + || !hasTags + || entry.tagIds.includes(normalizedTagId); markerElement.style.display = isVisible ? '' : 'none'; }); @@ -499,6 +434,17 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap markerElement.style.display = ''; }); }, + setVisible: (isVisible) => { + markerEntries.forEach((entry) => { + const markerElement = entry.marker.getElement(); + + if (!markerElement) { + return; + } + + markerElement.style.display = isVisible ? '' : 'none'; + }); + }, }; }; @@ -517,17 +463,13 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event const tagButtons = Array.from(wrapper.querySelectorAll('[data-map-tag-filter]')); - const collectActiveTagIds = () => tagButtons - .filter((button) => button.getAttribute('aria-pressed') === 'true') - .map((button) => String(button.dataset.mapTagFilter || '').trim()) - .filter((value) => /^\d+$/.test(value)); - const setButtonState = (button, isActive) => { button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); button.classList.toggle('is-active', isActive); }; const eventToggleButton = wrapper.querySelector('[data-map-event-toggle="1"]'); + const actionAll = wrapper.querySelector('[data-map-filter-action="all"]'); const setEventButtonState = (isActive) => { if (!eventToggleButton) { @@ -538,48 +480,80 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event eventToggleButton.classList.toggle('is-active', isActive); }; + const setAllButtonState = (isActive) => { + if (!actionAll) { + return; + } + + actionAll.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + actionAll.classList.toggle('is-active', isActive); + }; + + let activeTagId = null; + let eventsOnly = false; + const applyFilter = () => { - if (!tagButtons.length) { - organizationMarkerManager.setAllVisible(); + if (eventsOnly) { + organizationMarkerManager.setVisible(false); + setAllButtonState(false); + + if (eventLayerManager) { + eventLayerManager.setVisible(true); + } return; } - organizationMarkerManager.setActiveTagIds(collectActiveTagIds()); + organizationMarkerManager.setVisible(true); + + if (activeTagId) { + organizationMarkerManager.setOnlyTagId(activeTagId); + setAllButtonState(false); + + if (eventLayerManager) { + eventLayerManager.setVisible(false); + } + + return; + } + + organizationMarkerManager.setAllVisible(); + setAllButtonState(true); + + if (eventLayerManager) { + eventLayerManager.setVisible(true); + } }; tagButtons.forEach((button) => { button.addEventListener('click', () => { + const clickedTagId = String(button.dataset.mapTagFilter || '').trim(); + + if (!/^\d+$/.test(clickedTagId)) { + return; + } + const isActive = button.getAttribute('aria-pressed') === 'true'; - setButtonState(button, !isActive); + activeTagId = isActive ? null : clickedTagId; + eventsOnly = false; + + tagButtons.forEach((otherButton) => { + const otherTagId = String(otherButton.dataset.mapTagFilter || '').trim(); + setButtonState(otherButton, !!activeTagId && otherTagId === activeTagId); + }); + + setEventButtonState(false); applyFilter(); }); }); - const actionAll = wrapper.querySelector('[data-map-filter-action="all"]'); - const actionNone = wrapper.querySelector('[data-map-filter-action="none"]'); - if (actionAll) { actionAll.addEventListener('click', () => { - tagButtons.forEach((button) => setButtonState(button, true)); - applyFilter(); - - if (eventLayerManager) { - setEventButtonState(true); - eventLayerManager.setVisible(true); - } - }); - } - - if (actionNone) { - actionNone.addEventListener('click', () => { + activeTagId = null; + eventsOnly = false; tagButtons.forEach((button) => setButtonState(button, false)); + setEventButtonState(false); applyFilter(); - - if (eventLayerManager) { - setEventButtonState(false); - eventLayerManager.setVisible(false); - } }); } @@ -593,17 +567,30 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event toggleButton.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false'); filterGroup.hidden = !nextExpanded; - toggleButton.textContent = nextExpanded ? 'Bereiche ausblenden' : 'Bereiche anzeigen'; + toggleButton.classList.toggle('is-expanded', nextExpanded); + toggleButton.classList.toggle('is-collapsed', !nextExpanded); + + wrapper.classList.toggle('is-filter-expanded', nextExpanded); + wrapper.classList.toggle('is-filter-collapsed', !nextExpanded); }); + + const initiallyExpanded = toggleButton.getAttribute('aria-expanded') !== 'false'; + filterGroup.hidden = !initiallyExpanded; + toggleButton.classList.toggle('is-expanded', initiallyExpanded); + toggleButton.classList.toggle('is-collapsed', !initiallyExpanded); + wrapper.classList.toggle('is-filter-expanded', initiallyExpanded); + wrapper.classList.toggle('is-filter-collapsed', !initiallyExpanded); } if (eventToggleButton && eventLayerManager) { eventToggleButton.addEventListener('click', () => { const isActive = eventToggleButton.getAttribute('aria-pressed') === 'true'; - const nextActive = !isActive; + eventsOnly = !isActive; + activeTagId = null; - setEventButtonState(nextActive); - eventLayerManager.setVisible(nextActive); + tagButtons.forEach((button) => setButtonState(button, false)); + setEventButtonState(eventsOnly); + applyFilter(); }); } else if (eventToggleButton) { eventToggleButton.disabled = true; @@ -1137,10 +1124,12 @@ const initSingleMap = (container) => { const locationItems = items.filter((item) => item.type === 'location'); const eventItems = items.filter((item) => item.type === 'event'); const allCoordinates = items.map((item) => [item.longitude, item.latitude]); - const organizationColors = parseOrganizationColorScheme(container.dataset.mapOrganizationColors); - const organizationTagColorMap = buildOrganizationTagColorMap(container, organizationColors, organizationItems); + const organizationColor = normalizeHexColor( + container.dataset.mapOrganizationColor, + eventColor, + ); - const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationTagColorMap); + const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationColor); const locationLayerManager = initLocationLayers(map, locationItems, eventColor); let eventLayerManager = null; diff --git a/src/Controller/Frontend/EventMapController.php b/src/Controller/Frontend/EventMapController.php index d3e6db1..33bc5d6 100644 --- a/src/Controller/Frontend/EventMapController.php +++ b/src/Controller/Frontend/EventMapController.php @@ -35,7 +35,7 @@ class EventMapController extends AbstractFrontendModuleController $showEvents = '1' === (string) ($model->mapShowEvents ?? ''); $centerMode = (string) ($model->mapCenterMode ?? self::DEFAULT_CENTER_MODE); $eventColor = $this->normalizeHexColor((string) ($model->mapEventColor ?? self::DEFAULT_EVENT_COLOR)); - $organizationColorScheme = trim((string) ($model->mapOrganizationColorScheme ?? '')); + $organizationColor = $this->normalizeHexColor((string) ($model->mapOrganizationColor ?? $eventColor), $eventColor); if (!in_array($centerMode, ['markers', 'custom'], true)) { $centerMode = self::DEFAULT_CENTER_MODE; @@ -48,7 +48,7 @@ class EventMapController extends AbstractFrontendModuleController $template->set('mapStyleUrl', self::MAP_STYLE_URL); $template->set('mapCenterMode', $centerMode); $template->set('mapEventColor', $eventColor); - $template->set('mapOrganizationColorScheme', $organizationColorScheme); + $template->set('mapOrganizationColor', $organizationColor); $template->set('mapCenterLat', trim((string) ($model->mapCenterLat ?? ''))); $template->set('mapCenterLng', trim((string) ($model->mapCenterLng ?? ''))); $template->set('mapCenterZoom', (int) ($model->mapCenterZoom ?? 12)); @@ -61,7 +61,7 @@ class EventMapController extends AbstractFrontendModuleController return $template->getResponse(); } - private function normalizeHexColor(string $value): string + private function normalizeHexColor(string $value, string $fallback = self::DEFAULT_EVENT_COLOR): string { $normalized = strtoupper(trim($value)); @@ -91,6 +91,6 @@ class EventMapController extends AbstractFrontendModuleController ); } - return self::DEFAULT_EVENT_COLOR; + return $fallback; } }