Add configurable target event list ID for event filter module
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<div id="eventfilters">
|
||||
<button type="button" data-filter-tag="all">Alle</button>
|
||||
<div id="eventfilters" data-eventlist-id="{{ targetEventListId|default('eventlist')|e('html_attr') }}">
|
||||
<button type="button" data-filter-tag="all" class="active">Alle</button>
|
||||
|
||||
{% for tag in tagButtons|default([]) %}
|
||||
<button type="button" data-filter-tag="{{ tag.id }}">{{ tag.title }} ({{ tag.count }})</button>
|
||||
@@ -25,3 +25,182 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.event-filter-target-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.event-filter-target-list .event {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 220ms ease, transform 220ms ease;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.event-filter-target-list .event.is-filtered-out {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#eventfilters .active,
|
||||
#eventfilters select.active {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
const filters = document.getElementById('eventfilters');
|
||||
|
||||
if (!filters) {
|
||||
throw new Error('Event filter requires #eventfilters.');
|
||||
}
|
||||
|
||||
const targetEventListId = filters.dataset.eventlistId || 'eventlist';
|
||||
const list = document.getElementById(targetEventListId);
|
||||
|
||||
if (!list) {
|
||||
console.warn(`[event_filter] Target event list #${targetEventListId} was not found.`);
|
||||
}
|
||||
|
||||
list?.classList.add('event-filter-target-list');
|
||||
|
||||
const events = list ? Array.from(list.querySelectorAll(':scope > .event')) : [];
|
||||
const tagButtons = Array.from(filters.querySelectorAll('button[data-filter-tag]'));
|
||||
const locationSelect = filters.querySelector('#location-filter');
|
||||
const orgSelect = filters.querySelector('#org-filter');
|
||||
|
||||
const animationMs = 220;
|
||||
let hideTimers = new WeakMap();
|
||||
let currentFilter = { type: 'tag', value: 'all' };
|
||||
|
||||
const clearHideTimer = (eventItem) => {
|
||||
const timer = hideTimers.get(eventItem);
|
||||
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
hideTimers.delete(eventItem);
|
||||
}
|
||||
};
|
||||
|
||||
const showEvent = (eventItem) => {
|
||||
clearHideTimer(eventItem);
|
||||
eventItem.hidden = false;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
eventItem.classList.remove('is-filtered-out');
|
||||
});
|
||||
};
|
||||
|
||||
const hideEvent = (eventItem) => {
|
||||
clearHideTimer(eventItem);
|
||||
eventItem.classList.add('is-filtered-out');
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
eventItem.hidden = true;
|
||||
hideTimers.delete(eventItem);
|
||||
}, animationMs);
|
||||
|
||||
hideTimers.set(eventItem, timer);
|
||||
};
|
||||
|
||||
const setActiveControl = ({ type, value }) => {
|
||||
tagButtons.forEach((button) => {
|
||||
button.classList.toggle('active', type === 'tag' && button.dataset.filterTag === value);
|
||||
});
|
||||
|
||||
if (locationSelect) {
|
||||
locationSelect.classList.toggle('active', type === 'location');
|
||||
}
|
||||
|
||||
if (orgSelect) {
|
||||
orgSelect.classList.toggle('active', type === 'org');
|
||||
}
|
||||
};
|
||||
|
||||
const parseIdList = (rawValue) => (rawValue ?? '')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const matches = (eventItem, filterState) => {
|
||||
if (filterState.value === 'all') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filterState.type === 'tag') {
|
||||
return parseIdList(eventItem.dataset.tags).includes(filterState.value);
|
||||
}
|
||||
|
||||
if (filterState.type === 'location') {
|
||||
return eventItem.dataset.location === filterState.value;
|
||||
}
|
||||
|
||||
if (filterState.type === 'org') {
|
||||
return parseIdList(eventItem.dataset.org).includes(filterState.value);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const applyFilter = (filterState) => {
|
||||
currentFilter = filterState;
|
||||
setActiveControl(filterState);
|
||||
|
||||
events.forEach((eventItem) => {
|
||||
if (matches(eventItem, filterState)) {
|
||||
showEvent(eventItem);
|
||||
} else {
|
||||
hideEvent(eventItem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tagButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
if (locationSelect) {
|
||||
locationSelect.value = 'all';
|
||||
}
|
||||
|
||||
if (orgSelect) {
|
||||
orgSelect.value = 'all';
|
||||
}
|
||||
|
||||
applyFilter({ type: 'tag', value: button.dataset.filterTag ?? 'all' });
|
||||
});
|
||||
});
|
||||
|
||||
locationSelect?.addEventListener('change', () => {
|
||||
const selectedValue = locationSelect.value;
|
||||
|
||||
if (orgSelect) {
|
||||
orgSelect.value = 'all';
|
||||
}
|
||||
|
||||
applyFilter(
|
||||
selectedValue === 'all'
|
||||
? { type: 'tag', value: 'all' }
|
||||
: { type: 'location', value: selectedValue.replace('location-', '') },
|
||||
);
|
||||
});
|
||||
|
||||
orgSelect?.addEventListener('change', () => {
|
||||
const selectedValue = orgSelect.value;
|
||||
|
||||
if (locationSelect) {
|
||||
locationSelect.value = 'all';
|
||||
}
|
||||
|
||||
applyFilter(
|
||||
selectedValue === 'all'
|
||||
? { type: 'tag', value: 'all' }
|
||||
: { type: 'org', value: selectedValue.replace('org-', '') },
|
||||
);
|
||||
});
|
||||
|
||||
applyFilter(currentFilter);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user