diff --git a/src/components/organisms/Calendar/Calendar.js b/src/components/organisms/Calendar/Calendar.js
index c4c58c5f..2fee08cb 100644
--- a/src/components/organisms/Calendar/Calendar.js
+++ b/src/components/organisms/Calendar/Calendar.js
@@ -9,7 +9,11 @@ import dayGridPlugin from '@fullcalendar/daygrid';
const template = document.createElement('template');
template.innerHTML = `
-
+
`;
@@ -58,6 +62,7 @@ class Calendar extends HTMLElement {
this.calendar = new FullCalendar(calendarEl, {
plugins: [dayGridPlugin],
initialView: 'dayGridMonth',
+ height: 'auto',
headerToolbar: {
left: 'title',
center: '',
@@ -65,6 +70,7 @@ class Calendar extends HTMLElement {
},
eventSources: [eventArraySource],
});
+ this.buildEventFilters();
this.calendar.render();
}
@@ -120,6 +126,117 @@ class Calendar extends HTMLElement {
return this.shadowRoot.getElementById('calendar').childElementCount > 0;
}
+ /**
+ * Creates filter form elements above the calendar based on the event filters provided.
+ *
+ * @param {string} [newFiltersJSON=null] - A JSON serialized array of event filter definitions.
+ * If null, filters will be fetched from the 'event-filters' attribute on the
+ * component instead.
+ * @returns void
+ */
+ buildEventFilters(newFiltersJSON = null) {
+ const calendarFilterElt = this.shadowRoot.getElementById('calendarFilters');
+ if (calendarFilterElt === null) {
+ return;
+ }
+
+ if (newFiltersJSON === null) {
+ newFiltersJSON = this.getAttribute('event-filters');
+ }
+
+ let filters = {};
+ try {
+ filters = JSON.parse(newFiltersJSON ?? '{}');
+ } catch (error) {
+ // TODO: Introduce proper error logging.
+ // eslint-disable-next-line no-console
+ console.error(`Failed to parse list of filters:\n${newFiltersJSON}`);
+ }
+ for (const filter in filters) {
+ this.buildEventFilter(calendarFilterElt, filters[filter]);
+ }
+ }
+
+ /**
+ * Creates a single filter form field set above the calendar based
+ * on the event filter provided.
+ *
+ * @param {HTMLElement} calendarFilterElt - An HTML element to be used
+ * as the container for the event filter created.
+ * @param {Object} filter - A single event filter object.
+ * @returns void
+ */
+ buildEventFilter(calendarFilterElt, filter) {
+ switch (filter.type) {
+ case 'radio': {
+ const radioFiltersContainer = document.createElement('fieldset');
+ radioFiltersContainer.classList.add(
+ 'd-flex',
+ 'flex-wrap',
+ 'm-3',
+ 'justify-content-center',
+ );
+ const legend = document.createElement('legend');
+ legend.classList.add('visually-hidden');
+ legend.innerText = filter.legend;
+ radioFiltersContainer.appendChild(legend);
+ filter.values.forEach((value) => {
+ const radioButtonContainer = document.createElement('div');
+ radioButtonContainer.classList.add('m-2');
+ const radioButtonInput = document.createElement('input');
+ radioButtonInput.setAttribute('type', 'radio');
+ radioButtonInput.setAttribute('id', value);
+ radioButtonInput.setAttribute('name', filter.key);
+ radioButtonInput.setAttribute('value', value);
+ radioButtonInput.classList.add('btn-check');
+ // Bind event handler to this instance.
+ radioButtonInput.addEventListener(
+ 'click',
+ this.filterEvents.bind(this),
+ );
+ radioButtonContainer.appendChild(radioButtonInput);
+ const radioButtonLabel = document.createElement('label');
+ radioButtonLabel.setAttribute('for', value);
+ radioButtonLabel.classList.add('btn', 'btn-primary');
+ radioButtonLabel.innerText = value;
+ radioButtonContainer.appendChild(radioButtonLabel);
+ radioFiltersContainer.appendChild(radioButtonContainer);
+ });
+ calendarFilterElt.appendChild(radioFiltersContainer);
+ break;
+ }
+ default: {
+ // TODO: Introduce proper error logging.
+ // eslint-disable-next-line no-console
+ console.warn(`Unsupported event filter type provided: ${filter.type}`);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handles filter element events by filter down events to the
+ * user-selected criteria.
+ *
+ * @param {Event} browserEvent - The browser event triggered on the filter
+ * form element.
+ */
+ filterEvents(browserEvent) {
+ const inputKey = browserEvent.target.name;
+ const inputValue = browserEvent.target.value;
+ const currentEventsJSON = this.getAttribute('events');
+ let events = [];
+ try {
+ events = JSON.parse(currentEventsJSON ?? '[]');
+ } catch (error) {
+ // TODO: Introduce proper error logging.
+ // eslint-disable-next-line no-console
+ console.error(`Failed to parse list of events:\n${currentEventsJSON}`);
+ }
+ events = events.filter((calEvent) => calEvent[inputKey] === inputValue);
+ this.updateEventArraySource(JSON.stringify(events));
+ }
+
attributeChangedCallback(name, oldValue, newValue) {
if (name in Calendar.observedAttributeCbs) {
this.handleObservedAttribute(
diff --git a/src/stories/calendar.stories.js b/src/stories/calendar.stories.js
index 55c1b369..6d03b2c8 100644
--- a/src/stories/calendar.stories.js
+++ b/src/stories/calendar.stories.js
@@ -9,15 +9,57 @@ export default {
'A JSON array of events conforming to the FullCalenar \
event model. See: https://fullcalendar.io/docs/event-model.',
},
+ eventFilters: {
+ control: { type: 'text' },
+ description: `A JSON object of event filters to be applied. The filters
+ should take the following form:
+ {
+ "filter_name": {
+ type: "ui_filter_type",
+ legend: "A helpful legend.",
+ key: "event_field_key",
+ values: [
+ "event_field_key_value1",
+ "event_field_key_value2"
+ ],
+ }
+ }`,
+ },
},
args: {
events: JSON.stringify([
{
- title: 'event1',
+ title: 'event @ Say Detroit Play Center',
+ start: new Date().toISOString(),
+ allDay: true,
+ location: 'Say Detroit Play Center',
+ },
+ {
+ title: 'event @ Senior Facility',
start: new Date().toISOString(),
allDay: true,
+ location: 'Senior Facility',
+ },
+ {
+ title: 'event @ Detroit Housing Commission',
+ start: new Date().toISOString(),
+ allDay: true,
+ location: 'Detroit Housing Commission',
},
]),
+ eventFilters: JSON.stringify({
+ dei_category_filter: {
+ type: 'radio',
+ legend: 'Select a location filter:',
+ key: 'location',
+ values: [
+ 'Say Detroit Play Center',
+ 'Senior Facility',
+ 'Dick & Sandy Boys and Girls Club',
+ 'Detroit Housing Commission',
+ ],
+ },
+ }),
},
};
@@ -25,6 +67,9 @@ export default {
const Template = (args) => {
const calendarElt = document.createElement('cod-calendar');
calendarElt.setAttribute('events', args.events);
+ if (args.eventFilters) {
+ calendarElt.setAttribute('event-filters', args.eventFilters);
+ }
return calendarElt;
};