diff --git a/fec/fec/static/js/modules/calendar-helpers.js b/fec/fec/static/js/modules/calendar-helpers.js index 0920fc6c36..7e443f5aed 100644 --- a/fec/fec/static/js/modules/calendar-helpers.js +++ b/fec/fec/static/js/modules/calendar-helpers.js @@ -1,10 +1,160 @@ +/* eslint-disable */ /** * */ import moment from 'moment'; import { default as URI } from 'urijs'; +function dateString(dateToParse, format, adjustmentHours) { + const parsed = new Date(Date.parse(dateToParse)); + + if (adjustmentHours) parsed.setHours(parsed.getHours() + adjustmentHours); + + let YYYY = parsed.getUTCFullYear(); + let MM = (parsed.getUTCMonth() + 1).toString().padStart(2, '0'); // Months are 0-indexed + let DD = (parsed.getUTCDate()).toString().padStart(2, '0'); + let hh = (parsed.getUTCHours()).toString().padStart(2, '0'); + let mm = (parsed.getUTCMinutes()).toString().padStart(2, '0'); + let ss = (parsed.getUTCSeconds()).toString().padStart(2, '0'); + + if (format == 'YYYYMMDD') + return `${YYYY}${MM}${DD}`; + + else if (format == 'YYYYMMDD[T]HHmmss') + return `${YYYY}${MM}${DD}T${hh}${mm}${ss}`; + + return dateToParse; +} + +/** + * @enum {Object} - Returns an object with settings for various datetime formats. + * For dates, use .toLocaleDateString('en-US', {}), for time use .toLocaleTimeString('en-US', {}); + * Converts all dates to U.S. Eastern (i.e. 'America/New_York') + * For ISO dates (i.e. 1970-01-01), use only .toISOString(); + * @see https://moment.github.io/luxon/#/formatting?id=presets + */ +export const dateTimeFormatOptions = { + /** @example '1/1/1970' (This is the default for en-US) */ + DATE_SHORT: {}, + + /** @example '01/01/1970' */ + DATE_SHORT_01: { month: '2-digit', day: '2-digit', year: 'numeric' }, + + /** @example 'January 1, 1970' */ + DATE_FULL: { month: 'long', day: 'numeric', year: 'numeric' }, + + /** @example 'January 01, 1970' */ + DATE_FULL_01: { month: 'long', day: '2-digit', year: 'numeric' }, + + /** @example 'January 1 */ + DATE_FULL_MONTH_DAY: { month: 'long', day: '2-digit' }, + + /** @example 'Wednesday' */ + WEEKDAY_FULL: { weekday: 'long' }, + + /** @example '1:30 PM' */ + TIME_SIMPLE: { hour: "numeric", minute: "2-digit" }, + + /** @example '13:30' */ + TIME_24_SIMPLE: {}, +}; + +/** + * @param {Object} event - {start: '', end: '', title: '', summary: ''} + * @param {string} event.start + * @param {string} [event.end] + * @param {string} [event.title] + * @param {string} [event.summary] + * @returns {string} + */ export function getGoogleUrl(event) { + console.log('getGoogleUrl(event): ', event); + let datesString; + + if (event.end) { + datesString = `${convertDateForGoogle(event.start, true)}/${convertDateForGoogle(event.end, true)}`; + + } else { + const startDateObj = new Date(event.start); + startDateObj.setHours(startDateObj.getHours() + 24); + const fakeEndDate = startDateObj.toISOString(); + const startEndComboString = `${convertDateForGoogle(event.start, false).substring(0,10)}/${convertDateForGoogle(fakeEndDate, false)}`; + datesString = startEndComboString; + } + + const toReturn = + URI('https://calendar.google.com/calendar/render') + .addQuery({ + action: 'TEMPLATE', + text: event.title, + details: event.summary, + dates: datesString + }) + .toString(); + + console.log(' toReturn: ', toReturn); + + return toReturn; +} + +export function getMs365Url(event) { + console.log('getMs365Url(event): ', event); + let datesString; + + if (event.end) { + datesString = `${convertDateForGoogle(event.start, true)}/${convertDateForGoogle(event.end, true)}`; + + } else { + const startDateObj = new Date(event.start); + startDateObj.setHours(startDateObj.getHours() + 24); + const fakeEndDate = startDateObj.toISOString(); + const startEndComboString = `${convertDateForGoogle(event.start, false).substring(0,10)}/${convertDateForGoogle(fakeEndDate, false)}`; + datesString = startEndComboString; + } + + const toReturn = + URI('https://outlook.office.com/calendar/0/deeplink/compose') + .addQuery({ + allday: true, + body: event.summary, + enddt: '2025-01-31', + location: '', + // path: '', + // rru: '', + startdt: '2025-01-30', + subject: event.title + // action: 'TEMPLATE', + // text: event.title, + // details: event.summary, + // dates: datesString + }) + .toString(); + console.log(' toReturn: ', toReturn); + + return toReturn; +} + +/** + * Takes a Date object, removes the dashes and colon + * @param {string} input -sdf + * @param {boolean} [includeTime=true] - Whether the returned string should be 200012311945 or 20001231 + * @returns {string} + */ +function convertDateForGoogle(input, includeTime = true) { + // const passedDate = new Date(input); + const toReturn = input.replaceAll(/[-:.]/g, ''); + return includeTime ? toReturn : toReturn.substring(0, 7); +} + +/** + * @param {Object} event - {start: '', end: '', title: '', summary: ''} + * @param {Moment} [event.start] + * @param {Moment} [event.end] + * @param {string} [event.title] + * @param {string} [event.summary] + * @returns {string} + */ +export function getGoogleUrl_moment(event) { let fmt, dates; if (event.end) { fmt = 'YYYYMMDD[T]HHmmss'; @@ -29,7 +179,14 @@ export function getGoogleUrl(event) { .toString(); } -export function calendarDownload(path, params) { +/** + * @param {string} path + * @param {Object} params - key-value pairs on an Object + * @returns {string} a string like '/v1/path/?param1key=param1val¶m2key=param2val' + */ +// export function calendarDownload(path, params) { +export function getDownloadUrl(path, params) { + console.log('getDownloadUrl(path, params): ', path, params); const url = URI(window.API_LOCATION) .path(Array.prototype.concat(window.API_VERSION, path || [], '').join('/')) .addQuery({ @@ -43,6 +200,13 @@ export function calendarDownload(path, params) { return URI.decode(url); } +/** + * Builds a URL from the parameters provided plus API_LOCATION and API_VERSION + * @param {string[]} path - An array of strings to be combined to build the path. ex: ['data', 'path'] will become 'data/path' + * @param {Object} params - Key/value pairs to add after the ? in the full URL + * @param {string} [type] - Adds the public key or, if 'sub', uses the calendar public key + * @returns {string} + */ export function getUrl(path, params, type) { //if 'type' arg is present and set to 'sub', use API_KEY_PUBLIC_CALENDAR as api_key, otherwise use API_KEY_PUBLIC; const apiKey = @@ -57,6 +221,12 @@ export function getUrl(path, params, type) { .toString(); return URI.decode(url); } + +/** + * Returns a class name for multi-date events, i.e. those with valid but different start day and end day + * @param {Object} event - Event object, looking at start_date and end_date + * @returns {string} Returns a class name for multi-day events, else '' + */ export function className(event) { const start = event.start_date ? moment(event.start_date).format('M D') : null; const end = event.end_date ? moment(event.end_date).format('M D') : null; @@ -67,6 +237,11 @@ export function className(event) { } } +/** + * Does event.start_date have an hour (vs only yyyy-mm-dd or null) + * @param {Object} event + * @returns {boolean} true if event.start_date has a legitimate hour included, otherwise false + */ export function checkStartTime(event) { if (event.start_date) { return moment(event.start_date).hour() ? true : false; @@ -75,8 +250,12 @@ export function checkStartTime(event) { } } +/** + * Matches the category parameter from calendar date API + * @param {string} category + * @returns {string} String for the tooltip content for category + */ export function mapCategoryDescription(category) { - // matches the category parameter from calendar date API const tooltipContent = { 'Reporting Deadlines': 'Throughout the year, filers submit regularly scheduled reports about their campaign finance activity. These reporting requirements are outlined in Title 11 of the Code of Federal Regulations (CFR) and vary, depending on the type of filer.', diff --git a/fec/fec/static/js/modules/calendar-list-view.js b/fec/fec/static/js/modules/calendar-list-view.js index 56b33630ef..17dc2da6e3 100644 --- a/fec/fec/static/js/modules/calendar-list-view.js +++ b/fec/fec/static/js/modules/calendar-list-view.js @@ -1,26 +1,31 @@ +/* eslint-disable */ // TODO: remove eslint-disable /** - * + * @fileoverview Plugin for fullcalendar.io being used by ./calendar.js + * @license CC0-1.0 + * @owner fec.gov + * @version 2.0 */ -import $ from 'jquery'; -import { default as moment } from 'moment'; -import { default as _chain } from 'underscore/modules/chain.js'; -import { default as _each } from 'underscore/modules/each.js'; -import { default as _pairs } from 'underscore/modules/pairs.js'; -import { default as _reduce } from 'underscore/modules/reduce.js'; -import 'fullcalendar'; -import Dropdown from './dropdowns.js'; -import { default as eventTemplate } from '../templates/calendar/events.hbs'; +import { createPlugin, sliceEvents } from '@fullcalendar/core'; + +import { dateTimeFormatOptions, getDownloadUrl, getGoogleUrl, getMs365Url } from '../modules/calendar-helpers.js'; -const FC = $.fullCalendar; -const View = FC.View; +import Dropdown from './dropdowns.js'; +import { default as template_events } from '../templates/calendar/events.hbs'; +import { default as template_listToggles } from '../templates/calendar/listToggles.hbs'; // 'Sort by: Category' view // Property name is the category // Then followed by a list of the types of events under that category // List items are the first token of the event category parameter from the API // example: 'ie' for 'IE Periods' -const categories = { + +/** */ +const customClassName = 'fec-list-view'; +const LIST_SORT = ['monthTime', 'monthCategory']; + +/** @enum {string[]} */ +const categories_lookup = { Elections: ['election'], 'Filing deadlines': ['reporting', 'pre'], 'Reporting and compliance periods': ['ie', 'ec', 'fea'], @@ -30,74 +35,317 @@ const categories = { Other: ['other'] }; -const categoriesInverse = _reduce( - _pairs(categories), - function(memo, pair) { - const key = pair[0]; - const values = pair[1]; - _each(values, function(value) { - memo[value] = key; - }); - return memo; - }, - {} -); +/** + * Similar to categories_lookup but flipped so every value there is a key here. + * e.g. {Meetings: ['open', 'executive']} in categories_lookup would be {open: 'Meetings', executive: 'Meetings']} here + * @enum {string[]} + */ +const categories_reverseLookup = Object.getOwnPropertyNames(categories_lookup).reduce((accumulator, key) => { + categories_lookup[key].forEach(val => { + accumulator[val] = key; + }) + return accumulator; +}, {}); +/** + * Group the events by category,
+ * otherwise events with no recognized category will be group at the top as Unknown. + * @param {Object[]} events - Array of Objects with event details + * @param {string} [start] - ISO-formatted date string + * @param {string} [end] - ISO-formatted date string + * @returns {Object[]} An array of objects groups of events like [{title: 'Filing deadlines', datetime_string: '', events: [{}]}, …] + */ const categoryGroups = function(events, start, end) { - return _chain(events) - .filter(function(event) { - return start <= event.start && event.start < end; - }) - .sortBy('start') - .groupBy(function(event) { - const category = event.category - ? event.category.split(/[ -]+/)[0].toLowerCase() - : null; - return categoriesInverse[category]; - }) - .map(function(values, key) { - return { - title: key, - events: values - }; - }) - .sortBy(function(group) { - return Object.keys(categories).indexOf(group.title); - }) - .value(); + const filteredEvents = [...events];//.filter(event => (event.range.start && event.range.start < end)); + + filteredEvents.forEach(event => { + // When does the event start? + const eventStartDate = new Date(event.range.start); + // Adjust for the time offset // TODO: this may need to be done elsewhere, too + eventStartDate.setMinutes(eventStartDate.getMinutes() + eventStartDate.getTimezoneOffset()); + event.groupByLabel = categories_reverseLookup[event.def.extendedProps.category]; + event.datetime = eventStartDate.toISOString().substring(0,10); // used inside + }); + + // Sort them by date + filteredEvents.sort((a, b) => { + if (a.range.start == b.range.start) return 0; + else return a.range.start < b.range.start ? -1 : 1; + }); + + const toReturn = []; + let nextGroup = {title: '', datetime_string: '', events: []}; + // For each pretty category name + Object.getOwnPropertyNames(categories_lookup).forEach(key => { + // The events with this pretty category name + const eventsInThisCat = filteredEvents.filter(event => { + const lowerFirstWordOfCategory = event.def.extendedProps.category.split(/[ -]+/)[0].toLowerCase(); + return key == categories_reverseLookup[lowerFirstWordOfCategory]; + }); + + // To account for any events that aren't in designated categories, + // let's remove each of these chosen events from filteredEvents + eventsInThisCat.forEach(event => { + filteredEvents.splice(filteredEvents.indexOf(event), 1); + }); + + // If there are events, create a new group + if (eventsInThisCat.length > 0) { + nextGroup = {title: key, datetime_string: '', events: [...eventsInThisCat]}; + toReturn.push(nextGroup); + } + }); + + // If there are filteredEvents left, they didn't get a category, so… + if (filteredEvents.length > 0) { + nextGroup = {title: 'Unknown category', datetime_string: '', events: [...filteredEvents]}; + // …let's put them at the top of the list + toReturn.unshift(nextGroup); + } + + return toReturn; }; +/** + * Group the events by the formatted value of their start dates, + * @param {Object[]} events - Array of Objects with event details + * @param {string} [start] - ISO-formatted date string + * @param {string} [end] - ISO-formatted date string + * @returns {Object[]} An array of objects groups of events like [{title: 'January 1, 1970', datetime_string: '1970-01-01', events: [{}]}, …] + */ const chronologicalGroups = function(events, start, end) { - events = _chain(events) - .filter(function(event) { - return start <= event.start && event.start < end; - }) - .map(function(value) { - // Group the events by the formatted value of their start dates, - // otherwise events with a time on their date will be grouped separately - // from those that just have a date - value.groupByValue = value.start.format('MMMM D, YYYY'); - return value; - }) - .sortBy('start') - .groupBy('groupByValue') - .map(function(values, key) { - return { - title: moment.utc(new Date(key)).format('MMMM D, YYYY'), - events: values - }; - }) - .value(); - return events; + const filteredEvents = [...events];//events.filter(event => (event.range.start && event.range.start < end)); + + // Go through every event and add its eventStartTime, the string like January 1, 1970 + filteredEvents.forEach(event => { + console.log('chronologicalGroups.forEach()'); + console.log(' event: ', event); + const thisEventObj = calendar.getEventById(event.def.publicId); + console.log(' thisEventObj: ', thisEventObj); + console.log(' .id: ', thisEventObj.id); + console.log(' .start: ', thisEventObj.start); + console.log(' .end: ', thisEventObj.end); + console.log(' .startStr: ', thisEventObj.startStr); + console.log(' .endStr: ', thisEventObj.endStr); + console.log(' .extendedProps: ', thisEventObj.extendedProps); + + // When does the event start? + const eventStartDate = new Date(event.range.start); + console.log(' eventStartDate: ', eventStartDate); + console.log(' typeof, start, eventStartDate: ', typeof thisEventObj.start, typeof eventStartDate); + console.log(' typeof, start, eventStartDate: ', Date.parse(thisEventObj.start), Date.parse(eventStartDate)); + // Adjust for the time offset // TODO: this may need to be done elsewhere, too + eventStartDate.setMinutes(eventStartDate.getMinutes() + eventStartDate.getTimezoneOffset()); + event.groupByLabel = eventStartDate.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL); + event.datetime = eventStartDate.toISOString().substring(0,10); // used inside + }); + + filteredEvents.sort((a, b) => { + if (a.range.start == b.range.start) return 0; + else return a.range.start < b.range.start ? -1 : 1; + }); + + const toReturn = []; + let prevGroupTitle = ''; + let nextGroup = {title: '', datetime_string: '', events: []}; + filteredEvents.forEach(event => { + // If the label has changed, we'll need to start a new group + if (event.groupByLabel != prevGroupTitle) { + // First, close out the previous group if it's not empty + if (nextGroup.events.length > 0) toReturn.push(nextGroup); + // Save this label + prevGroupTitle = event.groupByLabel; + // Start a new group + nextGroup = {title: event.groupByLabel, datetime_string: event.datetime, events: []}; + } + nextGroup.events.push(event); + }); + toReturn.push(nextGroup); + + return toReturn; +}; + +/** + * + * @param {*} view + */ +const manageListToggles = function(listEventOrder) { + const listTogglesSelector = '.cal-list__toggles'; + + const headerToolbarCenter = document.querySelector('.fc-header-toolbar .fc-toolbar-center'); + + let listToggles = document.querySelector('.cal-list__toggles'); + + // If there's no listToggles, create it + if (!listToggles) { + listToggles = document.createElement('div'); + listToggles.classList.add(`${listTogglesSelector.substring(1)}`); // drop the period from the selector + headerToolbarCenter.appendChild(listToggles); + } + + listToggles.innerHTML = template_listToggles({listEventOrder: listEventOrder}); + + // Activate the list buttons + listToggles.querySelectorAll('.js-toggle-list-sort').forEach(button => { + button.addEventListener('click', e => { + // If we aren't already sorting like this + if (!e.target.classList.contains('is-active')) { + calendar.view.custom.listEventOrder = e.target.dataset.triggerSort; + calendar.render(); + } + }); + }); + + if (!listEventOrder) listToggles.setAttribute('aria-hidden', true); + else listToggles.removeAttribute('aria-hidden'); + + // Highlight the "List" button on monthTime + // if (view.name === 'monthCategory') { + // this.$calendar.find('.fc-monthTime-button').addClass('fc-state-active'); + // } }; -const ListView = View.extend({ - setDate: function(date) { - const intervalUnit = this.options.duration.intervalUnit || this.intervalUnit; - View.prototype.setDate.call(this, date.startOf(intervalUnit)); +/** + * Called from inside `content` after an animation frame. + * `didMount` fires first, which doesn't do any good here. + * `content` fires next but only returns the html for every calendar entry and the view and + * doesn't actually create the elements. + * TODO: as fullcalendar adds it for custom views, I'd like to move this to something like eventDidMount + */ +const initDropdowns = function() { + const dropdownsToInit = document.querySelectorAll(`.${customClassName} .dropdown`); + dropdownsToInit.forEach(el => { + new Dropdown($(el), { checkboxes: false }); + }); +}; + +const fecListViewConfig = { + classNames: [customClassName], + + /** + * Called when this view is created + * @param {Object} d + * @param {Object} d.dateProfile + * @param {HTMLElement} d.el + * @param {Object} d.eventStore + * @param {Object} d.eventUiBases + * @param {Function} [callback] + */ + didMount: function(props, callback) { + calendar.el.dataset.fecView = 'list'; }, + willUnmount: function(e, e1) { + manageListToggles(false); + calendar.el.dataset.fecView = 'grid'; + }, + + eventDidMount: function(e) { + console.log('CustomViewConfig.eventDidMount(e): ', e); + }, + + /** + * @param {Object} d + * @param {(Function)} callback + * @returns {Object} {html: ''} + */ + content: function(props) { + console.log('CustomViewConfig.content(props): ', props); + + const segs = sliceEvents(props, false); + console.log(' segs: ', segs); + + if (!calendar.view.custom) calendar.view.custom = {}; + if (!calendar.view.custom.settings) calendar.view.custom.settings = {}; + if (!calendar.view.custom.listEventOrder) calendar.view.custom.listEventOrder = 'monthTime'; + + if (LIST_SORT.includes(calendar.view.custom.listEventOrder)) + manageListToggles(calendar.view.custom.listEventOrder); + + const groups = calendar.view.custom.listEventOrder == 'monthCategory' + ? categoryGroups( + segs, + props.dateProfile.currentRange.start, + props.dateProfile.currentRange.end) + : chronologicalGroups( + segs, + props.dateProfile.currentRange.start, + props.dateProfile.currentRange.end); + + // Let's convert these events to something the template can use + const groupsForTemplate = []; + groups.forEach(group => { + const newGroup = {title: group.title, datetime_string: group.datetime_string, events: []}; + // + group.events.forEach(event => { + // Start with a new event, based on the incoming extendedProps + console.log(' customview.forEach(event): ', event); + const newEvent = Object.assign({},event.def.extendedProps); + // Get the start and end Date objects (GMT) + let start_date = new Date(event.range.start); + let end_date = event.def.hasEnd ? new Date(event.range.end) : false; + newEvent.start_google = 'YAY-start'; + newEvent.end_google = 'YAY-end'; + + start_date.setMinutes(start_date.getMinutes() + start_date.getTimezoneOffset()); + newEvent.start_iso = start_date.toISOString(); + newEvent.start_day = start_date.toLocaleDateString('en-US', dateTimeFormatOptions.WEEKDAY_FULL); + newEvent.start_date_string = start_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL); + if (newEvent.hasStartTime) + newEvent.start_time_string = start_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE) + .toLowerCase().replace(' ', ''); + + if (end_date) { + end_date.setMinutes(end_date.getMinutes() + end_date.getTimezoneOffset()); + newEvent.end_iso = end_date.toISOString(); + newEvent.end_day = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.WEEKDAY_FULL); + newEvent.end_date_string = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL); + if (newEvent.hasStartTime) + newEvent.end_time_string = end_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE) + .toLowerCase().replace(' ', ''); + } + newEvent.download = getDownloadUrl(newEvent); + newEvent.google = getGoogleUrl({ + start: new Date(event.range.start).toISOString(), + end: newEvent.hasStartTime && event.range.end ? new Date(event.range.end).toISOString() : false, + title: newEvent.title, + summary: newEvent.description || newEvent.title + }); + newEvent.ms365 = getMs365Url({ + start: new Date(event.range.start).toISOString(), + end: newEvent.hasStartTime && event.range.end ? new Date(event.range.end).toISOString() : false, + title: newEvent.title, + summary: newEvent.description || newEvent.title + }); + + newGroup.events.push(newEvent); + }); + groupsForTemplate.push(newGroup); + }); + + window.requestAnimationFrame(initDropdowns.bind(this)); + + return { html: template_events({ + groups: groupsForTemplate, settings: {listEventOrder: calendar.view.custom.listEventOrder} + }) }; + + // TRYING TO ADD MOUSE EVENT LISTENERS TO DROPDOWNS + }, + // dayHeaderContent: function(d, two) {}, // Doesn't do anything here + // render: function(d) {} // Not valid here + + + // Deprecated + // setDate: function(date) { + // console.log('CustomViewConfig.setDate(props): ', props); + // const intervalUnit = this.options.duration.intervalUnit || this.intervalUnit; + // ViewRoot.prototype.setDate.call(this, date.startOf(intervalUnit)); + // }, + + // Deprecated + /* renderEvents: function(events) { + console.log('CustomViewConfig.renderEvents(events): ', events); const groups = this.options.categories ? categoryGroups(events, this.start, this.end) : chronologicalGroups(events, this.start, this.end); @@ -106,20 +354,33 @@ const ListView = View.extend({ sortBy: this.options.sortBy }; - this.el.html(eventTemplate({ groups: groups, settings: settings })); + this.el.html(template_events({ groups: groups, settings: settings })); this.dropdowns = $(this.el.html) .find('.dropdown') .map(function(idx, elm) { return new Dropdown($(elm), { checkboxes: false }); }); - }, + },*/ + // Deprecated + /* unrenderEvents: function() { + console.log('CustomViewConfig.unrenderEvents(props): ', props); this.dropdowns.each(function(idx, dropdown) { dropdown.destroy(); }); this.el.html(''); + }*/ +}; + + + + + + + +export default createPlugin({ + views: { + fecList: fecListViewConfig } }); - -FC.views.list = ListView; diff --git a/fec/fec/static/js/modules/calendar-tooltip.js b/fec/fec/static/js/modules/calendar-tooltip.js index 3b087e968a..634cacb63c 100644 --- a/fec/fec/static/js/modules/calendar-tooltip.js +++ b/fec/fec/static/js/modules/calendar-tooltip.js @@ -2,38 +2,94 @@ * */ import Dropdown from './dropdowns.js'; -import Listeners from './listeners.js'; - -export function CalendarTooltip(content, $container) { - this.$content = $(content); - this.$container = $container; - this.$close = this.$content.find('.js-close'); - this.$dropdown = this.$content.find('.dropdown'); - this.exportDropdown = new Dropdown(this.$dropdown, { - checkboxes: false - }); - - this.events = new Listeners(); - this.events.on(this.$close, 'click', this.close.bind(this)); - this.events.on($(document.body), 'click', this.handleClickAway.bind(this)); - - this.$container.addClass('is-active'); -} +// import Listeners from './listeners.js'; + +/** + * Activates a new .tooltip element. Doesn't create it, but + * - activates listeners + * - activates the Dropdown + * - destroys the element on click-outside + * - handles adding listeners to and then destroying calendar events' tooltips/details pop-up + * (doesn't create the element but will remove it) + */ +export class CalendarTooltip { + /** + * @constructor + * @param {HTMLElement} el - The HTMLElement that is the tooltip, i.e. the .tooltip element + */ + constructor(el) { + CalendarTooltip.instances.push(this); + + el.previousElementSibling.classList.add('visible-tooltip'); -CalendarTooltip.prototype.handleClickAway = function(e) { - const $target = $(e.target); - if ( - !this.$content.has($target).length && - !this.$container.has($target).length - ) { - this.close(); + this.dropdown = new Dropdown(el.querySelector('.dropdown'), { checkboxes: false }); + + // Catch the click on its way in, which happens before the tooltip is created, + // otherwise we get into + // # 1) click capture phase + // # 2) create tooltip and add listeners + // # 3) click bubbling is caught by tooltip listeners + // # 4) tooltip is removed + window.addEventListener('click', CalendarTooltip.handleExternalClicks, { capture: true }); + + el.querySelector('.js-close').addEventListener('click', CalendarTooltip.handleCloseClick); } -}; - -CalendarTooltip.prototype.close = function() { - this.$content.remove(); - this.exportDropdown.destroy(); - this.$container.removeClass('is-active'); - this.$container.focus(); // TODO: jQuery deprecation - this.events.clear(); -}; + + // PUBLIC METHODS + + // GETTERS + + // SETTERS + + // METHODS + + // PUBLIC PROPERTIES + + // STATIC + static instances = []; + + /** + * @param {PointerEvent} e + */ + static handleCloseClick = function(e) { + e.stopImmediatePropagation(); + e.preventDefault(); + CalendarTooltip.destroyAll(); + }; + + /** + * @param {PointerEvent} e + */ + static handleExternalClicks = function(e) { + // If the target isn't inside a .tooltip, close this tooltip and destroy everything + if (!e.target.closest('.tooltip')) { + CalendarTooltip.destroyAll(); + } + }; + + static destroyAll = function() { + CalendarTooltip.instances.forEach(instance => { + instance.#selfDestruct(); + }); + CalendarTooltip.instances = []; + let calendarEntriesWithTooltips = document.querySelectorAll('.visible-tooltip'); + calendarEntriesWithTooltips.forEach(el => { + el.classList.remove('visible-tooltip'); + }); + calendarEntriesWithTooltips = []; + }; + + #selfDestruct = function() { + window.removeEventListener('click', CalendarTooltip.handleExternalClicks, true); + document.querySelector('.tooltip .js-close').removeEventListener('click', CalendarTooltip.handleCloseClick); + + let el = document.querySelector('#calendar-details'); + el.removeEventListener('mouseleave', CalendarTooltip.handleMouseLeave); + + this.dropdown.destroy(); + this.dropdown = null; + el.remove(); + el = null; + delete this; + }; +} diff --git a/fec/fec/static/js/modules/calendar.js b/fec/fec/static/js/modules/calendar.js index bdda2ea1cb..f49bd2900c 100644 --- a/fec/fec/static/js/modules/calendar.js +++ b/fec/fec/static/js/modules/calendar.js @@ -1,216 +1,358 @@ +/* eslint-disable */ // TODO: remove eslint-disable /** - * + * @fileoverview Using fullcalendar.io, builds and runs the various views for the fec.gov calendar pages + * @license CC0-1.0 + * @owner fec.gov + * @version 2.0 */ -import { default as Handlebars } from 'hbsfy/runtime.js'; + +import bootstrap5Plugin from '@fullcalendar/bootstrap5'; // Being included because fullcalendar requires a themesystem and the default is too bossy +import { Calendar as FullCalendar } from '@fullcalendar/core'; +import dayGridPlugin from '@fullcalendar/daygrid'; +// import { default as Handlebars } from 'hbsfy/runtime.js'; import $ from 'jquery'; -import moment from 'moment'; -import { default as _extend } from 'underscore/modules/extend.js'; import { default as _isEqual } from 'underscore/modules/isEqual.js'; import { default as URI } from 'urijs'; - -import 'fullcalendar'; - -import { checkStartTime, className, getGoogleUrl, mapCategoryDescription } from './calendar-helpers.js'; +import { checkStartTime, dateTimeFormatOptions, getGoogleUrl, getDownloadUrl, getMs365Url, mapCategoryDescription } from './calendar-helpers.js'; +import { default as FecListViewPlugin } from './calendar-list-view.js'; import { CalendarTooltip } from './calendar-tooltip.js'; import Dropdown from './dropdowns.js'; -import { LOADING_DELAY, SUCCESS_DELAY, datetime, eq, isLargeScreen, toUpperCase } from './helpers.js'; +import { LOADING_DELAY, SUCCESS_DELAY, isLargeScreen } from './helpers.js'; import { pushQuery, updateQuery } from './urls.js'; -import './calendar-list-view.js'; import { default as template_details } from '../templates/calendar/details.hbs'; import { default as template_download } from '../templates/calendar/download.hbs'; -import { default as template_listToggles } from '../templates/calendar/listToggles.hbs'; import { default as template_subscribe } from '../templates/calendar/subscribe.hbs'; -// TODO: do we need to registerHelper? -Handlebars.registerHelper('eq', eq); -Handlebars.registerHelper('datetime', datetime); -Handlebars.registerHelper('toUpperCase', toUpperCase); - +/** @enum {html} */ const templates = { details: template_details, download: template_download, subscribe: template_subscribe, - listToggles: template_listToggles }; -const LIST_VIEWS = ['monthTime', 'monthCategory']; - -const FC = $.fullCalendar; -const Grid = FC.Grid; +// Moved to the plugin +// const FEC_LIST_SORT = ['monthTime', 'monthCategory']; // Globally override event sorting to order all-day events last // TODO: Convince fullcalendar.io support this behavior without monkey-patching -Grid.prototype.compareEventSegs = function(seg1, seg2) { - return ( - seg1.event.allDay - seg2.event.allDay || // put all-day events last (booleans cast to 0/1) - seg1.eventStartMS - seg2.eventStartMS || // tie? earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - FC.compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs) - ); -}; +// Grid.prototype.compareEventSegs = function(seg1, seg2) { +// return ( +// seg1.event.allDay - seg2.event.allDay || // put all-day events last (booleans cast to 0/1) +// seg1.eventStartMS - seg2.eventStartMS || // tie? earlier events go first +// seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first +// FC.compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs) +// ); +// }; export default function Calendar(opts) { - this.opts = $.extend({}, this.defaultOpts(), opts); - - this.$calendar = $(this.opts.selector); - this.$head = $('.data-container__head'); - this.$calendar.fullCalendar(this.opts.calendarOpts); - this.url = URI(this.opts.url); - this.subscribeUrl = URI(this.opts.subscribeUrl); - this.exportUrl = URI(this.opts.exportUrl); + console.log('new Calendar(opts): ', opts); + this.opts = Object.assign({}, this.defaultOpts(), opts); + + this.calendarEl = document.querySelector(this.opts.selector); + this.headEl = document.querySelector('.data-container__head'); + this.calendar = new FullCalendar(this.calendarEl, this.opts.calendarOpts); + window.calendar = this.calendar; + this.url_api = URI(this.opts.url_api); + this.url_subscribe = URI(this.opts.url_subscribe); + console.log('this.url_subscribe: ', this.url_subscribe); + this.url_export = URI(this.opts.url_export); this.filterPanel = this.opts.filterPanel; this.filterSet = this.filterPanel.filterSet; + this.tagList = this.opts.tagList; this.popoverId = 'calendar-popover'; this.detailsId = 'calendar-details'; - this.sources = null; + this.eventSources = null; this.params = null; - this.$download = $(opts.download); - this.$subscribe = $(opts.subscribe); + // this.$download = $(opts.selector_download); + this.downloadEl = document.querySelector(opts.selector_download); + // this.$subscribe = $(opts.selector_subscribe); + this.subscribeEl = document.querySelector(opts.selector_subscribe); + + // this.$calendar.on('click', '.js-toggle-view', this.toggleListView.bind(this)); + const toggleView = this.calendarEl.querySelector('.js-toggle-view'); + // toggleView.addEventListener('click', this.toggleListView.bind(this)); + + // this.$calendar.on( + // 'keypress', + // '.fc-event, .fc-more, .fc-close', + // this.simulateClick.bind(this) + // ); + const keypressElements = document.querySelectorAll('.fc-event, .fc-more, .fc-close'); + keypressElements.forEach(el => { + el.addEventListener('keypress', this.simulateClick.bind(this)); + }); - this.$calendar.on('click', '.js-toggle-view', this.toggleListView.bind(this)); + // this.$calendar.on('click', '.fc-more', this.managePopoverControl.bind(this)); + // this.calendarEl.querySelector('.fc-more').addEventListener('click', this.managePopoverControl.bind(this)); - this.$calendar.on( - 'keypress', - '.fc-event, .fc-more, .fc-close', - this.simulateClick.bind(this) - ); - this.$calendar.on('click', '.fc-more', this.managePopoverControl.bind(this)); + this.buildTagsForInitialFilters(); + + // if (queryParams.search) + // - this.filterPanel.$form.on('change', this.filter.bind(this)); - $(window).on('popstate', this.filter.bind(this)); + this.filterPanel.$form.on('change', this.handleFilterChange.bind(this)); + // this.filterPanel.$form.addEventListener('change', this.handleFilterChange.bind(this)); + $(window).on('popstate', this.handleFilterChange.bind(this)); + // window.addEventListener('popstate', this.handleFilterChange.bind(this)); updateQuery(this.filterSet.serialize(), this.filterSet.fields); - this.filter(); - this.styleButtons(); + this.handleFilterChange(); if (!isLargeScreen()) { - this.$head.after($('#filters')); + // this.$head.after($('#filters')); + this.headEl.insertAdjacentElement('afterend', document.querySelector('#filters')); } + + // Make it exist + // this.calendar.render(); + + this.styleButtons(); } Calendar.prototype.toggleListView = function(e) { - const newView = $(e.target).data('trigger-view'); - this.$calendar.fullCalendar('changeView', newView); + // console.log('Calendar.toggleListView(e): ', e); + // const newView = $(e.target).data('trigger-view'); + const newView = e.target.dataset.triggerView; + // this.$calendar.fullCalendar('changeView', newView); + this.calendar.changeView(newView); }; +/** + * TODO: TEST HOW DATES DISPLAY FOR COMPUTERS IN OTHER TIME ZONES + */ Calendar.prototype.defaultOpts = function() { return { calendarOpts: { - header: { + timeZone: 'America/New_York', // or 'UTC'? 'GMT'? + headerToolbar: { left: 'prev,next,today', - center: '', - right: 'monthTime,month' + center: '', // this is where we'll put the list toggles + right: 'fecList,dayGridMonth', // fecList is custom, dayGridMonth comes from @fullcalendar/daygrid }, - buttonIcons: false, - buttonText: { + buttonIcons: false, // The icons fc should put on buttons + buttonText: { // The text labels for buttons today: 'This Month', - week: 'Week' + // next: '', + // prev: '' + // week: 'Week YAY' }, - contentHeight: 'auto', - dayRender: this.handleDayRender.bind(this), - dayPopoverFormat: 'MMMM D, YYYY', - defaultView: 'monthTime', - eventRender: this.handleEventRender.bind(this), - eventAfterAllRender: this.handleRender.bind(this), - eventClick: this.handleEventClick.bind(this), - eventLimit: true, + // contentHeight: 'auto', + // dayRender: this.handleDayRender.bind(this), // Not valid here + // dayPopoverFormat: 'MMMM D, YYYY', + initialView: 'fecList',//'listMonth' + themeSystem: 'bootstrap5', // of the three choices, bootstrap5 is the lowest impact + // eventRender: this.handleEventRender.bind(this), // Not valid here + // eventAfterAllRender: this.handleRender.bind(this), // Not valid here + // eventClick: this.handleEventClick.bind(this), // TODO: What does this do? + // success: this.success.bind(this), // Not valid here + // failure: this.failure.bind(this), // Not valid here + datesSet: this.handleCalendarDatesSet.bind(this), // TODO: Is this doing anything here? + // eventLimit: true, // Not valid here nowIndicator: true, + plugins: [dayGridPlugin, /*timeGridPlugin, listPlugin,*/ bootstrap5Plugin, FecListViewPlugin], views: { - agenda: { - scrollTime: '09:00:00', - minTime: '08:00:00', - maxTime: '20:00:00' - }, + // dayGridMonth: {}, + // timeGridWeek: {}, + // dayGrid: {}, + // timeGrid: {}, + // week: {}, + // day: {} + // agenda: { + // scrollTime: '09:00:00', + // minTime: '08:00:00', + // maxTime: '20:00:00' + // }, month: { - eventLimit: 3, - buttonText: 'Grid' - }, - monthCategory: { - type: 'list', - categories: true, - sortBy: 'category', - duration: { months: 1, intervalUnit: 'month' } + type: 'grid', + dayMaxEvents: 3, + buttonText: 'Grid', + editable: false, + eventClassNames: function(info) { return ['FEC-EVENT']}, + duration: { months: 1, intervalUnit: 'month' }, + // render: function(arg) {}, // Not valid here + // onRender: function(arg) {}, // Not valid here + // noEventsDidMount: function(info) {}, // Not valid here + // eventContent: function(info) { + // If this exists, it's called instead of Calendar.eventContent() + // If this doesn't return anything, events will be put onto the page but will have aria-hidden="true" + // return {html: ''}; + // }, + // dayHeaderContent: function(d) { + // The row of "Sun", "Mon", etc + // console.log('views.month.dayHeaderContent(d): ', d); + // If this exists, it's called instead of Calendar.dayHeaderContent() + // If this doesn't return anything, day headers will be put onto the page but will have aria-hidden="true" + // return { html: `
${d.text}
` }; + // }, + // dayHeaderDidMount: function(d) { + // console.log('views.month.dayHeaderDidMount(d): ', d); + // // If this exists, it's called instead of Calendar.dayHeaderContent() + // // If this doesn't return anything, day headers will be put onto the page but will have aria-hidden="true" + // return d; + // }, + viewDidMount: function(arg) { + console.log('month.view.viewDidMount(arg): ', arg); + // For some reason, the dayHeader (Sun, Mon, etc) and dateHeader (1, 2, 3) are getting aria-hidden + const hiddenElementsToShow = document.querySelectorAll( + `.fc-daygrid-header .fc-cell-inner[aria-hidden="true"], .fc-daygrid-day-number[aria-hidden="true"]` + ); + hiddenElementsToShow.forEach(el => {el.removeAttribute('aria-hidden')}); + }, + viewWillUnmount: function(arg) { + console.log('views.month.viewWillUnmount(arg): ', arg); + }, + eventClick: this.handleEventClick.bind(this), + // eventMouseEnter: function(info) { info.jsEvent.preventDefault(); console.log('eventMouseEnter(info): ', info); }, + // eventMouseLeave: function(info) { info.jsEvent.preventDefault(); console.log('eventMouseLeave(info): ', info); }, + eventDidMount: function(event) { + console.log('views.month.eventDidMount(event): ', event); + // console.log(' event.event._context.getCurrentData(): ', event.event._context.getCurrentData()); + } }, - monthTime: { + fecList: { type: 'list', buttonText: 'List', - sortBy: 'time', - duration: { months: 1, intervalUnit: 'month' } + duration: { months: 1, intervalUnit: 'month' }, + // noEventsDidMount: function(info) {}, // Not valid here + // dayHeaderContent: function(d) {}, // Overridden by FecListViewPlugin + // eventContent: function(arg) {}, // Overridden by FecListViewPlugin + // eventDataTransform: function(arg) {}, // Overridden by FecListViewPlugin + // viewDidMount: function(arg) {}, // Overridden by FecListViewPlugin + // viewWillUnmount: function(arg) {}, // Overridden by FecListViewPlugin + // + // onRender: function(arg) {}, // Not valid here + // render: function(arg) {}, // Not valid here + eventDidMount: function(event) { + console.log('views.fecList.eventDidMount(event): ', event); + // console.log(' event.event._context.getCurrentData(): ', event.event._context.getCurrentData()); + } } - } + }, + // dayHeaderContent: function(d) { + // This is called if views.month.dayHeaderContent doesn't exist but is never called for views.fecList + // If this doesn't return anything, day headers will be put onto the page but will have aria-hidden="true" + // return { html: `` }; + // }, + // eventContent: function(arg) { + // This is called if views.month.eventContent doesn't exist but is never called for views.fecList + // If this doesn't return anything, events will be put onto the page but will have aria-hidden="true" + // console.log('Calendar.eventContent(arg): ', arg); + // }, + // render: function(arg) {}, // Not valid here + // onRender: function(arg) {}, // Not valid here }, sourceOpts: { startParam: 'min_start_date', endParam: 'max_start_date', - success: this.success.bind(this) + initialDate: '2024-12-15', + success: this.handleCalendarSuccess.bind(this), + failure: this.handleCalendarFailure.bind(this), + datesSet: this.handleCalendarDatesSet.bind(this), // TODO: is this doing anything here? + eventDidMount: function(event) { + console.log('sourceOpts.eventDidMount(event): ', event); + // console.log(' event.event._context.getCurrentData(): ', event.event._context.getCurrentData()); + } } }; }; -Calendar.prototype.filter = function() { +/** + * Called on $form.change, but also on window.popstate and inside the Calendar constructor + * @returns + */ +Calendar.prototype.handleFilterChange = function() { + // console.log('Calendar.handleFilterChange()'); const params = this.filterSet.serialize(); + // A possible way to get rid of _isEqual for Objects is to use + // new Set(Object.getOwnPropertyNames(o1)).intersection(new Set(Object.getOwnPropertyNames(o2))); + // and + // new Set(Object.values(o1)).intersection(new Set(Object.values(o2))); if (_isEqual(params, this.params)) { return; } - const url = this.url + const url_data = this.url_api .clone() .addQuery(params || {}) .toString(); + + // console.log(' url_data: ', url_data); pushQuery(this.filterSet.serialize(), this.filterSet.fields); - this.$calendar.fullCalendar('removeEventSource', this.sources); - this.sources = $.extend({}, this.opts.sourceOpts, { url: url }); - this.$calendar.fullCalendar('addEventSource', this.sources); + // this.$calendar.fullCalendar('removeEventSource', this.eventSources); + // if (this.eventSources) this.eventSources.remove();// TODO: do we need to remove them anymore? + // this.eventSources = $.extend({}, this.opts.sourceOpts, { url: url }); + this.eventSources = Object.assign( + {}, + this.opts.sourceOpts, + { + url: url_data, + } + ); + this.calendar.removeAllEventSources(); + this.calendar.addEventSource(this.eventSources); this.updateLinks(params); this.params = params; + + this.calendar.render(); +}; + +/** + * Called when the view or month changes + * TODO: Do we still need this? is it doing anything—are startStr or endStr being used anywhere? + * @param {Object} obj + */ +Calendar.prototype.handleCalendarDatesSet = function(obj) { + console.log('Calendar.handleCalendarDatesSet(obj): ', obj); + // obj.startStr = obj.startStr.slice(0,10); + // obj.endStr = obj.endStr.slice(0,10); +}; + +// TODO: do this? +Calendar.prototype.handleCalendarFailure = function(arg) { + console.log('Calendar.handleCalendarFailure(arg): ', arg); }; -Calendar.prototype.success = function(response) { +/** + * + * @param {Object} content + * @param {Response} response + * @returns + */ +Calendar.prototype.handleCalendarSuccess = function(content, response) { + console.log('Calendar.handleCalendarSuccess(content, response): ', content, response); const self = this; setTimeout(function() { - $('.is-loading') - .removeClass('is-loading') - .addClass('is-successful'); + const elements = document.querySelectorAll('.is-loading'); + elements.forEach(el => { + el.classList.remove('is-loading'); + el.classList.add('is-successful'); + }); }, LOADING_DELAY); setTimeout(function() { - $('.is-successful').removeClass('is-successful'); + const elements = document.querySelectorAll('is-successful'); + elements.forEach(el => { + el.classList.remove('is-successful'); + }); }, SUCCESS_DELAY); - return response.results.map(function(event) { - let processed = { - category: event.category, - location: event.location, - title: event.summary || 'Event title', - summary: event.summary || 'Event summary', - description: event.description || 'Event description', - state: event.state ? event.state.join(', ') : null, - start: event.start_date ? moment(event.start_date) : null, - hasStartTime: checkStartTime(event), - end: event.end_date ? moment(event.end_date) : null, - className: className(event), - tooltipContent: mapCategoryDescription(event.category), - allDay: event.all_day, - detailUrl: event.url - }; - _extend(processed, { - google: getGoogleUrl(processed), - download: self.subscribeUrl - .clone() - .addQuery({ event_id: event.event_id }) - .toString() - }); - return processed; + self.handleCalendarRender(self.calendar.view); + + content.results = content.results.map(event => { + return self.convertEventImplToVisualEvent(event); }); + + return content.results; }; Calendar.prototype.updateLinks = function(params) { - const url = this.exportUrl.clone().addQuery(params || {}); - const subscribeURL = this.subscribeUrl.clone().addQuery(params || {}); + console.log('Calendar.updateLinks(params): ', params); + const url = this.url_export.clone().addQuery(params || {}); + const url_subscribe = this.url_subscribe.clone().addQuery(params || {}); const urls = { ics: url.toString(), csv: url @@ -224,86 +366,128 @@ Calendar.prototype.updateLinks = function(params) { encodeURIComponent( url .clone() - .protocol('http') + .protocol('webcal') .toString() ), calendar: url.protocol('webcal').toString(), + subscribe: url_subscribe.protocol('webcal').toString(), - googleSubscribe: + subscribe_webcal: url_subscribe.protocol('webcal').toString(), + subscribe_google: 'https://calendar.google.com/calendar/render?cid=' + encodeURIComponent( - subscribeURL + url_subscribe + .clone() + .protocol('webcal') + .toString() + ), + subscribe_ms365: + // 'https://outlook.office.com/owa?path=%2Fcalendar%2Faction%2Fcompose&rru=addsubscription&name=FEC&url=' + + 'https://outlook.office.com/calendar/addfromweb?name=FEC&url=' + + encodeURIComponent( + url_subscribe .clone() - .protocol('http') + // .setSearch('min_start_date', '01/01/2025') + // .setSearch('per_page', 2) + .protocol('webcal') .toString() ), - calendarSubscribe: subscribeURL.protocol('webcal').toString() + + // url_subscribe.protocol('webcal').toString(), }; - this.$download.html(templates.download(urls)); - this.$subscribe.html(templates.subscribe(urls)); + // this.$download.html(templates.download(urls)); + this.downloadEl.innerHTML = templates.download(urls); + // this.$subscribe.html(templates.subscribe(urls)); + this.subscribeEl.innerHTML = templates.subscribe(urls); - if (this.downloadButton) { - this.downloadButton.destroy(); - } - - if (this.subscribeButton) { - this.subscribeButton.destroy(); - } + if (this.downloadButton) this.downloadButton.destroy(); + this.downloadButton = new Dropdown(this.opts.selector_download, { checkboxes: false }); - this.downloadButton = new Dropdown(this.$download, { - checkboxes: false - }); - this.subscribeButton = new Dropdown(this.$subscribe, { - checkboxes: false - }); + if (this.subscribeButton) this.subscribeButton.destroy(); + this.subscribeButton = new Dropdown(this.opts.selector_subscribe, { checkboxes: false }); }; +/** + * Adds the fec-specific class names to fc elements + */ Calendar.prototype.styleButtons = function() { - const baseClasses = 'button'; - this.$calendar.find('.fc-button').addClass(baseClasses); - this.$calendar.find('.fc-today-button').addClass('button--alt'); - this.$calendar - .find('.fc-next-button') - .addClass('button--next button--standard'); - this.$calendar - .find('.fc-prev-button') - .addClass('button--previous button--standard'); - this.$calendar - .find('.fc-right .fc-button-group') - .addClass('toggles--buttons'); - this.$calendar - .find('.fc-monthTime-button') - .addClass('button--list button--alt'); - this.$calendar.find('.fc-month-button').addClass('button--grid button--alt'); + const baseClasses = ['button']; + // document.querySelector('.fc-button').classList.add(...baseClasses); + // document.querySelectorAll('.fc-button').forEach(el => { el.classList.add(...baseClasses) }); + // document.querySelector('.fc-toolbar-start .fc-today-button').classList.add('button', 'button--alt'); + // document.querySelector('.fc-toolbar-start .fc-next-button').classList.add( + // 'button', 'button--next', 'button--standard'); + // document.querySelector('.fc-toolbar-start .fc-prev-button').classList.add( + // 'button', 'button--previous', 'button--standard'); + // document.querySelector('.fc-toolbar-end .btn-group').classList.add('toggles--buttons'); + // document.querySelector('.fc-toolbar-end .fc-fecList-button').classList.add( + // 'button', 'button--list', 'button--alt'); + // document.querySelector('.fc-toolbar-end .fc-dayGridMonth-button').classList.add('button', 'button--grid', 'button--alt'); }; -Calendar.prototype.handleRender = function(view) { - $(document.body).trigger($.Event('calendar:rendered')); - if (LIST_VIEWS.indexOf(view.name) !== -1) { - this.manageListToggles(view); - } else if (this.$listToggles) { - this.$listToggles.remove(); - this.$listToggles = null; - } - this.$calendar - .find('.fc-more') - .attr({ tabindex: '0', 'aria-describedby': this.popoverId }); - this.$head.find('.js-calendar-title').html(view.title); +/** + * + * @param {ViewImpl} view + * No other params + */ +Calendar.prototype.handleCalendarRender = function(view) { + console.log('Calendar.handleCalendarRender(view, two): ', view); + console.log(' calendar.view: ', calendar.view); + + $(document.body).trigger($.Event('calendar:rendered')); // TODO: do we need this? Is anything listening to it? + + + // const viewButtonMonth = document.querySelector('.fc-dayGrid-button'); + // viewButtonMonth.dataset.triggerView = 'dayGridMonth'; + // viewButtonMonth.addEventListener('click', this.handleViewToggle.bind(this)); + + // MOVED TO THE PLUGIN + // if (FEC_LIST_SORT.includes(calendar.view.custom.sortBy)) { + // console.log(' if'); + // this.manageListToggles(view); + + // } else if (this.$listToggles) { + // console.log(' else if'); + // this.$listToggles.remove(); + // this.$listToggles = null; + // } else { + // console.log(' else'); + // } + // this.$calendar + // .find('.fc-more') + // .attr({ tabindex: '0', 'aria-describedby': this.popoverId }); + const fcMore = this.calendarEl.querySelector('.fc-more'); + if (fcMore) fcMore.setAttribute('tabindex', 0); + if (fcMore) fcMore.setAttribute('aria-describedby', this.popoverId); + + // Move 'Month YYYY' to be the page title, not just the view title + document.querySelector('.js-calendar-title').innerHTML = view.title; }; -Calendar.prototype.manageListToggles = function(view) { - if (!this.$listToggles) { - this.$listToggles = $('
'); - this.$listToggles.appendTo(this.$calendar.find('.fc-right')); - } - this.$listToggles.html(templates.listToggles(view.options)); - // Highlight the "List" button on monthTime - if (view.name === 'monthCategory') { - this.$calendar.find('.fc-monthTime-button').addClass('fc-state-active'); - } +/** + * + * @param {PointerEvent} e + */ +Calendar.prototype.handleViewToggle = function(e) { + // console.log('Calendar.handleViewToggle(view): ', e); + calendar.changeView(e.target.dataset.triggerView); + e.target.classList.add('active'); + + // const togglesHolder = document.querySelector('.fc-header-toolbar .fc-toolbar-center'); + + // if (!this.$listToggles) { + // this.$listToggles = $('
'); + // this.$listToggles.appendTo(this.$calendar.find('.fc-toolbar-end .toggles--buttons')); + // } + // this.$listToggles.html(templates.listToggles(view.options)); + // // Highlight the "List" button on monthTime + // if (view.name === 'monthCategory') { + // this.$calendar.find('.fc-monthTime-button').addClass('fc-state-active'); + // } }; Calendar.prototype.handleEventRender = function(event, element) { + console.log('Calendar.handleEventRender(event, element): ', event, element); const eventLabel = event.title + ' ' + @@ -318,40 +502,232 @@ Calendar.prototype.handleEventRender = function(event, element) { }; Calendar.prototype.handleDayRender = function(date, cell) { + console.log('Calendar.handleDayRender(date, cell): ', date, cell); if (date.date() === 1) { cell.append(date.format('MMMM')); } }; -Calendar.prototype.handleEventClick = function(calEvent, jsEvent) { - const $target = $(jsEvent.target); - if (!$target.closest('.tooltip').length) { - const $eventContainer = $target.closest('.fc-event'); - const tooltip = new CalendarTooltip( - templates.details(_extend({}, calEvent, { detailsId: this.detailsId })), - - $eventContainer - ); - $eventContainer.append(tooltip.$content); +/** + * + * @param {Object} eventObject + * @param {HTMLElement} eventObject.el - The button element clicked (.fc-event) + * @param {EventImpl} eventObject.event + * @param {PointerEvent} eventObject.jsEvent + * @param {ViewImpl} eventObject.view + */ +Calendar.prototype.handleEventClick = function(eventObject) { + console.log('Calendar.handleEventClick(eventObject): ', eventObject); + // eventObject.jsEvent.preventDefault(); + // eventObject.jsEvent.stopPropagation(); + + const tooltipParentEl = eventObject.el.parentNode; + console.log('tooltipParentEl: ', tooltipParentEl); + + + let tooltip = tooltipParentEl.querySelector('.tooltip'); + console.log(' tooltip: ', tooltip); + + // If there's no tooltip, + if (!tooltip || tooltip == null) { + // build its content, + + tooltip = document.createElement('div'); + tooltip.setAttribute('class', 'tooltip tooltip--under cal-details'); + tooltip.setAttribute('id', this.detailsId); + tooltip.setAttribute('role', 'tooltip'); + // + + + const eventDataObjForTemplate = this.convertEventImplToVisualEvent(eventObject.event._def.publicId); + // eventDataObjForTemplate.detailsId = this.detailsId; + console.log(' eventDataObjForTemplate: ', eventDataObjForTemplate); + const popupDetailsContent = templates.details(eventDataObjForTemplate); + + // console.log(' going to create this tooltip: ', popupDetailsContent); + // append the content, + tooltip.innerHTML = popupDetailsContent; + // and select the element again. + // tooltip = tooltipParentEl.querySelector('.tooltip'); + eventObject.el.insertAdjacentElement('afterend', tooltip); + + // tooltip = null; + // event + // tooltip = document.createElement('div'); + // tooltipHolder.classList.add('tooltip-holder'); + // target.appendChild(tooltipHolder); } + + new CalendarTooltip(tooltip); + tooltip = null; + + // NOW we can activate the tooltip for this event + + + // const tooltip = new CalendarTooltip( + // , + // target + // ); + // target.appendChild(tooltip.$content); + + // const $target = $(jsEvent.target); + // if (!$target.closest('.tooltip').length) { + // const $eventContainer = $target.closest('.fc-event'); + // const tooltip = new CalendarTooltip( + // templates.details(_extend({}, calEvent, { detailsId: this.detailsId })), + + // $eventContainer + // ); + // $eventContainer.append(tooltip.$content); + // } }; // Simulate clicks when hitting enter on certain full-calendar elements Calendar.prototype.simulateClick = function(e) { + console.log('Calendar.simulateClick(e): ', e); if (e.keyCode === 13) { - $(e.target).click(); // TODO: jQuery deprecation + $(e.target).trigger('click'); } }; +/** + * + * @param {*} e + */ Calendar.prototype.managePopoverControl = function(e) { + console.log('Calendar.managePopoverControl(e): ', e); const $target = $(e.target); const $popover = this.$calendar.find('.fc-popover'); $popover.attr('id', this.popoverId).attr('role', 'tooltip'); $popover .find('.fc-close') .attr('tabindex', '0') - .focus() // TODO: jQuery deprecation + .trigger('focus') .on('click', function() { - $target.focus(); // TODO: jQuery deprecation + $target.trigger('focus'); }); }; + +/** + * Creates the original dataSources event, adds parameters as needed, and returns an object used to replace that + * initial element or used to send into hbs templates. + * @param {(number|Object)} event - the number that comes in from dataSources or the value saved to publicId + * @returns {Object} Data object to be used for events through the app and in hbs templates + */ +Calendar.prototype.convertEventImplToVisualEvent = function(event) { + console.log('Calendar.convertEventImplToVisualEvent(event): ', event); + + let d = {}; + let toReturn = {}; + + // If event is an object, we need to create it for the first time, + if (typeof event == 'object') { + d = event; + + toReturn = { + allDay: d.all_day, + category: d.category, + className: '', // className(d), + description: d.description || 'Event description', + end: d.end_date ? d.end_date : null, // used by getGoogleUrl() // TODO: Do we need this if we're using GMT? + end_google: d.end_date, + event_id: d.event_id, + display: 'block', + hasStartTime: checkStartTime(d), + id: d.event_id, // gets converted to publicId to be used with calendar.getEventById(); + location: d.location, + start: d.start_date, // used by getGoogleUrl() // TODO: Do we need this if we're using GMT? + start_google: d.start_date, + state: d.state ? d.state.join(', ') : null, + summary: d.summary || 'Event summary', + title: d.summary || 'Event title', + tooltipContent: mapCategoryDescription(d.category), + url_details: d.url + }; + + // Adjusting the datetime here (from GMT to ET) doesn't seem to work + + toReturn.url_google = getGoogleUrl({ + start: 'START', + end: 'END', + title: 'TITLE', + summary: 'SUMMARY' + }); + toReturn.url_ms365 = getMs365Url({ + start: 'START', + end: 'END', + title: 'TITLE', + summary: 'SUMMARY' + }); + toReturn.url_download = getDownloadUrl( + this.url_subscribe + .clone() + .addQuery({ event_id: d.event_id }) + .toString() + ); + + // But it's not an object, we need to find the calendar's event object to just use those values + } else { + d = calendar.getEventById(event); + console.log(' d: ', d); + toReturn = Object.assign( + {}, + d._def.extendedProps, + { + allDay: d._def.allDay, + className: d._def.className, + hasEnd: d._def.hasEnd, + // end: d._def.end_date ? d.end_date : null, // used by getGoogleUrl() // TODO: Do we need this if we're using GMT? + id: d.event_id, // gets converted to publicId to be used with calendar.getEventById(); + publicId: d._def.publicId, + range: d._instance.range, + // start: d._def.start_date, // used by getGoogleUrl() // TODO: Do we need this if we're using GMT? + title: d._def.title, + // url_details: d._def.url, + } + ); + toReturn.url_google = getGoogleUrl({ + start: 'START2', + end: 'END2', + title: 'TITLE2', + summary: 'SUMMARY2' + }); + + // Get the start and end Date objects (GMT) + let start_date = new Date(toReturn.range.start); + let end_date = toReturn.hasEnd ? new Date(toReturn.range.end) : false; + + start_date.setMinutes(start_date.getMinutes() + start_date.getTimezoneOffset()); + toReturn.start_dayMonthTime = start_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL_MONTH_DAY); + toReturn.start_dayMonthTime += `, ${start_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE)}`; + + toReturn.start_iso = start_date.toISOString(); + // toReturn.start_day = start_date.toLocaleDateString('en-US', dateTimeFormatOptions.WEEKDAY_FULL); + toReturn.start_date_string = start_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL); + // if (toReturn.hasStartTime) + // toReturn.start_time = start_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE); + + if (end_date) { + end_date.setMinutes(end_date.getMinutes() + end_date.getTimezoneOffset()); + toReturn.end_dayMonthTime = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL_MONTH_DAY); + toReturn.end_dayMonthTime += `, ${end_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE)}`; + toReturn.end_iso = end_date.toISOString(); + // toReturn.end_dayMonth = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL_MONTH_DAY); + + // toReturn.end_day = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.WEEKDAY_FULL); + // toReturn.end_date_string = end_date.toLocaleDateString('en-US', dateTimeFormatOptions.DATE_FULL); + // if (toReturn.hasStartTime) + // toReturn.end_time = end_date.toLocaleTimeString('en-US', dateTimeFormatOptions.TIME_SIMPLE); + } + } + console.log(' going to return: ', toReturn); + return toReturn; +} + +Calendar.prototype.buildTagsForInitialFilters = function() { + const filterAndTagName = 'calendar_category_id'; + const selectedCategoryInputs = document.querySelectorAll(`#category-filters input[name="${filterAndTagName}"]:checked`); + selectedCategoryInputs.forEach(input => { + this.tagList.addTag(null, { key: input.value, name: filterAndTagName, value: input.labels[0].textContent }); + }); +} diff --git a/fec/fec/static/js/modules/dropdowns.js b/fec/fec/static/js/modules/dropdowns.js index 0d1c0762f1..5bbb4118e2 100644 --- a/fec/fec/static/js/modules/dropdowns.js +++ b/fec/fec/static/js/modules/dropdowns.js @@ -16,6 +16,7 @@ const defaultOpts = { * @constructor * @param {string} selector - CSS selector for the fieldset that contains everything * @param {Object} opts - Options + * @param {boolean} opts.checkboxes */ export default function Dropdown(selector, opts) { this.opts = $.extend({}, defaultOpts, opts); @@ -171,6 +172,12 @@ Dropdown.prototype.handleCheckboxRemoval = function($input) { } }; +/** + * + * @param {JQuery.Events} e + * @param {Object} opts + * @param {string} opts.key + */ Dropdown.prototype.handleRemoveClick = function(e, opts) { let $input = $(e.target) .parent() diff --git a/fec/fec/static/js/modules/election-search.js b/fec/fec/static/js/modules/election-search.js index a81dac7970..0990f6e3db 100644 --- a/fec/fec/static/js/modules/election-search.js +++ b/fec/fec/static/js/modules/election-search.js @@ -564,6 +564,7 @@ ElectionSearch.prototype.formatName = function(result) { /** * Get the date of the general election for a two-year period * @param {Object} result + * @param {number} result.cycle * @returns {string} - In the `MMMM Do, YYYY` format * (i.e. Full month name + ordinal date + four-digit year e.g. August 8th 2008) */ diff --git a/fec/fec/static/js/modules/filters/filter-panel.js b/fec/fec/static/js/modules/filters/filter-panel.js index afd68691f3..bfad2366b9 100644 --- a/fec/fec/static/js/modules/filters/filter-panel.js +++ b/fec/fec/static/js/modules/filters/filter-panel.js @@ -2,7 +2,7 @@ import { default as FilterSet } from './filter-set.js'; import { removeTabindex, restoreTabindex } from '../accessibility.js'; import { BREAKPOINTS, getWindowWidth, isLargeScreen } from '../helpers.js'; -/** @enum */ +/** @enum {string} */ const defaultOptions = { body: '.filters', content: '.filters__content', diff --git a/fec/fec/static/js/modules/filters/filter-set.js b/fec/fec/static/js/modules/filters/filter-set.js index c7f1e3829c..97bcf47a19 100644 --- a/fec/fec/static/js/modules/filters/filter-set.js +++ b/fec/fec/static/js/modules/filters/filter-set.js @@ -27,7 +27,7 @@ export default function FilterSet(elm) { this.$body.on('filters:validation', this.handleValidation.bind(this)); this.efiling = this.$body.data('efiling-filters') || false; - /** Array of names of filter/API var name IDs {@example ['committee_id', 'form_line_number']} */ + /** Array of names of filter/API var name IDs ex. ['committee_id', 'form_line_number'] */ this.fields = []; this.isValid = true; this.firstLoad = true; diff --git a/fec/fec/static/js/modules/filters/filter-tags.js b/fec/fec/static/js/modules/filters/filter-tags.js index ac0706ea22..23d2e35c80 100644 --- a/fec/fec/static/js/modules/filters/filter-tags.js +++ b/fec/fec/static/js/modules/filters/filter-tags.js @@ -110,6 +110,7 @@ export default function TagList(opts) { * @param {boolean} [opts.range=false] - If true, will use opts.rangename over opts.name * @param {string} [opts.rangeName] - Used as data-tag-category="" if opts.range is true * @param {boolean} [opts.nonremovable=false] - determines which template to use. Default: false + * @param {string} [opts.value] - The text label */ TagList.prototype.addTag = function(e, opts) { const tag = opts.nonremovable diff --git a/fec/fec/static/js/modules/urls.js b/fec/fec/static/js/modules/urls.js index c8931aa2cb..0ed6a7e32c 100644 --- a/fec/fec/static/js/modules/urls.js +++ b/fec/fec/static/js/modules/urls.js @@ -12,8 +12,9 @@ import { sanitizeQueryParams } from './helpers.js'; /** * Takes a list of key/value, runs them through {@linkcode nextUrl()} and sets them to the window.history * then logs an analytics pageView - * @param {Object} params - Object of key/value for query params and their values - * @param {Array} fields - Object TODO + * @param {Object} params - Object of key/value for query params and their values. + * ex: {data_type: ['processed'], max_date: ['12/31/2000'], min_date: ['01/01/2000']} + * @param {Array} fields - Array of allowed filter fields. ex: ['data_type', 'committee_id', 'max_date', 'min_date'] */ export function updateQuery(params, fields) { const queryString = nextUrl(params, fields); diff --git a/fec/fec/static/js/pages/calendar-page.js b/fec/fec/static/js/pages/calendar-page.js index 7a4fe7a498..35111e7371 100644 --- a/fec/fec/static/js/pages/calendar-page.js +++ b/fec/fec/static/js/pages/calendar-page.js @@ -1,7 +1,10 @@ /** - * + * For the calendar page, + * - initializes the filterPanel + * - initializes the tagList + * - initializes the Calendar (Calendar is the FEC implementation of @fullcalendar/core) */ -import { calendarDownload, getUrl } from '../modules/calendar-helpers.js'; +import { getUrl } from '../modules/calendar-helpers.js'; import Calendar from '../modules/calendar.js'; import FilterPanel from '../modules/filters/filter-panel.js'; import TagList from '../modules/filters/filter-tags.js'; @@ -10,22 +13,21 @@ import TagList from '../modules/filters/filter-tags.js'; const filterPanel = new FilterPanel(); // Initialize filter tags -const $tagList = new TagList({ +const tagList = new TagList({ resultType: 'events', emptyText: 'all events' -}).$body; +}); -$('.js-filter-tags').prepend($tagList); +$('.js-filter-tags').prepend(tagList.$body); // Initialize calendar new Calendar({ + filterPanel: filterPanel, + tagList: tagList, selector: '#calendar', - download: '#calendar-download', - subscribe: '#calendar-subscribe', - url: getUrl(['calendar-dates']), - exportUrl: calendarDownload(['calendar-dates', 'export']), - subscribeUrl: getUrl(['calendar-dates', 'export'], '', [ - 'sub' - ]), - filterPanel: filterPanel + selector_download: '#calendar-download', + selector_subscribe: '#calendar-subscribe', + url_api: getUrl(['calendar-dates']), + url_export: getUrl(['calendar-dates', 'export']), + url_subscribe: getUrl(['calendar-dates', 'export'], '', ['sub']) }); diff --git a/fec/fec/static/js/templates/calendar/details.hbs b/fec/fec/static/js/templates/calendar/details.hbs index 870995b027..72c62ff92c 100644 --- a/fec/fec/static/js/templates/calendar/details.hbs +++ b/fec/fec/static/js/templates/calendar/details.hbs @@ -1,34 +1,32 @@ - --}} diff --git a/fec/fec/static/js/templates/calendar/event.hbs b/fec/fec/static/js/templates/calendar/event.hbs new file mode 100644 index 0000000000..ceb0ea4b7e --- /dev/null +++ b/fec/fec/static/js/templates/calendar/event.hbs @@ -0,0 +1,53 @@ +
+
+
+ {{datetime start format="pretty"}} + {{datetime start format="fullDayOfWeek"}} +
+
+ {{#if hasStartTime}} + {{datetime start format="time"}} + {{#if end}}–{{datetime end format="time"}}{{/if}} + {{else}} +   + {{/if}} +
+
+

{{summary}}

+

{{description}}

+ {{#if location}} + Location: {{location}} + {{/if}} + {{#if url_details}} + Learn more + {{/if}} +
+
+ {{#if tooltipContent }} +
+ {{ toUpperCase category }} +
+ + +
+
+ {{else}} + {{ toUpperCase category }} + {{/if}} +
+
+
+ +
+
diff --git a/fec/fec/static/js/templates/calendar/events.hbs b/fec/fec/static/js/templates/calendar/events.hbs index 5f0acd065d..be88c4cb87 100644 --- a/fec/fec/static/js/templates/calendar/events.hbs +++ b/fec/fec/static/js/templates/calendar/events.hbs @@ -1,23 +1,24 @@ -
+
{{#each groups}} -
+
{{#if title}} -

{{title}}

+ {{!--

{{title}}

--}} + {{/if}} {{#each events}}
- {{datetime start format="pretty"}} - {{datetime start format="fullDayOfWeek"}} + {{start_date_string}} + {{start_day}}
- {{#if hasStartTime}} - {{datetime start format="time"}} - {{#if end}}–{{datetime end format="time"}}{{/if}} - {{else}} + {{~#if hasStartTime ~}} + {{~ start_time_string ~}} + {{#if end_time_string}}–{{end_time_string}}{{/if}} + {{~else~}}   - {{/if}} + {{~/if~}}

{{summary}}

@@ -25,8 +26,8 @@ {{#if location}} Location: {{location}} {{/if}} - {{#if detailUrl}} - Learn more + {{#if url_details}} + Learn more {{/if}}
@@ -48,12 +49,13 @@
- diff --git a/fec/fec/static/js/templates/calendar/listToggles.hbs b/fec/fec/static/js/templates/calendar/listToggles.hbs index 9b99342cde..22a592e088 100644 --- a/fec/fec/static/js/templates/calendar/listToggles.hbs +++ b/fec/fec/static/js/templates/calendar/listToggles.hbs @@ -1,12 +1,12 @@
Sort by: + class="toggle js-toggle-list-sort + {{#if (eq listEventOrder 'monthTime')}}is-active{{/if}}" + data-trigger-sort="monthTime">Date | + class="toggle js-toggle-list-sort + {{#if (eq listEventOrder 'monthCategory')}}is-active{{/if}}" + data-trigger-sort="monthCategory">Category
diff --git a/fec/fec/static/js/templates/calendar/subscribe.hbs b/fec/fec/static/js/templates/calendar/subscribe.hbs index 6ec52830ed..b7395328b6 100644 --- a/fec/fec/static/js/templates/calendar/subscribe.hbs +++ b/fec/fec/static/js/templates/calendar/subscribe.hbs @@ -1,9 +1,9 @@ - diff --git a/fec/fec/static/scss/components/_buttons.scss b/fec/fec/static/scss/components/_buttons.scss index caa7adfd51..5436679f76 100644 --- a/fec/fec/static/scss/components/_buttons.scss +++ b/fec/fec/static/scss/components/_buttons.scss @@ -74,7 +74,7 @@ } .button--alt { - background-color: none; + background-color: transparent; border-color: $gray; color: $base; @extend .button; diff --git a/fec/fec/static/scss/components/_calendar.scss b/fec/fec/static/scss/components/_calendar.scss index f787163d29..9dce2cd861 100644 --- a/fec/fec/static/scss/components/_calendar.scss +++ b/fec/fec/static/scss/components/_calendar.scss @@ -3,38 +3,12 @@ // Styles for the Calendar page, which uses fullcalendar // -.fc-view-container { - font-family: $sans-serif; - padding: u(0 2rem 2rem 2rem); -} - -.fc-bg { - z-index: -1; - position: absolute; - top: 0; - left: 0; - right: 0; - - .fc-day { - font-weight: bold; - padding: u(0.7rem 1rem 0 2rem); - } -} - -.fc-row { - position: relative; -} - -// Controls -.fc-toolbar { - padding: u(1rem); - @include clearfix; -} - +// Before the calendar starts .calendar__head { border-bottom: 1px solid $neutral; padding: u(1rem); - @include clearfix; + + // @include clearfix(); .data-container__action { margin-top: u(1rem); @@ -48,429 +22,429 @@ } } -.fc-view-controls { - padding: u(2rem); - width: 100%; - @include clearfix; -} - -.fc-left { - float: left; - - .fc-button-group { - float: left; - margin-right: u(1rem); - } - - .fc-today-button { - margin-left: u(1rem); - } +// Header toolbar (prev, next, This Month, Sort by, List, Grid) +.fc-header-toolbar { + font-family: $sans-serif; + padding: 1rem; + // @include clearfix(); +} +.fc-toolbar-start { .fc-prev-button, .fc-next-button { + background-position: center; margin-right: 1px; + padding: u(.8rem 1rem); text-indent: -9999px; - padding: u(0.8rem 1rem); + @extend .button--standard; &::after { - left: u(0.5rem); - right: u(0.5rem); + left: u(.5rem); + right: u(.5rem); } } + .fc-prev-button { + @extend .button--previous; + } + .fc-next-button { + @extend .button--next; + } + .fc-today-button { + margin-left: u(1rem); + @extend .button--alt; + } } - -.fc-prev-button { - border-radius: 2px 0 0 2px; -} - -.fc-next-button { - border-radius: 0 2px 2px 0; -} - -.fc-state-active.button--alt { - @extend .button--alt, .is-active; -} - -.fc-right { +.fc-toolbar-center { display: none; -} + flex-grow: 1; + text-align: right; -.fc-center { - text-align: left; + @include media($med) { + display: block; + } - h2 { - margin: 0; + .toggle:last-child { + margin-right: 0; } } +.fc-toolbar-end { + display: none; -// Body + .btn-group { + @extend .toggles--buttons; + } + .btn { + background-position: center; + padding: 8px 2rem; + text-indent: -9999px; + width: 4rem; -.fc-day-header { - color: $primary; - font-weight: normal; - padding-top: u(0.5rem); + &.active { + @extend .is-active; + } + } + .fc-fecList-button { + @extend .button--list; + @extend .button--alt; + } + .fc-dayGridMonth-button { + @extend .button--grid; + @extend .button--alt; + } + @include media($med) { + display: block; + } } -.fc-content-skeleton { - height: 100%; - table { - min-height: u(12rem); +// fullcalendar/bootstrap overrides +.fc { + font-family: $sans-serif; + gap: 1rem; + + .fc-view-outer { + padding: u(0 2rem 2rem 2rem); + } + + // overrides specifically for our custom list view + &[data-fec-view="list"] { + display: block; // Overriding fc defaults + + .fc-view-outer { + padding: 0 2rem 2rem !important; + position: relative; + } + .fc-fecList-view { + display: block; + position: relative; + } + .fc-list-day { + display: block; + padding: 0; + } } } -.fc-other-month { - &.fc-day-top, - &.fc-day { - opacity: 0.5; - } +.fc-theme-bootstrap5 { + line-height: inherit; } -.fc-week { - td:not(.fc-day-top) { - border-width: 0 2px; - border-style: solid; - border-color: $neutral; - border-top: none; - } - - &:last-child { - border-bottom: 2px solid $neutral; +// Only applies to the grid +.fc-daygrid-header { + .fc-day { + color: $primary; + font-weight: normal; + padding-top: u(.5rem); } } -.fc-day-top { - border: 2px solid $neutral; - border-bottom: none; - color: $primary; - font-weight: bold; - padding: u(0.3rem 1rem); +.fc-daygrid { + padding: 0 1rem; } - -.fc-day-number.fc-today { - background-color: $primary; - color: $inverse; +.fc-daygrid-day-header { + flex-direction: row; + font-weight: bold; + min-height: 3rem; + overflow-x: hidden; + width: 100%; } - -.fc-event-container { +.fc-daygrid-day-body { padding: 2px; - vertical-align: top; -} - -.fc-more { - margin: 0 4px; } +.fc-daygrid-day { + border: 1px solid $neutral; + min-height: 12rem; + padding: 0; -.fc-content { - white-space: nowrap; - overflow: hidden; - background: none; - color: $primary; - font-size: u(1.4rem); - line-height: 1.2; - padding: 2px 4px; - position: relative; - - &::before { - content: ''; - display: block; - width: u(0.6rem); - height: u(0.6rem); - background-color: $primary; - border-radius: u(0.8rem); - float: left; - margin: 5px 5px 5px 0; + &.fc-day-today { + background: transparent; } - &::after { - content: ''; - display: block; + &[data-date$="-01"] .fc-daygrid-day-number::after { + display: inline-block; position: absolute; - top: 0; - bottom: 0; - right: 0; - width: u(3rem); - @include linear-gradient(90deg, rgba($gray-lightest, 0), $gray-lightest); + left: 1.33em; } -} - -.fc-multi-day { - .fc-content { - border: 1px solid $gray; - border-radius: 2px; - padding-left: 2px; - - &::before { - display: none; - } + &[data-date$="-01-01"] .fc-daygrid-day-number::after { + content: 'January'; } -} - -.is-active { - .fc-content { - background-color: $primary; - border-radius: 4px; - color: $inverse; - - &::before { - background-color: $inverse; - } - - &::after { - display: none; - background-color: transparent; - } + &[data-date$="-02-01"] .fc-daygrid-day-number::after { + content: 'February'; + } + &[data-date$="-03-01"] .fc-daygrid-day-number::after { + content: 'March'; + } + &[data-date$="-04-01"] .fc-daygrid-day-number::after { + content: 'April'; + } + &[data-date$="-05-01"] .fc-daygrid-day-number::after { + content: 'May'; + } + &[data-date$="-06-01"] .fc-daygrid-day-number::after { + content: 'June'; + } + &[data-date$="-07-01"] .fc-daygrid-day-number::after { + content: 'July'; + } + &[data-date$="-08-01"] .fc-daygrid-day-number::after { + content: 'August'; + } + &[data-date$="-09-01"] .fc-daygrid-day-number::after { + content: 'September'; + } + &[data-date$="-10-01"] .fc-daygrid-day-number::after { + content: 'October'; + } + &[data-date$="-11-01"] .fc-daygrid-day-number::after { + content: 'November'; + } + &[data-date$="-12-01"] .fc-daygrid-day-number::after { + content: 'December'; } -} - -.fc-title { - display: inline-block; - vertical-align: top; -} -.fc-time { - display: inline-block; + .fc-daygrid-day-number { + padding: .5em 1rem; + } } -.fc-limited { - display: none; -} +// GRID CALENDAR ENTRIES +// UNDO - First, strictly undoing the defaults +// Overriding the awful event absolute positioning +.fc-daygrid-day-events { + height: auto !important; -.cal__category { - border-radius: 3px; - padding: u(0.5rem); + .fc-abs { + position: relative !important; + left: initial !important; + right: initial !important; + top: initial !important; + } } -// Event details -.cal-details { +.fc-daygrid-event { font-size: u(1.4rem); - left: auto !important; - top: auto !important; - margin-top: u(1.5rem); - margin-left: u(-8rem); - padding: 0 !important; - text-align: left; - width: u(26rem); - white-space: initial; + line-height: 1.2; - &::before { - background-image: none; - left: 20%; - top: u(-2rem) !important; - @include triangle(2rem, $primary, up); + @include media($lg) { + font-size: 1.6rem; } - .button--close--primary { - position: absolute; - top: 0; - right: 0; + &:focus { + box-shadow: none; } -} + &:hover { + cursor: pointer; -.fc-event-container:nth-child(1) { - .cal-details { - margin-left: unset; + .fc-event-inner { + background-color: $gray-lightest; + } + } - &::before { - right: auto; - left: u(2rem); + &.visible-tooltip { + pointer-events: none; + + .fc-event-inner { + background-color: $federal-blue; + border-radius: 4px; + color: $inverse; /* (white) */ + + &::before { + background-color: $inverse; + } + &::after { + display: none; + } } } } -.fc-event-container:nth-child(7) { - .cal-details { - right: 0; +.fc-h-event { + background-color: inherit; + border: none; + + .fc-event-inner { + color: unset; + padding: 2px 4px; &::before { - left: auto; - right: u(1rem); + background-color: $primary; + border-radius: u(.8rem); + content: ''; + display: block; + float: left; + height: u(.6rem); + margin: 5px 5px 5px 0; + width: u(.6rem); } &::after { - left: auto; - right: u(1.2rem); + bottom: 0; + content: ''; + display: block; + position: absolute; + right: 0; + top: 0; + width: 2rem; + @include linear-gradient(90deg, rgba($gray-lightest, 0), $inverse); } } } - -.cal-details__date { - display: block; - padding-bottom: u(0.5rem); -} - -.cal-details__title { - display: block; - font-weight: bold; - line-height: 1.4; - padding-bottom: u(0.5rem); -} - -.cal-details__summary { - display: block; - font-weight: bold; - line-height: 1.2; - padding-bottom: u(1rem); +.fc-daygrid-block-event { + .fc-event-time { + font-weight: inherit; + padding: 0; + } + .fc-event-title { + padding: 0; + } } -.cal-details__description { - display: block; - line-height: 1.2; - padding-bottom: u(0.5rem); +// Overriding the default flex and absolute garbage +.fc-view-outer-aspect-ratio { + padding-bottom: 0 !important; } - -.cal-details__location { +.fc-view-outer-aspect-ratio > .fc-view.fc-dayGridMonth-view { + position: relative; display: block; - line-height: 1.2; - padding-bottom: u(0.5rem); -} - -.cal-details__add { - margin-top: u(1rem); } -.fc-popup { - background: $inverse; +.fc-popover { + background: $neutral; border: 2px solid $neutral; position: absolute; - max-width: u(20rem); - - .button { - position: absolute; - top: 0; - right: 0; - width: u(3rem); - height: u(3rem); - } -} - -.fc-popup__header { - background: $neutral; - color: $primary; - font-weight: bold; - padding: 4px; - position: relative; -} - -// Week view -.fc-time-grid-container { - overflow: scroll; -} + width: u(22rem); -.fc-time-grid { - position: relative; + // .fc-close { + // // cursor: pointer; + // float: right; + // @include u-icon($x, $primary, 2rem, 2rem, 100%); + // } - .fc-content-skeleton { - border-bottom: 2px solid $neutral; - position: absolute; - top: 0; - left: 0; - right: 0; - } -} + // .fc-event-container { + // padding: u(.5rem); + // } -.fc-slats { - td { - height: u(1.5rem); + // .fc-content { + // margin-bottom: u(.5rem); + // } + .fc-event { + margin-bottom: .5rem; } - - tr { - border: 2px solid $neutral; - border-bottom: none; - } - - .fc-minor { - border-bottom: none; - border-top: none; + .fc-event-title { + font-weight: bold; } } - -.fc-agenda-view { - .fc-bg { - display: none; // Temporary hack to remove mis-aligned background in agenda view - } - - .fc-axis { - padding: u(0.5rem 1rem 0 1rem); - text-align: right; - vertical-align: middle; - white-space: nowrap; - } - - .fc-divider { - display: none; - } - - .fc-week { - border-top: 2px solid $neutral; - } +.fc-popover-header { + padding: u(3px .5rem); } - -.cal-view__toggles { - float: right; - padding-left: u(1rem); +.fc-popover-title { + font-weight: bold; + height: auto; + line-height: u(2rem); + width: auto; } - -// List view -.cal-list__toggles { - float: left; - font-family: $sans-serif; - padding-top: u(0.5rem); +.fc-popover-body { + background-color: transparent; } .cal-list { - letter-spacing: -0.3px; + letter-spacing: -.3px; + display: flex; + flex-direction: column; } .cal-list__title { background-color: $gray-light; - border-left: 0.5rem solid $primary; + border-left: .5rem solid $primary; + display: block; font-family: $sans-serif; + font-size: 1.8rem; + font-weight: 700; letter-spacing: 0; + line-height: 1.6; margin: u(2rem 0 0 0); padding: u(1rem); } - +.cal-list__toggles { + font-family: $sans-serif; +} .cal-list__event { border-bottom: 1px solid $neutral; + display: block; + float: left; padding: u(2rem 0); - @include clearfix; + width: 100%; &:last-child { border-bottom: none; padding-bottom: 0; } -} + @include media($lg) { + padding: 2rem; + } +} +.cal-list__event-title { + margin: 0; +} +.cal-list__date { + float: left; + font-weight: bold; + width: calc(100% - 15rem); +} +.cal-list__time { + display: inline-block; + width: 15rem; +} .cal-list__details { margin-bottom: u(1rem); @include span-columns(8); -} + + @include media($lg) { + font-size: 1.6rem; + @include span-columns(10); + } + // large sizes for and inside this element + @include media($lg) { + + margin-bottom: 0; + .cal-list__date { + display: none; + } + .cal-list__time { + float: left; + width: 7rem; + } + .cal-list__info { + float: left; + margin: 0; + padding: 0 1rem 0; + width: calc(70% - 7rem); + } + .cal-list__category { + float: right; + width: calc(30%); + } + } +} .cal-list__add { @include span-columns(4); + @include media($lg) { + float: right; + @include span-columns(2); + } + .button { float: right; } } - .cal-list__summary { display: block; font-weight: bold; line-height: 1.2; - margin: 0 0 0.5rem; -} - -.cal-list__date { - font-weight: bold; - @include span-columns(4 of 8); -} - -.cal-list__time { - margin-right: 0 !important; // overriding omega() - @include span-columns(4 of 8); - @include omega; + margin: 0 0 .5rem 0; } .cal-list__info { @@ -479,6 +453,10 @@ p { font-size: u(1.4rem); + + @include media($lg) { + font-size: 1.6rem; + } } } @@ -487,26 +465,30 @@ } .cal-list__category-title { - display: table; - border: 1px solid; + flex-direction: row; + align-items: center; border-color: $primary; - font-size: u(1.4rem); - padding: u(0.5rem); border-radius: 3px; - @include clearfix; + border: 1px solid; + display: inline-flex; + font-size: u(1.4rem); + padding: u(.5rem); + .tooltip__trigger { + margin: 0; + } .tooltip__trigger-text { border-right: 1px solid $primary; display: table-cell; - padding-right: 0.5rem; - line-height: 1.2; + margin-right: .5rem; + padding-right: .5rem; vertical-align: middle; } .tooltip__container { - display: table-cell; - vertical-align: middle; - padding-left: 0.5rem; + height: unset; + margin: 0; + width: unset; } .list--bulleted li:last-child { @@ -514,147 +496,97 @@ } } -.cal-list__event-title { - margin: 0; -} - .cal-list__location { display: block; -} - -// Popover -.fc-popover { - background-color: $inverse; - border: 2px solid $neutral; - position: absolute; - width: u(22rem); - .fc-header { - background: $neutral; - padding: u(3px 0.5rem); - @include clearfix; - - .fc-title { - font-weight: bold; - height: auto; - line-height: u(2rem); - width: auto; - } + @include media($lg) { + font-size: 1.6rem; } +} - .fc-close { - cursor: pointer; - float: right; - @include u-icon($x, $primary, 2rem, 2rem, 100%); - } - .fc-event-container { - padding: u(0.5rem); +.cal-details { + font-size: u(1.4rem); + left: auto !important; + top: auto !important; + margin-top: u(1.5rem); + margin-left: u(-8rem); + padding: 0 !important; + text-align: left; + width: u(26rem); + white-space: initial; + + &::before { + background-image: none; + left: 20%; + top: u(-2rem) !important; + @include triangle(2rem, $primary, up); } - .fc-content { - margin-bottom: u(0.5rem); + .button--close--primary { + position: absolute; + top: 0; + right: 0; } } -.fc-clear { - clear: both; +.cal-details__date { + display: block; + padding-bottom: u(.5rem); } -// BREAKPOINT: MEDIUM -// - Navigation buttons on the left -// - Title in the center -// - View buttons on the right - -@include media($med) { - .calendar__head { - padding: u(1rem 2rem); - } +.cal-details__title { + display: block; + font-weight: bold; + line-height: 1.4; + padding-bottom: u(.5rem); +} - .fc-clear { - display: none; - } +.cal-details__summary { + display: block; + font-weight: bold; + line-height: 1.2; + padding-bottom: u(1rem); +} - .fc-center { - float: left; - text-align: center; - } +.cal-details__description { + display: block; + line-height: 1.2; + padding-bottom: u(.5rem); +} - .fc-right { - display: block; - float: right; +.cal-details__location { + display: block; + line-height: 1.2; + padding-bottom: u(.5rem); +} - .fc-button-group { - float: right; - } +.cal-details__add { + margin-top: u(1rem); +} +.fc-day:nth-child(1) { + .cal-details { + margin-left: unset; - .button { - background-position: 50% 50%; - padding-left: u(2rem); - text-indent: -9999px; - width: u(4rem); + &::before { + right: auto; + left: u(2rem); } } - - .cal-list__toggles { - float: right; - } } -// BREAKPOINT: LARGE -// - List events line up horizontally - -@include media($lg) { - .cal-list { - font-size: u(1.6rem); +.fc-day:nth-child(7) { + .cal-details { + right: 0; - p { - font-size: u(1.6rem); + &::before { + left: auto; + right: u(1rem); } - } - .cal-list--time { - .cal-list__date { - display: none; - } - } - - .cal-list--category { - .cal-list__category { - visibility: hidden; + &::after { + left: auto; + right: u(1.2rem); } } - - .cal-list__event { - padding: u(2rem); - @include clearfix; - } - - .cal-list__details { - margin-bottom: 0; - @include span-columns(10); - } - - .cal-list__add { - font-size: u(1.4rem); - @include span-columns(2); - } - - .cal-list__date { - font-weight: bold; - @include span-columns(2 of 10); - } - - .cal-list__time { - @include span-columns(1 of 10); - } - - .cal-list__info { - padding-top: 0; - @include span-columns(6 of 10); - } - - .cal-list__category { - @include span-columns(3 of 10); - } } diff --git a/fec/fec/static/scss/components/_form-styles.scss b/fec/fec/static/scss/components/_form-styles.scss index 28dbf7bd58..f6bdf52d62 100644 --- a/fec/fec/static/scss/components/_form-styles.scss +++ b/fec/fec/static/scss/components/_form-styles.scss @@ -29,7 +29,7 @@ // .select--alt { - background-color: none; + background-color: transparent; border: 2px solid $gray; color: $base; diff --git a/fec/fec/templates/base.html b/fec/fec/templates/base.html index 6930afcbd3..d3ade68024 100644 --- a/fec/fec/templates/base.html +++ b/fec/fec/templates/base.html @@ -9,7 +9,7 @@ {% include 'partials/meta-tags-preconnects.html' %} {% endif %} - {% block title %}{% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }} | FEC {% endif %}{% endblock %}{% block title_suffix %}{% endblock %} + {% block title %}{% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }} | FEC{% endif %}{% endblock %}{% block title_suffix %}{% endblock %} {% if not request.is_preview %}{% include 'partials/google-tag-manager-script.html' %}{% endif %} diff --git a/fec/fec/tests/js/calendar-helpers.js b/fec/fec/tests/js/calendar-helpers.js new file mode 100644 index 0000000000..82dbc95510 --- /dev/null +++ b/fec/fec/tests/js/calendar-helpers.js @@ -0,0 +1,134 @@ +// Common for all/most tests +import './setup.js'; +import * as sinonChai from 'sinon-chai'; +import { expect, use } from 'chai'; +use(sinonChai); +// (end common) + +import { + calendarDownload, + checkStartTime, + className, + mapCategoryDescription +} from '../../static/js/modules/calendar-helpers.js'; +import moment from 'moment'; +import ElectionSearch from '../../static/js/modules/election-search.js'; + +describe('calendarDownload', function () { + describe('should', function () { + let expectedUrl = `${window.API_LOCATION}/${window.API_VERSION}/test/path/`; + expectedUrl += `?api_key=${window.CALENDAR_DOWNLOAD_PUBLIC_API_KEY}&per_page=500`; + expectedUrl += `¶m1=val+1¶m+2=(val2)`; + + it('return expected results', function () { + expect( + calendarDownload('test/path', { param1: 'val 1', 'param 2': '(val2)' }) + ).to.equal(expectedUrl); + }); + it('handle errors as expected', function () { + // TODO + // TODO + // TODO + }); + }); +}); + +describe('checkStartTime', function () { + describe('should', function () { + it('return expected results', function () { + expect(checkStartTime({ start_date: '2000-01-01 12:34' })).to.be.true; + expect(checkStartTime({ start_date: '2000-01-01 01' })).to.be.true; + expect(checkStartTime({ start_date: '2000-01-01T23:59:59Z' })).to.be.true; + expect(checkStartTime({ start_date: '2000-01-01' })).to.be.false; + expect(checkStartTime({ start_date: new Date() })).to.be.true; + }); + it('handle errors as expected', function () { + expect(checkStartTime({ start_date: '2000-01-01 99:99' })).to.be.false; + expect(checkStartTime({ start_date: '2000-01-01 13:99' })).to.be.false; + expect(checkStartTime({ start_date: '2000-01-01 24' })).to.be.false; + expect(checkStartTime({ start_date: '2000-01-01 13:99' })).to.be.false; + }); + }); +}); + +describe('className', function () { + describe('should', function () { + it('return expected results', function () { + expect(className({ start_date: '2000-01-01', end_date: '2000-01-02' })).to.equal('fc-multi-day'); + expect(className({ start_date: '2000-01-01 00:00', end_date: '2000-01-01 01:00' })).to.equal(''); + }); + it('handle errors as expected', function () { + expect(className({})).to.be.equal(''); + expect(className({ start_date: '2000' })).to.equal(''); + expect(className({ end_date: '2000' })).to.equal('fc-multi-day'); + expect(className({ start_date: '2000', end_date: '2001' })).to.equal(''); + expect(className({ start_date: '2000-10', end_date: '2001-11' })).to.equal('fc-multi-day'); + expect(className({ start_date: '2000-01-05', end_date: '2000-01-03' })).to.equal('fc-multi-day'); + }); + }); +}); + +// getGoogleUrl is being tested in tests/js/calendar.js +// getUrl is being tested in tests/js/calendar.js + +describe('mapCategoryDescription', function () { + describe('should', function () { + it('return expected results', function () { + expect(mapCategoryDescription('Election Dates')).to.be.equal('Federal elections. These include primary, general and special elections as well as caucuses and conventions.'); + }); + it('handle errors as expected', function () { + expect(mapCategoryDescription()).to.be.undefined; + expect(mapCategoryDescription('gibberish')).to.be.undefined; + expect(mapCategoryDescription(false)).to.be.undefined; + }); + }); +}); + +// These functions are for things that used to include Moment +describe('ElectionSearch', function () { + // From modules/election-search.js + // let prev = moment(upcomingElections[0].election_date, 'YYYY-MM-DD'); + // let repl = new Date(upcomingElections[0].election_date) + // moment(upcomingElections[0].election_date, 'YYYY-MM-DD'); + describe('formatGenericElectionDate', function() { + it('replacement should go smoothly', function() { + const result = { cycle: 2020 }; + let date = moment() + .year(result.cycle) + .month('November') + .date(1); + while (date.format('E') !== '1') { + date = date.add(1, 'day'); + } + let prev = date.add(1, 'day').format('MMMM Do, YYYY'); + // ^^ THAT'S OUR GOAL ^^ + + let repl; + + const newElectionDate = new Date(result.cycle, 10, 1); + // Looking at the days of the week, we want the first Tuesday of the month. + // Problem: getDay() is local and Sunday isn't always the first day of the week, so we'll look at UTCDay + let i = 0; + while (newElectionDate.getUTCDay() != 2 && i < 10) { + newElectionDate.setDate(newElectionDate.getDate() + 1); + i++; + } + // repl.setFullYear(result.cycle); + repl = newElectionDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); + const newDate = newElectionDate.getDate(); + if (newDate == 1) + repl = repl.replace(',', 'st,'); + else if (newDate == 2) + repl = repl.replace(',', 'nd,'); + else if (newDate == 3) + repl = repl.replace(',', 'rd,'); + else + repl = repl.replace(',', 'th,'); + + expect(repl).to.equal(prev); + + }); + }); + + // expect(mapCategoryDescription()).to.be.undefined; +}); diff --git a/fec/fec/tests/js/calendar.js b/fec/fec/tests/js/calendar.js index 05a0bd916b..60040c3115 100644 --- a/fec/fec/tests/js/calendar.js +++ b/fec/fec/tests/js/calendar.js @@ -29,18 +29,14 @@ describe('calendar', function() { }); beforeEach(function() { - this.$fixture.empty().append( - '
' + - '
' + - '
' - ); + this.$fixture.empty().append('
'); this.calendar = new Calendar({ selector: '#calendar', - download: '#download', - subscribe: '#subscribe', - url: 'http://test.calendar', - exportUrl: 'http://test.calendar/export', - subscribeUrl: 'http://test.calendar/export', + selector_download: '#download', + selector_subscribe: '#subscribe', + url_base: 'http://test.calendar', + url_export: 'http://test.calendar/export', + url_subscribe: 'http://test.calendar/export', filterPanel: { $form: { on: function() {} }, // eslint-disable-line no-empty-function filterSet: { @@ -58,8 +54,8 @@ describe('calendar', function() { describe('constructor()', function() { it('memorizes options', function() { - expect(this.calendar.opts.url).to.equal('http://test.calendar'); - expect(this.calendar.opts.exportUrl).to.equal('http://test.calendar/export'); + expect(this.calendar.opts.url_base).to.equal('http://test.calendar'); + expect(this.calendar.opts.url_export).to.equal('http://test.calendar/export'); }); it('finds dom nodes', function() { @@ -209,7 +205,7 @@ describe('calendar', function() { this.calendar.handleEventClick({}, { target: target }); expect(calendarTooltip.CalendarTooltip).to.have.been.called; }); - + it('does not make a tooltip if there is one', function() { var target = '
'; this.calendar.handleEventClick({}, { target: target }); @@ -233,10 +229,7 @@ describe('calendar', function() { describe('calendar tooltip', function() { beforeEach(function() { - var dom = - '
' + - '' + - '
'; + var dom = '
'; $(document.body).append($(dom)); var $container = $('.cal-event'); var content = tooltipContent({}); @@ -271,9 +264,9 @@ describe('calendar tooltip', function() { }); describe('helpers', function() { - describe('getGoogleUrl()', function() { + describe('getGoogleUrl_moment()', function() { it('builds the correct Google url', function() { - var googleUrl = calendarHelpers.getGoogleUrl({ + var googleUrl = calendarHelpers.getGoogleUrl_moment({ title: 'the big one', summary: 'vote today', end: moment('2016-11-01'), @@ -290,36 +283,27 @@ describe('helpers', function() { }); it('builds the correct subscribe url', function() { - var subscribeUrl = calendarHelpers.getUrl('calendar', { category: 'election' }, 'sub'); - expect(subscribeUrl).to.equal('/v1/calendar/?api_key=67890&per_page=500&category=election'); - + var url_subscribe = calendarHelpers.getUrl('calendar', { category: 'election' }, 'sub'); + expect(url_subscribe).to.equal('/v1/calendar/?api_key=67890&per_page=500&category=election'); + }); }); describe('calendar.updateLinks()', function() { it('builds the correct, encoded googleSubscribe url', function() { - var subscribeUrl = calendarHelpers.getUrl('calendar-dates/export', { category: 'election' }, 'sub').toString(); - var googleSubscribe = - 'https://calendar.google.com/calendar/render?cid=' + - encodeURIComponent( - subscribeUrl - .toString() - ); - expect(googleSubscribe).to.equal('https://calendar.google.com/calendar/render?cid=%2Fv1%2Fcalendar-dates%2Fexport%2F%3Fapi_key%3D67890%26per_page%3D500%26category%3Delection') - }); - }); + var url_subscribe = calendarHelpers.getUrl('calendar-dates/export', { category: 'election' }, 'sub').toString(); + var googleSubscribe = + 'https://calendar.google.com/calendar/render?cid=' + + encodeURIComponent(url_subscribe.toString()); + expect(googleSubscribe).to.equal('https://calendar.google.com/calendar/render?cid=%2Fv1%2Fcalendar-dates%2Fexport%2F%3Fapi_key%3D67890%26per_page%3D500%26category%3Delection'); + }); + }); describe('className()', function() { it('adds a multi-day class for multi-day events', function() { - var multiEvent = { - start_date: moment('2012-11-02'), - end_date: moment('2012-11-03') - }; - var singleEvent = { - start_date: moment('2012-11-02'), - end_date: moment('2012-11-02') - }; + var multiEvent = { start_date: moment('2012-11-02'), end_date: moment('2012-11-03') }; + var singleEvent = { start_date: moment('2012-11-02'), end_date: moment('2012-11-02') }; var multiDayClass = calendarHelpers.className(multiEvent); var singleDayClass = calendarHelpers.className(singleEvent); expect(multiDayClass).to.equal('fc-multi-day'); @@ -327,3 +311,9 @@ describe('helpers', function() { }); }); }); + +describe('calendar date functions', function() { + it('should work properly', function() { + expect(true).to.be(false); + }); +}); diff --git a/fec/fec/tests/js/setup.js b/fec/fec/tests/js/setup.js index aa07620da3..2a16461542 100644 --- a/fec/fec/tests/js/setup.js +++ b/fec/fec/tests/js/setup.js @@ -1,9 +1,9 @@ -// export default function() { - // Append jQuery to `window` for use by legacy libraries - window.$ = window.jQuery = $; +// Append jQuery to `window` for use by legacy libraries +if (!window.$) window.$ = window.jQuery = $; - // Add global variables - global = Object.assign(global, { // eslint-disable-line no-global-assign +// Add global variables +/* + // global = Object.assign(global, { // eslint-disable-line no-global-assign BASE_PATH: '/', API_LOCATION: '', API_VERSION: '/v1', @@ -11,3 +11,18 @@ API_KEY_PUBLIC_CALENDAR: '67890', DEFAULT_TIME_PERIOD: '2016' }); +*/ + +let globalVars = { + BASE_PATH: '/', + API_LOCATION: '', //http://localhost:5000', + API_VERSION: 'v1', + API_KEY_PUBLIC: '12345', + API_KEY_PUBLIC_CALENDAR: '67890', + CALENDAR_DOWNLOAD_PUBLIC_API_KEY: '54321', + DEFAULT_TIME_PERIOD: '2016' +}; + +for (let n in globalVars) { + window[n] = globalVars[n]; +} diff --git a/fec/gulpfile.js b/fec/gulpfile.js index 63d114062a..bc04fff6a4 100644 --- a/fec/gulpfile.js +++ b/fec/gulpfile.js @@ -29,7 +29,7 @@ gulp.task('_css-compile', function() { gulp .src('./fec/static/scss/*.scss') // compiles sass - .pipe(sass().on('error', sass.logError)) + .pipe(sass.sync().on('error', sass.logError)) // minifies css .pipe(csso()) // sourcemaps for local to back-trace source of scss @@ -82,7 +82,7 @@ gulp.task('_widgets-compile-sass', function() { gulp .src('./fec/static/scss/widgets/*.scss') // compiles sass - .pipe(sass().on('error', sass.logError)) + .pipe(sass.sync().on('error', sass.logError)) // minifies css .pipe(csso()) // sourcemaps for local to back-trace source of scss diff --git a/fec/webpack.config.cjs b/fec/webpack.config.cjs index 8a77901f99..b48bea3a48 100644 --- a/fec/webpack.config.cjs +++ b/fec/webpack.config.cjs @@ -183,7 +183,8 @@ module.exports = [ }, module: { rules: [ - { test: /\.hbs/, use: ['handlebars-loader'] } + { test: /\.hbs/, use: ['handlebars-loader'] }, + { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, resolve: { @@ -225,7 +226,7 @@ module.exports = [ name: 'draftail', entry: { draftail: `${js}/draftail/App.js` }, output: { - clean: mode === 'development' ? true : undefined, // Deploying to Prod doesn't need this at all + // clean: mode === 'development' ? true : undefined, // Deploying to Prod doesn't need this at all filename: '[name]-[contenthash].js', path: path.resolve(__dirname, './dist/fec/static/js') }, diff --git a/package-lock.json b/package-lock.json index 19c63e2895..f3b6ec31fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,9 +27,12 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@fullcalendar/core": "^5.11.5", - "@fullcalendar/daygrid": "^5.11.5", - "@fullcalendar/list": "^5.11.5", + "@fullcalendar/bootstrap5": "^7.0.0-beta.4", + "@fullcalendar/core": "^7.0.0-beta.4", + "@fullcalendar/daygrid": "^7.0.0-beta.4", + "@fullcalendar/interaction": "^7.0.0-beta.4", + "@fullcalendar/list": "^7.0.0-beta.4", + "@fullcalendar/timegrid": "^7.0.0-beta.4", "a11y-dialog": "^8.0.4", "aria-accordion": "^1.1.0", "babel-jest": "^29.7.0", @@ -48,6 +51,7 @@ "component-sticky": "^1.1.0", "concurrently": "^8.2.2", "corejs-typeahead": "^1.3.4", + "css-loader": "^7.1.2", "d3-array": "^3.2.4", "d3-geo": "^3.1.0", "d3-scale": "^4.0.2", @@ -63,7 +67,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-react": "^7.34.1", "eslint-plugin-simple-import-sort": "^12.0.0", - "fullcalendar": "^3.3.1", "glossary-panel": "^1.0.0", "graceful-fs": "^4.2.10", "gulp": "^4.0.2", @@ -111,6 +114,7 @@ "sinon-chai": "^3.7.0", "sprintf-js": "^1.1.3", "stream-browserify": "^3.0.0", + "style-loader": "^4.0.0", "topojson-client": "^3.1.0", "underscore": "^1.13.6", "urijs": "^1.19.11", @@ -2200,48 +2204,68 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fullcalendar/common": { - "version": "5.11.5", - "resolved": "https://registry.npmjs.org/@fullcalendar/common/-/common-5.11.5.tgz", - "integrity": "sha512-3iAYiUbHXhjSVXnYWz27Od2cslztUPsOwiwKlfGvQxBixv2Kl6a8IPwaijKFYJHXdwYmfPoEgK7rvqAGVoIYwA==", + "node_modules/@fullcalendar/bootstrap5": { + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/bootstrap5/-/bootstrap5-7.0.0-beta.4.tgz", + "integrity": "sha512-z1ZX73Ix6x/6nLxkBKnRzAt0Xc/oPLqucjfcghL3UzQi4dOTAqvG8Pic+bSCDqw0EYEBCxBY6N2kBKYguGcg2w==", "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" + "peerDependencies": { + "@fullcalendar/core": "7.0.0-beta.4", + "bootstrap": ">=5.2.0 <6.0.0" } }, "node_modules/@fullcalendar/core": { - "version": "5.11.5", - "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-5.11.5.tgz", - "integrity": "sha512-M/WQuq1+uUHxFDEIu2ib/aaPZ70VsRk2ITECo/WCLSLTVWcHPXwEg83reyP3G8JrMM4gRL4vScEHhX0U5aoNSw==", + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-7.0.0-beta.4.tgz", + "integrity": "sha512-fHVQ+mGjHuDImaOlA5Sdh8yioc9vOJNMqN3J91zJlm8HNsUWwbDNvByWnVX+WhcysluUtvRTWf3sQXzmW5eCLA==", "dev": true, "license": "MIT", "dependencies": { - "@fullcalendar/common": "~5.11.5", - "preact": "~10.12.1", - "tslib": "^2.1.0" + "preact": "~10.23.1" } }, "node_modules/@fullcalendar/daygrid": { - "version": "5.11.5", - "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-5.11.5.tgz", - "integrity": "sha512-hMpq0U3Nucys2jDD+crbkJCr+tVt3fDw04OE3fbpisuzqtrHxIzRmnUOdbWUjJQyToAAkt7UVUQ9E7hYdmvyGA==", + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-7.0.0-beta.4.tgz", + "integrity": "sha512-Eon7XhXygtvNO+VPSF65TfmchtQpik2oG2XejbUWsZxWgiONLoBnmxp2GHGWxeUCFF3lcT4PnzZNAlCc8aYGjw==", "dev": true, "license": "MIT", - "dependencies": { - "@fullcalendar/common": "~5.11.5", - "tslib": "^2.1.0" + "peerDependencies": { + "@fullcalendar/core": "7.0.0-beta.4" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-7.0.0-beta.4.tgz", + "integrity": "sha512-NuaSi5nKPtuhvNk/1te/WeTa2d9yqjI52XDS3o4nPl15Fn6SjxJefVlIoRqmF0OKAkpPbc6yTUEFCzmExjmj+g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "7.0.0-beta.4" } }, "node_modules/@fullcalendar/list": { - "version": "5.11.5", - "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-5.11.5.tgz", - "integrity": "sha512-ZYMPT4CVt9tIYkVVNx7CKkB2xc+n9L56+vgXkurptgYgPsacXYkcpF/1Hy/B5LKlg0ROEF9Qfftjow8xjANqaA==", + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-7.0.0-beta.4.tgz", + "integrity": "sha512-pp3VcUURvmXnloCuHSEyE1Z34H8QQ1NFXBJ+XE91tKK1DDLqRlKEJEtQYSX/yQTnf0t8d+y8lmSgISPVARMXEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "7.0.0-beta.4" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "7.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-7.0.0-beta.4.tgz", + "integrity": "sha512-Sz7iNJnlXgSe1OvGp3Ir9SWt+jLIRBTsztStSNG6qhd1AI3zAtDnVYIrOXV++WaoOZKBA5VutCrXdvPfuVBWrA==", "dev": true, "license": "MIT", "dependencies": { - "@fullcalendar/common": "~5.11.5", - "tslib": "^2.1.0" + "@fullcalendar/daygrid": "7.0.0-beta.4" + }, + "peerDependencies": { + "@fullcalendar/core": "7.0.0-beta.4" } }, "node_modules/@gulp-sourcemaps/identity-map": { @@ -2855,6 +2879,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", @@ -6359,6 +6395,27 @@ "dev": true, "license": "ISC" }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peer": true, + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6638,9 +6695,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -7937,6 +7994,55 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -9319,9 +9425,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.78", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.78.tgz", - "integrity": "sha512-UmwIt7HRKN1rsJfddG5UG7rCTCTAKoS9JeOy/R0zSenAyaZ8SU3RuXlwcratxhdxGRNpk03iq8O7BA3W7ibLVw==", + "version": "1.5.79", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.79.tgz", + "integrity": "sha512-nYOxJNxQ9Om4EC88BE4pPoNI8xwSFf8pU/BAeOl4Hh/b/i6V4biTAzwV7pXi3ARKeoYO5JZKMIXTryXSVer5RA==", "dev": true, "license": "ISC" }, @@ -11655,17 +11761,6 @@ "node": ">= 4.0" } }, - "node_modules/fullcalendar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-3.3.1.tgz", - "integrity": "sha512-YmJ7CrwJJKRPchZcIfuId+qKAzCEwvAPQJAhEUdtiTytlVcmx0NDnGgNmrC6ZYxrwV9Ulqq5dtftOJdXQjfsJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jquery": "2 - 3", - "moment": "^2.9.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -13811,6 +13906,19 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -19507,6 +19615,97 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/postcss-selector-parser": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", @@ -19521,10 +19720,17 @@ "node": ">=4" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/preact": { - "version": "10.12.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", - "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "version": "10.23.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.23.2.tgz", + "integrity": "sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA==", "dev": true, "license": "MIT", "funding": { @@ -22800,6 +23006,23 @@ "node": ">=0.10.0" } }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, "node_modules/subarg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", @@ -24095,9 +24318,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -24116,7 +24339,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index fa7fc7201b..c0edf661e2 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,12 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.24.7", "@babel/preset-react": "^7.24.7", - "@fullcalendar/core": "^5.11.5", - "@fullcalendar/daygrid": "^5.11.5", - "@fullcalendar/list": "^5.11.5", + "@fullcalendar/bootstrap5": "^7.0.0-beta.4", + "@fullcalendar/core": "^7.0.0-beta.4", + "@fullcalendar/daygrid": "^7.0.0-beta.4", + "@fullcalendar/interaction": "^7.0.0-beta.4", + "@fullcalendar/list": "^7.0.0-beta.4", + "@fullcalendar/timegrid": "^7.0.0-beta.4", "a11y-dialog": "^8.0.4", "aria-accordion": "^1.1.0", "babel-jest": "^29.7.0", @@ -65,6 +68,7 @@ "component-sticky": "^1.1.0", "concurrently": "^8.2.2", "corejs-typeahead": "^1.3.4", + "css-loader": "^7.1.2", "d3-array": "^3.2.4", "d3-geo": "^3.1.0", "d3-scale": "^4.0.2", @@ -80,7 +84,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-react": "^7.34.1", "eslint-plugin-simple-import-sort": "^12.0.0", - "fullcalendar": "^3.3.1", "glossary-panel": "^1.0.0", "graceful-fs": "^4.2.10", "gulp": "^4.0.2", @@ -128,6 +131,7 @@ "sinon-chai": "^3.7.0", "sprintf-js": "^1.1.3", "stream-browserify": "^3.0.0", + "style-loader": "^4.0.0", "topojson-client": "^3.1.0", "underscore": "^1.13.6", "urijs": "^1.19.11",