Make "Active Filters" accessible from Javascript

While working on our Event Calendar this past weekend, we noticed there wasn’t an easy way to set the Event Calendar filters using query params automatically. We managed to get this working with the following script in the code block that loads the calendar:

<script src="https://static.elfsight.com/platform/platform.js" data-use-service-core defer></script>
<div class="elfsight-app-WIDGET_ID" data-elfsight-app-layout="month"></div>
<script>
  (function () {
    // Parse query parameters from URL
    function getQueryParams() {
      const params = new URLSearchParams(window.location.search);
      const filters = {};

      // Map query parameters to Elfsight calendar attributes
      // Adjust these parameter names based on what you want to support
      const paramMapping = {
        tags: "data-elfsight-app-filter-tags",
        eventType: "data-elfsight-app-filter-event-type",
        host: "data-elfsight-app-filter-host",
        venue: "data-elfsight-app-filter-venue",
      };

      // Collect all matching parameters
      for (const [queryParam, dataAttr] of Object.entries(paramMapping)) {
        const value = params.get(queryParam);
        if (value) {
          filters[dataAttr] = value;
        }
      }

      return filters;
    }

    // Apply filters to Elfsight calendar widgets
    function applyFiltersToCalendar() {
      const filters = getQueryParams();

      // Find all Elfsight calendar elements
      const calendarElements = document.querySelectorAll(
        '[class*="elfsight-app"]'
      );

      if (Object.keys(filters).length === 0) {
        console.log("No calendar filters found in query parameters");
        return;
      }

      // Apply each filter as a data attribute
      calendarElements.forEach((element) => {
        for (const [attr, value] of Object.entries(filters)) {
          element.setAttribute(attr, value);
        }
      });
    }

    // Wait for DOM to be ready
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", applyFiltersToCalendar);
    } else {
      applyFiltersToCalendar();
    }
  })();
</script>

This works well for creating links to the page with pre-selected filters, but it doesn’t handle the scenario where we want to update the query params in the URL as the user selects the filters manually. One such use case for this would be to “share” the link to the pre-filtered calendar with someone else.

We were hoping to “inspect” the currently active filters using window.eventsCalendar.app but couldn’t find the information we were looking for.

Would it be possible to have the calendar automatically update the query params with the active filters, or at least make this information available on the page so we can adjust the query params ourselves?

1 Like

Hi there, @user9181 :waving_hand:

Thanks for such a detailed description of your use case!

I’ve forwarded your request to the devs and will update you once I have their response :slightly_smiling_face:

Thank you for forwarding this on to the developers. Ideally, the calendar would trigger an event when the filters are changed so we could react to the filters changing with an event listener (in our case, updating the query parameters in the URL).

1 Like

Yep, we’ll try to include this functionality in our custom solution. I’ll keep you updated :slightly_smiling_face:

Hi there, @user9181 :waving_hand:

Thank you for waiting!

Here is a solution from our devs:

const waitForElement = (selector, root = document) =>
	new Promise((resolve) => {
		const element = root.querySelector(selector);
		if (element) return resolve(element);

		const observer = new MutationObserver(() => {
			const el = root.querySelector(selector);
			if (el) {
				resolve(el);
				observer.disconnect();
			}
		});

		observer.observe(root, { childList: true, subtree: true });
	});

const filters = {};
let tempFilters = {};

const params = new URLSearchParams(window.location.search);
for (const [key, value] of params.entries()) {
	filters[key] = value.split('; ').filter(Boolean);
	tempFilters[key] = value.split('; ').filter(Boolean);
}

const getFilterType = (type) => {
	switch (type) {
		case 'tag':
			return 'tags';
		case 'location':
			return 'venue';
		default:
			return type;
	}
};

const toggleValue = (store, type, value) => {
	if (!store[type]) store[type] = [];
	const idx = store[type].indexOf(value);
	if (idx === -1) store[type].push(value);
	else store[type].splice(idx, 1);
};

const updateUrlFromFilters = () => {
	const params = new URLSearchParams();
	for (const [key, values] of Object.entries(filters)) {
		if (values.length) params.set(key, values.join('; '));
	}
	history.replaceState(
		null,
		'',
		`${window.location.pathname}?${params.toString()}`
	);
};

