Compare commits

..

2 Commits

Author SHA1 Message Date
Jürgen Mummert fb58c50f18 feat(map): allow backend-selected organization tags 2026-02-26 18:36:35 +01:00
Jürgen Mummert 621ce8dc8b fix(map): simplify filters and restore style toggle behavior 2026-02-26 18:26:20 +01:00
7 changed files with 172 additions and 142 deletions
+5 -5
View File
@@ -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_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']['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']['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,organizationTypeTags,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']['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'; $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'], 'sql' => ['type' => 'string', 'length' => 7, 'default' => '#BC5067'],
]; ];
$GLOBALS['TL_DCA']['tl_module']['fields']['mapOrganizationColorScheme'] = [ $GLOBALS['TL_DCA']['tl_module']['fields']['mapOrganizationColor'] = [
'label' => &$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColorScheme'], 'label' => &$GLOBALS['TL_LANG']['tl_module']['mapOrganizationColor'],
'exclude' => true, 'exclude' => true,
'inputType' => 'text', 'inputType' => 'text',
'eval' => ['maxlength' => 1024, 'tl_class' => 'clr long'], 'eval' => ['maxlength' => 7, 'rgxp' => 'hexcolor', 'colorpicker' => true, 'tl_class' => 'w50'],
'sql' => ['type' => 'string', 'length' => 1024, 'default' => ''], 'sql' => ['type' => 'string', 'length' => 7, 'default' => '#BC5067'],
]; ];
$GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterMode'] = [ $GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterMode'] = [
+1 -1
View File
@@ -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']['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']['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']['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'] = ['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'] = [ $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
'markers' => 'Anhand der Marker (alle Marker sichtbar)', 'markers' => 'Anhand der Marker (alle Marker sichtbar)',
+1 -1
View File
@@ -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']['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']['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']['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'] = ['Map centering', 'Choose whether the map should center by markers or fixed coordinates.'];
$GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [ $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
'markers' => 'By markers (fit all visible markers)', 'markers' => 'By markers (fit all visible markers)',
+30 -16
View File
@@ -9,12 +9,13 @@
> >
<button <button
type="button" type="button"
class="eventmanager-map-filter__toggle" class="eventmanager-map-filter__toggle is-expanded"
data-map-filter-toggle="1" data-map-filter-toggle="1"
aria-expanded="true" aria-expanded="true"
aria-controls="{{ mapFilterGroupId|e('html_attr') }}" aria-controls="{{ mapFilterGroupId|e('html_attr') }}"
> >
Bereiche ausblenden <span class="eventmanager-map-filter__toggle-label eventmanager-map-filter__toggle-label--expand">Filter einblenden</span>
<span class="eventmanager-map-filter__toggle-label eventmanager-map-filter__toggle-label--collapse">Filter ausblenden</span>
</button> </button>
<div <div
@@ -23,29 +24,43 @@
role="group" role="group"
aria-label="Organisationstypen" aria-label="Organisationstypen"
> >
<button
type="button"
class="eventmanager-map-filter__tag is-active"
data-map-filter-action="all"
aria-pressed="true"
>Alle</button>
{% if tags is iterable and tags|length > 0 %} {% if tags is iterable and tags|length > 0 %}
{% for tag in tags %} {% for tag in tags %}
<button <button
type="button" type="button"
class="eventmanager-map-filter__tag is-active" class="eventmanager-map-filter__tag"
data-map-tag-filter="{{ tag.id|e('html_attr') }}" data-map-tag-filter="{{ tag.id|e('html_attr') }}"
aria-pressed="true" aria-pressed="false"
>{{ tag.label|e }}</button> >{{ tag.label|e }}</button>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<button <button
type="button" type="button"
class="eventmanager-map-filter__tag is-active" class="eventmanager-map-filter__tag"
data-map-event-toggle="1" data-map-event-toggle="1"
aria-pressed="true" aria-pressed="false"
>Veranstaltungen</button> >Veranstaltungen</button>
</div>
<div class="eventmanager-map-filter__actions"> <button
<button type="button" data-map-filter-action="all">Alle auswählen</button> type="button"
<button type="button" data-map-filter-action="none">Alle abwählen</button> class="eventmanager-map-filter__tag"
<button type="button" data-map-style-mode="street" aria-pressed="true">Straße</button> data-map-style-mode="street"
<button type="button" data-map-style-mode="satellite" aria-pressed="false">Satellit</button> aria-pressed="true"
>Straße</button>
<button
type="button"
class="eventmanager-map-filter__tag"
data-map-style-mode="satellite"
aria-pressed="false"
>Satellit</button>
</div> </div>
</section> </section>
@@ -57,7 +72,7 @@
data-map-style="{{ mapStyleUrl|e('html_attr') }}" data-map-style="{{ mapStyleUrl|e('html_attr') }}"
data-map-data-id="{{ mapDataElementId|e('html_attr') }}" data-map-data-id="{{ mapDataElementId|e('html_attr') }}"
data-map-event-color="{{ mapEventColor|default('#BC5067')|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-mode="{{ mapCenterMode|default('markers')|e('html_attr') }}"
data-map-center-lat="{{ mapCenterLat|default('')|e('html_attr') }}" data-map-center-lat="{{ mapCenterLat|default('')|e('html_attr') }}"
data-map-center-lng="{{ mapCenterLng|default('')|e('html_attr') }}" data-map-center-lng="{{ mapCenterLng|default('')|e('html_attr') }}"
@@ -71,8 +86,7 @@
margin-bottom: .75rem; margin-bottom: .75rem;
} }
.eventmanager-map-filter__group, .eventmanager-map-filter__group {
.eventmanager-map-filter__actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: .5rem; gap: .5rem;
@@ -89,4 +103,4 @@
</style> </style>
<script type="application/json" id="{{ mapDataElementId|e('html_attr') }}">{{ mapItemsJson|raw }}</script> <script type="application/json" id="{{ mapDataElementId|e('html_attr') }}">{{ mapItemsJson|raw }}</script>
<script type="module" src="/bundles/mummertmediaeventmanager/assets/map-module.js"></script> <script type="module" src="/bundles/mummertmediaeventmanager/assets/map-module.js?v=20260226"></script>
+100 -111
View File
@@ -199,70 +199,6 @@ const normalizeHexColor = (value, fallback = DEFAULT_EVENT_COLOR) => {
return fallback; 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) => { const applyDefaultProjection = (map) => {
if (!map || typeof map.setProjection !== 'function') { if (!map || typeof map.setProjection !== 'function') {
return; return;
@@ -441,21 +377,18 @@ const toFeature = (item) => ({
}, },
}); });
const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap) => { const initOrganizationMarkers = (map, organizationItems, organizationColor) => {
if (!organizationItems.length) { if (!organizationItems.length) {
return { return {
setActiveTagIds: () => {}, setOnlyTagId: () => {},
setAllVisible: () => {}, setAllVisible: () => {},
setVisible: () => {},
}; };
} }
const markerEntries = organizationItems.map((item) => { const markerEntries = organizationItems.map((item) => {
const popupHtml = popupHtmlFor(item); const popupHtml = popupHtmlFor(item);
const primaryTagId = String(item?.extra?.organizationTagIds?.[0] || '').trim(); const marker = new maplibregl.Marker(organizationColor ? { color: organizationColor } : undefined)
const configuredColor = /^\d+$/.test(primaryTagId)
? (organizationTagColorMap?.byTagId?.[primaryTagId] || organizationTagColorMap?.fallbackColor || null)
: null;
const marker = new maplibregl.Marker(configuredColor ? { color: configuredColor } : undefined)
.setLngLat([item.longitude, item.latitude]); .setLngLat([item.longitude, item.latitude]);
if (popupHtml) { if (popupHtml) {
@@ -471,8 +404,9 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap
}); });
return { return {
setActiveTagIds: (activeTagIds) => { setOnlyTagId: (activeTagId) => {
const activeSet = new Set(normalizeTagIds(activeTagIds)); const normalizedTagId = String(activeTagId ?? '').trim();
const hasActiveTag = /^\d+$/.test(normalizedTagId);
markerEntries.forEach((entry) => { markerEntries.forEach((entry) => {
const markerElement = entry.marker.getElement(); const markerElement = entry.marker.getElement();
@@ -482,8 +416,9 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap
} }
const hasTags = entry.tagIds.length > 0; const hasTags = entry.tagIds.length > 0;
const isVisible = !hasTags const isVisible = !hasActiveTag
|| entry.tagIds.some((tagId) => activeSet.has(tagId)); || !hasTags
|| entry.tagIds.includes(normalizedTagId);
markerElement.style.display = isVisible ? '' : 'none'; markerElement.style.display = isVisible ? '' : 'none';
}); });
@@ -499,6 +434,17 @@ const initOrganizationMarkers = (map, organizationItems, organizationTagColorMap
markerElement.style.display = ''; 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 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) => { const setButtonState = (button, isActive) => {
button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
button.classList.toggle('is-active', isActive); button.classList.toggle('is-active', isActive);
}; };
const eventToggleButton = wrapper.querySelector('[data-map-event-toggle="1"]'); const eventToggleButton = wrapper.querySelector('[data-map-event-toggle="1"]');
const actionAll = wrapper.querySelector('[data-map-filter-action="all"]');
const setEventButtonState = (isActive) => { const setEventButtonState = (isActive) => {
if (!eventToggleButton) { if (!eventToggleButton) {
@@ -538,48 +480,80 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
eventToggleButton.classList.toggle('is-active', isActive); 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 = () => { const applyFilter = () => {
if (!tagButtons.length) { if (eventsOnly) {
organizationMarkerManager.setAllVisible(); organizationMarkerManager.setVisible(false);
setAllButtonState(false);
if (eventLayerManager) {
eventLayerManager.setVisible(true);
}
return; 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) => { tagButtons.forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const clickedTagId = String(button.dataset.mapTagFilter || '').trim();
if (!/^\d+$/.test(clickedTagId)) {
return;
}
const isActive = button.getAttribute('aria-pressed') === 'true'; 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(); applyFilter();
}); });
}); });
const actionAll = wrapper.querySelector('[data-map-filter-action="all"]');
const actionNone = wrapper.querySelector('[data-map-filter-action="none"]');
if (actionAll) { if (actionAll) {
actionAll.addEventListener('click', () => { actionAll.addEventListener('click', () => {
tagButtons.forEach((button) => setButtonState(button, true)); activeTagId = null;
applyFilter(); eventsOnly = false;
if (eventLayerManager) {
setEventButtonState(true);
eventLayerManager.setVisible(true);
}
});
}
if (actionNone) {
actionNone.addEventListener('click', () => {
tagButtons.forEach((button) => setButtonState(button, false)); tagButtons.forEach((button) => setButtonState(button, false));
applyFilter();
if (eventLayerManager) {
setEventButtonState(false); setEventButtonState(false);
eventLayerManager.setVisible(false); applyFilter();
}
}); });
} }
@@ -593,17 +567,30 @@ const bindExternalTagFilters = (container, map, organizationMarkerManager, event
toggleButton.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false'); toggleButton.setAttribute('aria-expanded', nextExpanded ? 'true' : 'false');
filterGroup.hidden = !nextExpanded; 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) { if (eventToggleButton && eventLayerManager) {
eventToggleButton.addEventListener('click', () => { eventToggleButton.addEventListener('click', () => {
const isActive = eventToggleButton.getAttribute('aria-pressed') === 'true'; const isActive = eventToggleButton.getAttribute('aria-pressed') === 'true';
const nextActive = !isActive; eventsOnly = !isActive;
activeTagId = null;
setEventButtonState(nextActive); tagButtons.forEach((button) => setButtonState(button, false));
eventLayerManager.setVisible(nextActive); setEventButtonState(eventsOnly);
applyFilter();
}); });
} else if (eventToggleButton) { } else if (eventToggleButton) {
eventToggleButton.disabled = true; eventToggleButton.disabled = true;
@@ -1137,10 +1124,12 @@ const initSingleMap = (container) => {
const locationItems = items.filter((item) => item.type === 'location'); const locationItems = items.filter((item) => item.type === 'location');
const eventItems = items.filter((item) => item.type === 'event'); const eventItems = items.filter((item) => item.type === 'event');
const allCoordinates = items.map((item) => [item.longitude, item.latitude]); const allCoordinates = items.map((item) => [item.longitude, item.latitude]);
const organizationColors = parseOrganizationColorScheme(container.dataset.mapOrganizationColors); const organizationColor = normalizeHexColor(
const organizationTagColorMap = buildOrganizationTagColorMap(container, organizationColors, organizationItems); container.dataset.mapOrganizationColor,
eventColor,
);
const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationTagColorMap); const organizationMarkerManager = initOrganizationMarkers(map, organizationItems, organizationColor);
const locationLayerManager = initLocationLayers(map, locationItems, eventColor); const locationLayerManager = initLocationLayers(map, locationItems, eventColor);
let eventLayerManager = null; let eventLayerManager = null;
+11 -6
View File
@@ -8,6 +8,7 @@ use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule; use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
use Contao\CoreBundle\Twig\FragmentTemplate; use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\ModuleModel; use Contao\ModuleModel;
use Contao\StringUtil;
use MummertMedia\EventManagerBundle\Service\MapModuleDataProvider; use MummertMedia\EventManagerBundle\Service\MapModuleDataProvider;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -33,9 +34,13 @@ class EventMapController extends AbstractFrontendModuleController
$showOrganizations = '1' === (string) ($model->mapShowOrganizations ?? ''); $showOrganizations = '1' === (string) ($model->mapShowOrganizations ?? '');
$showExternalOrganizations = '1' === (string) ($model->mapShowExternalOrganizations ?? ''); $showExternalOrganizations = '1' === (string) ($model->mapShowExternalOrganizations ?? '');
$showEvents = '1' === (string) ($model->mapShowEvents ?? ''); $showEvents = '1' === (string) ($model->mapShowEvents ?? '');
$selectedOrganizationTagIds = array_values(array_unique(array_filter(
array_map('intval', StringUtil::deserialize($model->organizationTypeTags ?? null, true)),
static fn (int $tagId): bool => $tagId > 0,
)));
$centerMode = (string) ($model->mapCenterMode ?? self::DEFAULT_CENTER_MODE); $centerMode = (string) ($model->mapCenterMode ?? self::DEFAULT_CENTER_MODE);
$eventColor = $this->normalizeHexColor((string) ($model->mapEventColor ?? self::DEFAULT_EVENT_COLOR)); $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)) { if (!in_array($centerMode, ['markers', 'custom'], true)) {
$centerMode = self::DEFAULT_CENTER_MODE; $centerMode = self::DEFAULT_CENTER_MODE;
@@ -48,20 +53,20 @@ class EventMapController extends AbstractFrontendModuleController
$template->set('mapStyleUrl', self::MAP_STYLE_URL); $template->set('mapStyleUrl', self::MAP_STYLE_URL);
$template->set('mapCenterMode', $centerMode); $template->set('mapCenterMode', $centerMode);
$template->set('mapEventColor', $eventColor); $template->set('mapEventColor', $eventColor);
$template->set('mapOrganizationColorScheme', $organizationColorScheme); $template->set('mapOrganizationColor', $organizationColor);
$template->set('mapCenterLat', trim((string) ($model->mapCenterLat ?? ''))); $template->set('mapCenterLat', trim((string) ($model->mapCenterLat ?? '')));
$template->set('mapCenterLng', trim((string) ($model->mapCenterLng ?? ''))); $template->set('mapCenterLng', trim((string) ($model->mapCenterLng ?? '')));
$template->set('mapCenterZoom', (int) ($model->mapCenterZoom ?? 12)); $template->set('mapCenterZoom', (int) ($model->mapCenterZoom ?? 12));
$template->set('mapItemsJson', json_encode( $template->set('mapItemsJson', json_encode(
$this->mapModuleDataProvider->getMapItems($showOrganizations, $showEvents, $showExternalOrganizations), $this->mapModuleDataProvider->getMapItems($showOrganizations, $showEvents, $showExternalOrganizations, $selectedOrganizationTagIds),
\JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR,
)); ));
$template->set('mapOrganizationTags', $this->mapModuleDataProvider->getOrganizationTags()); $template->set('mapOrganizationTags', $this->mapModuleDataProvider->getOrganizationTags($selectedOrganizationTagIds));
return $template->getResponse(); 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)); $normalized = strtoupper(trim($value));
@@ -91,6 +96,6 @@ class EventMapController extends AbstractFrontendModuleController
); );
} }
return self::DEFAULT_EVENT_COLOR; return $fallback;
} }
} }
+24 -2
View File
@@ -26,12 +26,17 @@ class MapModuleDataProvider
/** /**
* @return list<array{type:string,markerType:string,id:int,title:string,latitude:float,longitude:float,extra:array<string,mixed>}> * @return list<array{type:string,markerType:string,id:int,title:string,latitude:float,longitude:float,extra:array<string,mixed>}>
*/ */
public function getMapItems(bool $includeOrganizations = true, bool $includeEvents = true, bool $includeExternalOrganizations = false): array public function getMapItems(bool $includeOrganizations = true, bool $includeEvents = true, bool $includeExternalOrganizations = false, array $selectedOrganizationTagIds = []): array
{ {
if (!$includeOrganizations && !$includeEvents) { if (!$includeOrganizations && !$includeEvents) {
return []; return [];
} }
$selectedOrganizationTagIds = array_values(array_unique(array_map(
static fn (int|string $tagId): string => (string) (int) $tagId,
array_filter($selectedOrganizationTagIds, static fn (int|string $tagId): bool => (int) $tagId > 0),
)));
$locationTable = $this->resolveExistingTable(['tl_location']); $locationTable = $this->resolveExistingTable(['tl_location']);
$organizationTable = $this->resolveExistingTable(['tl_organization', 'tl_organisation']); $organizationTable = $this->resolveExistingTable(['tl_organization', 'tl_organisation']);
$locationGeoColumns = null !== $locationTable ? $this->resolveGeoColumns($locationTable) : null; $locationGeoColumns = null !== $locationTable ? $this->resolveGeoColumns($locationTable) : null;
@@ -76,6 +81,12 @@ class MapModuleDataProvider
$seen['organisation'][$id] = true; $seen['organisation'][$id] = true;
$tagData = $organizationTagMap[$id] ?? ['ids' => [], 'labels' => []]; $tagData = $organizationTagMap[$id] ?? ['ids' => [], 'labels' => []];
if ([] !== $selectedOrganizationTagIds
&& [] === array_intersect($selectedOrganizationTagIds, $tagData['ids'] ?? [])) {
continue;
}
$items[] = [ $items[] = [
'type' => 'organisation', 'type' => 'organisation',
'markerType' => $this->buildOrganizationMarkerType($tagData['ids']), 'markerType' => $this->buildOrganizationMarkerType($tagData['ids']),
@@ -152,12 +163,17 @@ class MapModuleDataProvider
/** /**
* @return list<array{id:int,label:string}> * @return list<array{id:int,label:string}>
*/ */
public function getOrganizationTags(): array public function getOrganizationTags(array $selectedTagIds = []): array
{ {
if (!$this->tableExists('tl_tags')) { if (!$this->tableExists('tl_tags')) {
return []; return [];
} }
$selectedTagIds = array_values(array_unique(array_filter(
array_map('intval', $selectedTagIds),
static fn (int $tagId): bool => $tagId > 0,
)));
$columns = $this->getColumnMap('tl_tags'); $columns = $this->getColumnMap('tl_tags');
$labelColumn = isset($columns['title']) ? 'title' : (isset($columns['tag']) ? 'tag' : null); $labelColumn = isset($columns['title']) ? 'title' : (isset($columns['tag']) ? 'tag' : null);
@@ -171,6 +187,12 @@ class MapModuleDataProvider
->from('tl_tags', 't') ->from('tl_tags', 't')
->orderBy(sprintf('t.%s', $labelColumn), 'ASC'); ->orderBy(sprintf('t.%s', $labelColumn), 'ASC');
if ([] !== $selectedTagIds) {
$qb
->andWhere('t.id IN (:selectedTagIds)')
->setParameter('selectedTagIds', $selectedTagIds, ArrayParameterType::INTEGER);
}
if (isset($columns['invisible'])) { if (isset($columns['invisible'])) {
$qb->andWhere("(t.invisible IS NULL OR t.invisible = '' OR t.invisible = '0')"); $qb->andWhere("(t.invisible IS NULL OR t.invisible = '' OR t.invisible = '0')");
} }