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; };