waitForElement(
	'#eapps-events-calendar-5cd9b582-3f6e-498a-84c2-c6f17e9a1da1'
).then((container) => {
	const filterButtons = container.querySelectorAll(
		'.es-filters-item-container'
	);

	filterButtons.forEach((filterButton) => {
		filterButton.addEventListener('click', () => {
			const type = getFilterType(filterButton.getAttribute('data-type'));

			waitForElement('div[data-radix-popper-content-wrapper]').then(
				(popover) => {
					popover.dataset.currentType = type;

					if (!tempFilters) {
						tempFilters = JSON.parse(JSON.stringify(filters));
					}

					if (popover.dataset.listenerAttached) return;

					const optionsContainer = popover.querySelector(
						'[class*="expandable__Container-sc"]'
					);
					if (!optionsContainer) return;

					optionsContainer.addEventListener('click', (e) => {
						if (e.detail === 0) return;

						const clickedButton = e.target.closest('.es-filter-button-button');
						const clickedCheckboxItem = e.target.closest(
							'[class*="checklist-item__ItemContainer-sc"]'
						);
						const target = clickedButton ?? clickedCheckboxItem;
						if (!target) return;

						const textEl =
							target.querySelector('.es-filter-button-text') ||
							target.querySelector('[class*="checklist-item__Label-sc"]');

						const currentType = popover.dataset.currentType;
						const value = textEl?.textContent?.trim();
						if (!value || !currentType) return;

						toggleValue(tempFilters, currentType, value);
					});

					// --- Apply ---
					const applyButton = popover.querySelector(
						'.es-apply-filters-button-button'
					);
					if (applyButton && !applyButton.dataset.listenerAttached) {
						applyButton.addEventListener('click', () => {
							if (!tempFilters) return;

							Object.assign(filters, tempFilters);
							updateUrlFromFilters();

							tempFilters = null;
						});
						applyButton.dataset.listenerAttached = '1';
					}

					// --- Clear ---
					waitForElement(
						'div[data-radix-popper-content-wrapper] .es-clear-filters-button-button'
					).then((clearButton) => {
						if (clearButton.dataset.listenerAttached) return;

						clearButton.addEventListener('click', () => {
							const currentType = popover.dataset.currentType;
							if (!currentType) return;

							if (tempFilters && tempFilters[currentType]) {
								tempFilters[currentType] = [];
							}
						});

						clearButton.dataset.listenerAttached = '1';
					});

					popover.dataset.listenerAttached = '1';
				}
			);
		});

		const buttonEl = filterButton.querySelector(
			'button[aria-haspopup="dialog"][type="button"]'
		);
		if (!buttonEl) return;

		const observer = new MutationObserver((mutations) => {
			mutations.forEach((m) => {
				if (m.type === 'attributes' && m.attributeName === 'data-state') {
					const state = buttonEl.dataset.state;

					if (state === 'closed') {
						const popover = document.querySelector(
							'div[data-radix-popper-content-wrapper]'
						);
						const currentType =
							popover?.dataset.currentType ||
							getFilterType(filterButton.getAttribute('data-type'));
						if (!currentType) return;

						if (tempFilters) {
							tempFilters[currentType] = filters[currentType] ?? [];
						}
					}
				}
			});
		});

		observer.observe(buttonEl, {
			attributes: true,
			attributeFilter: ['data-state'],
		});
	});

	const wrapper = container.querySelector('.es-filters-wrapper');
	if (wrapper) {
		const attachGlobalClearListener = (button) => {
			if (button && !button.dataset.globalListenerAttached) {
				button.addEventListener('click', () => {
					if (!filters) filters = {};
					if (!tempFilters) tempFilters = {};

					Object.keys(filters).forEach((key) => (filters[key] = []));
					Object.keys(tempFilters).forEach((key) => (tempFilters[key] = []));

					updateUrlFromFilters();
				});
				button.dataset.globalListenerAttached = '1';
			}
		};

		const existingButton = wrapper.querySelector(
			':scope > .es-clear-filters-button-button'
		);
		attachGlobalClearListener(existingButton);

		const globalObserver = new MutationObserver((mutations) => {
			mutations.forEach((m) => {
				if (m.type === 'childList') {
					const clearAllButton = wrapper.querySelector(
						':scope > .es-clear-filters-button-button'
					);
					attachGlobalClearListener(clearAllButton);
				}
			});
		});

		globalObserver.observe(wrapper, { childList: true, subtree: true });
	}
});

Please add this code to the Custom JS field on the Settings tab of your widget’s settings and let me know if it worked :slightly_smiling_face:

I also agree that it would be great to have links to specific categories by default and added this idea to the Wishlist on your behalf - Add query params to website URL when changing filters (get links to events filtered by specific criteria)