diff --git a/_pages/payment-accuracy/assets/js/uswds.js b/_pages/payment-accuracy/assets/js/uswds.js new file mode 100644 index 000000000..6565cd8c7 --- /dev/null +++ b/_pages/payment-accuracy/assets/js/uswds.js @@ -0,0 +1,6365 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i'], + 191: ['/', '?'], + 192: ['`', '~'], + 219: ['[', '{'], + 220: ['\\', '|'], + 221: [']', '}'], + 222: ["'", '"'], + 224: 'Meta', + 225: 'AltGraph', + 246: 'Attn', + 247: 'CrSel', + 248: 'ExSel', + 249: 'EraseEof', + 250: 'Play', + 251: 'ZoomOut' + } + }; + + // Function keys (F1-24). + var i; + for (i = 1; i < 25; i++) { + keyboardeventKeyPolyfill.keys[111 + i] = 'F' + i; + } + + // Printable ASCII characters. + var letter = ''; + for (i = 65; i < 91; i++) { + letter = String.fromCharCode(i); + keyboardeventKeyPolyfill.keys[i] = [letter.toLowerCase(), letter.toUpperCase()]; + } + function polyfill() { + if (!('KeyboardEvent' in window) || 'key' in KeyboardEvent.prototype) { + return false; + } + + // Polyfill `key` on `KeyboardEvent`. + var proto = { + get: function (x) { + var key = keyboardeventKeyPolyfill.keys[this.which || this.keyCode]; + if (Array.isArray(key)) { + key = key[+this.shiftKey]; + } + return key; + } + }; + Object.defineProperty(KeyboardEvent.prototype, 'key', proto); + return proto; + } + if (typeof define === 'function' && define.amd) { + define('keyboardevent-key-polyfill', keyboardeventKeyPolyfill); + } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { + module.exports = keyboardeventKeyPolyfill; + } else if (window) { + window.keyboardeventKeyPolyfill = keyboardeventKeyPolyfill; + } +})(); + +},{}],5:[function(require,module,exports){ +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ + +'use strict'; + +/* eslint-disable no-unused-vars */ +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + return Object(val); +} +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== 'abcdefghijklmnopqrst') { + return false; + } + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } +} +module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + return to; +}; + +},{}],6:[function(require,module,exports){ +"use strict"; + +const assign = require('object-assign'); +const delegate = require('../delegate'); +const delegateAll = require('../delegateAll'); +const DELEGATE_PATTERN = /^(.+):delegate\((.+)\)$/; +const SPACE = ' '; +const getListeners = function (type, handler) { + var match = type.match(DELEGATE_PATTERN); + var selector; + if (match) { + type = match[1]; + selector = match[2]; + } + var options; + if (typeof handler === 'object') { + options = { + capture: popKey(handler, 'capture'), + passive: popKey(handler, 'passive') + }; + } + var listener = { + selector: selector, + delegate: typeof handler === 'object' ? delegateAll(handler) : selector ? delegate(selector, handler) : handler, + options: options + }; + if (type.indexOf(SPACE) > -1) { + return type.split(SPACE).map(function (_type) { + return assign({ + type: _type + }, listener); + }); + } else { + listener.type = type; + return [listener]; + } +}; +var popKey = function (obj, key) { + var value = obj[key]; + delete obj[key]; + return value; +}; +module.exports = function behavior(events, props) { + const listeners = Object.keys(events).reduce(function (memo, type) { + var listeners = getListeners(type, events[type]); + return memo.concat(listeners); + }, []); + return assign({ + add: function addBehavior(element) { + listeners.forEach(function (listener) { + element.addEventListener(listener.type, listener.delegate, listener.options); + }); + }, + remove: function removeBehavior(element) { + listeners.forEach(function (listener) { + element.removeEventListener(listener.type, listener.delegate, listener.options); + }); + } + }, props); +}; + +},{"../delegate":8,"../delegateAll":9,"object-assign":5}],7:[function(require,module,exports){ +"use strict"; + +module.exports = function compose(functions) { + return function (e) { + return functions.some(function (fn) { + return fn.call(this, e) === false; + }, this); + }; +}; + +},{}],8:[function(require,module,exports){ +"use strict"; + +// polyfill Element.prototype.closest +require('element-closest'); +module.exports = function delegate(selector, fn) { + return function delegation(event) { + var target = event.target.closest(selector); + if (target) { + return fn.call(target, event); + } + }; +}; + +},{"element-closest":3}],9:[function(require,module,exports){ +"use strict"; + +const delegate = require('../delegate'); +const compose = require('../compose'); +const SPLAT = '*'; +module.exports = function delegateAll(selectors) { + const keys = Object.keys(selectors); + + // XXX optimization: if there is only one handler and it applies to + // all elements (the "*" CSS selector), then just return that + // handler + if (keys.length === 1 && keys[0] === SPLAT) { + return selectors[SPLAT]; + } + const delegates = keys.reduce(function (memo, selector) { + memo.push(delegate(selector, selectors[selector])); + return memo; + }, []); + return compose(delegates); +}; + +},{"../compose":7,"../delegate":8}],10:[function(require,module,exports){ +"use strict"; + +module.exports = function ignore(element, fn) { + return function ignorance(e) { + if (element !== e.target && !element.contains(e.target)) { + return fn.call(this, e); + } + }; +}; + +},{}],11:[function(require,module,exports){ +"use strict"; + +module.exports = { + behavior: require('./behavior'), + delegate: require('./delegate'), + delegateAll: require('./delegateAll'), + ignore: require('./ignore'), + keymap: require('./keymap') +}; + +},{"./behavior":6,"./delegate":8,"./delegateAll":9,"./ignore":10,"./keymap":12}],12:[function(require,module,exports){ +"use strict"; + +require('keyboardevent-key-polyfill'); + +// these are the only relevant modifiers supported on all platforms, +// according to MDN: +// +const MODIFIERS = { + 'Alt': 'altKey', + 'Control': 'ctrlKey', + 'Ctrl': 'ctrlKey', + 'Shift': 'shiftKey' +}; +const MODIFIER_SEPARATOR = '+'; +const getEventKey = function (event, hasModifiers) { + var key = event.key; + if (hasModifiers) { + for (var modifier in MODIFIERS) { + if (event[MODIFIERS[modifier]] === true) { + key = [modifier, key].join(MODIFIER_SEPARATOR); + } + } + } + return key; +}; +module.exports = function keymap(keys) { + const hasModifiers = Object.keys(keys).some(function (key) { + return key.indexOf(MODIFIER_SEPARATOR) > -1; + }); + return function (event) { + var key = getEventKey(event, hasModifiers); + return [key, key.toLowerCase()].reduce(function (result, _key) { + if (_key in keys) { + result = keys[key].call(this, event); + } + return result; + }, undefined); + }; +}; +module.exports.MODIFIERS = MODIFIERS; + +},{"keyboardevent-key-polyfill":4}],13:[function(require,module,exports){ +"use strict"; + +module.exports = function once(listener, options) { + var wrapped = function wrappedOnce(e) { + e.currentTarget.removeEventListener(e.type, wrapped, options); + return listener.call(this, e); + }; + return wrapped; +}; + +},{}],14:[function(require,module,exports){ +'use strict'; + +var RE_TRIM = /(^\s+)|(\s+$)/g; +var RE_SPLIT = /\s+/; +var trim = String.prototype.trim ? function (str) { + return str.trim(); +} : function (str) { + return str.replace(RE_TRIM, ''); +}; +var queryById = function (id) { + return this.querySelector('[id="' + id.replace(/"/g, '\\"') + '"]'); +}; +module.exports = function resolveIds(ids, doc) { + if (typeof ids !== 'string') { + throw new Error('Expected a string but got ' + typeof ids); + } + if (!doc) { + doc = window.document; + } + var getElementById = doc.getElementById ? doc.getElementById.bind(doc) : queryById.bind(doc); + ids = trim(ids).split(RE_SPLIT); + + // XXX we can short-circuit here because trimming and splitting a + // string of just whitespace produces an array containing a single, + // empty string + if (ids.length === 1 && ids[0] === '') { + return []; + } + return ids.map(function (id) { + var el = getElementById(id); + if (!el) { + throw new Error('no element with id: "' + id + '"'); + } + return el; + }); +}; + +},{}],15:[function(require,module,exports){ +"use strict"; + +const select = require("../utils/select"); +const behavior = require("../utils/behavior"); +const toggle = require("../utils/toggle"); +const isElementInViewport = require("../utils/is-in-viewport"); +const { + CLICK +} = require("../events"); +const { + prefix: PREFIX +} = require("../config"); +const ACCORDION = `.${PREFIX}-accordion, .${PREFIX}-accordion--bordered`; +const BUTTON = `.${PREFIX}-accordion__button[aria-controls]`; +const EXPANDED = "aria-expanded"; +const MULTISELECTABLE = "aria-multiselectable"; + +/** + * Get an Array of button elements belonging directly to the given + * accordion element. + * @param {HTMLElement} accordion + * @return {array} + */ +const getAccordionButtons = accordion => { + const buttons = select(BUTTON, accordion); + return buttons.filter(button => button.closest(ACCORDION) === accordion); +}; + +/** + * Toggle a button's "pressed" state, optionally providing a target + * state. + * + * @param {HTMLButtonElement} button + * @param {boolean?} expanded If no state is provided, the current + * state will be toggled (from false to true, and vice-versa). + * @return {boolean} the resulting state + */ +const toggleButton = (button, expanded) => { + const accordion = button.closest(ACCORDION); + let safeExpanded = expanded; + if (!accordion) { + throw new Error(`${BUTTON} is missing outer ${ACCORDION}`); + } + safeExpanded = toggle(button, expanded); + + // XXX multiselectable is opt-in, to preserve legacy behavior + const multiselectable = accordion.getAttribute(MULTISELECTABLE) === "true"; + if (safeExpanded && !multiselectable) { + getAccordionButtons(accordion).forEach(other => { + if (other !== button) { + toggle(other, false); + } + }); + } +}; + +/** + * @param {HTMLButtonElement} button + * @return {boolean} true + */ +const showButton = button => toggleButton(button, true); + +/** + * @param {HTMLButtonElement} button + * @return {boolean} false + */ +const hideButton = button => toggleButton(button, false); +const accordion = behavior({ + [CLICK]: { + [BUTTON](event) { + event.preventDefault(); + toggleButton(this); + if (this.getAttribute(EXPANDED) === "true") { + // We were just expanded, but if another accordion was also just + // collapsed, we may no longer be in the viewport. This ensures + // that we are still visible, so the user isn't confused. + if (!isElementInViewport(this)) this.scrollIntoView(); + } + } + } +}, { + init(root) { + select(BUTTON, root).forEach(button => { + const expanded = button.getAttribute(EXPANDED) === "true"; + toggleButton(button, expanded); + }); + }, + ACCORDION, + BUTTON, + show: showButton, + hide: hideButton, + toggle: toggleButton, + getButtons: getAccordionButtons +}); +module.exports = accordion; + +},{"../config":34,"../events":35,"../utils/behavior":43,"../utils/is-in-viewport":45,"../utils/select":49,"../utils/toggle":52}],16:[function(require,module,exports){ +"use strict"; + +const behavior = require("../utils/behavior"); +const { + CLICK +} = require("../events"); +const { + prefix: PREFIX +} = require("../config"); +const HEADER = `.${PREFIX}-banner__header`; +const EXPANDED_CLASS = `${PREFIX}-banner__header--expanded`; +const toggleBanner = function toggleEl(event) { + event.preventDefault(); + this.closest(HEADER).classList.toggle(EXPANDED_CLASS); +}; +module.exports = behavior({ + [CLICK]: { + [`${HEADER} [aria-controls]`]: toggleBanner + } +}); + +},{"../config":34,"../events":35,"../utils/behavior":43}],17:[function(require,module,exports){ +"use strict"; + +const select = require("../utils/select"); +const behavior = require("../utils/behavior"); +const { + prefix: PREFIX +} = require("../config"); +const CHARACTER_COUNT = `.${PREFIX}-character-count`; +const INPUT = `.${PREFIX}-character-count__field`; +const MESSAGE = `.${PREFIX}-character-count__message`; +const VALIDATION_MESSAGE = "The content is too long."; +const MESSAGE_INVALID_CLASS = `${PREFIX}-character-count__message--invalid`; + +/** + * The elements within the character count. + * @typedef {Object} CharacterCountElements + * @property {HTMLDivElement} characterCountEl + * @property {HTMLSpanElement} messageEl + */ + +/** + * Returns the root and message element + * for an character count input + * + * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element + * @returns {CharacterCountElements} elements The root and message element. + */ +const getCharacterCountElements = inputEl => { + const characterCountEl = inputEl.closest(CHARACTER_COUNT); + if (!characterCountEl) { + throw new Error(`${INPUT} is missing outer ${CHARACTER_COUNT}`); + } + const messageEl = characterCountEl.querySelector(MESSAGE); + if (!messageEl) { + throw new Error(`${CHARACTER_COUNT} is missing inner ${MESSAGE}`); + } + return { + characterCountEl, + messageEl + }; +}; + +/** + * Update the character count component + * + * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element + */ +const updateCountMessage = inputEl => { + const { + characterCountEl, + messageEl + } = getCharacterCountElements(inputEl); + const maxlength = parseInt(characterCountEl.getAttribute("data-maxlength"), 10); + if (!maxlength) return; + let newMessage = ""; + const currentLength = inputEl.value.length; + const isOverLimit = currentLength && currentLength > maxlength; + if (currentLength === 0) { + newMessage = `${maxlength} characters allowed`; + } else { + const difference = Math.abs(maxlength - currentLength); + const characters = `character${difference === 1 ? "" : "s"}`; + const guidance = isOverLimit ? "over limit" : "left"; + newMessage = `${difference} ${characters} ${guidance}`; + } + messageEl.classList.toggle(MESSAGE_INVALID_CLASS, isOverLimit); + messageEl.textContent = newMessage; + if (isOverLimit && !inputEl.validationMessage) { + inputEl.setCustomValidity(VALIDATION_MESSAGE); + } + if (!isOverLimit && inputEl.validationMessage === VALIDATION_MESSAGE) { + inputEl.setCustomValidity(""); + } +}; + +/** + * Setup the character count component + * + * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element + */ +const setupAttributes = inputEl => { + const { + characterCountEl + } = getCharacterCountElements(inputEl); + const maxlength = inputEl.getAttribute("maxlength"); + if (!maxlength) return; + inputEl.removeAttribute("maxlength"); + characterCountEl.setAttribute("data-maxlength", maxlength); +}; +const characterCount = behavior({ + input: { + [INPUT]() { + updateCountMessage(this); + } + } +}, { + init(root) { + select(INPUT, root).forEach(input => { + setupAttributes(input); + updateCountMessage(input); + }); + }, + MESSAGE_INVALID_CLASS, + VALIDATION_MESSAGE +}); +module.exports = characterCount; + +},{"../config":34,"../utils/behavior":43,"../utils/select":49}],18:[function(require,module,exports){ +"use strict"; + +const keymap = require("receptor/keymap"); +const select = require("../utils/select"); +const behavior = require("../utils/behavior"); +const Sanitizer = require("../utils/sanitizer"); +const { + prefix: PREFIX +} = require("../config"); +const { + CLICK +} = require("../events"); +const COMBO_BOX_CLASS = `${PREFIX}-combo-box`; +const COMBO_BOX_PRISTINE_CLASS = `${COMBO_BOX_CLASS}--pristine`; +const SELECT_CLASS = `${COMBO_BOX_CLASS}__select`; +const INPUT_CLASS = `${COMBO_BOX_CLASS}__input`; +const CLEAR_INPUT_BUTTON_CLASS = `${COMBO_BOX_CLASS}__clear-input`; +const CLEAR_INPUT_BUTTON_WRAPPER_CLASS = `${CLEAR_INPUT_BUTTON_CLASS}__wrapper`; +const INPUT_BUTTON_SEPARATOR_CLASS = `${COMBO_BOX_CLASS}__input-button-separator`; +const TOGGLE_LIST_BUTTON_CLASS = `${COMBO_BOX_CLASS}__toggle-list`; +const TOGGLE_LIST_BUTTON_WRAPPER_CLASS = `${TOGGLE_LIST_BUTTON_CLASS}__wrapper`; +const LIST_CLASS = `${COMBO_BOX_CLASS}__list`; +const LIST_OPTION_CLASS = `${COMBO_BOX_CLASS}__list-option`; +const LIST_OPTION_FOCUSED_CLASS = `${LIST_OPTION_CLASS}--focused`; +const LIST_OPTION_SELECTED_CLASS = `${LIST_OPTION_CLASS}--selected`; +const STATUS_CLASS = `${COMBO_BOX_CLASS}__status`; +const COMBO_BOX = `.${COMBO_BOX_CLASS}`; +const SELECT = `.${SELECT_CLASS}`; +const INPUT = `.${INPUT_CLASS}`; +const CLEAR_INPUT_BUTTON = `.${CLEAR_INPUT_BUTTON_CLASS}`; +const TOGGLE_LIST_BUTTON = `.${TOGGLE_LIST_BUTTON_CLASS}`; +const LIST = `.${LIST_CLASS}`; +const LIST_OPTION = `.${LIST_OPTION_CLASS}`; +const LIST_OPTION_FOCUSED = `.${LIST_OPTION_FOCUSED_CLASS}`; +const LIST_OPTION_SELECTED = `.${LIST_OPTION_SELECTED_CLASS}`; +const STATUS = `.${STATUS_CLASS}`; +const DEFAULT_FILTER = ".*{{query}}.*"; +const noop = () => {}; + +/** + * set the value of the element and dispatch a change event + * + * @param {HTMLInputElement|HTMLSelectElement} el The element to update + * @param {string} value The new value of the element + */ +const changeElementValue = function (el) { + let value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; + const elementToChange = el; + elementToChange.value = value; + const event = new CustomEvent("change", { + bubbles: true, + cancelable: true, + detail: { + value + } + }); + elementToChange.dispatchEvent(event); +}; + +/** + * The elements within the combo box. + * @typedef {Object} ComboBoxContext + * @property {HTMLElement} comboBoxEl + * @property {HTMLSelectElement} selectEl + * @property {HTMLInputElement} inputEl + * @property {HTMLUListElement} listEl + * @property {HTMLDivElement} statusEl + * @property {HTMLLIElement} focusedOptionEl + * @property {HTMLLIElement} selectedOptionEl + * @property {HTMLButtonElement} toggleListBtnEl + * @property {HTMLButtonElement} clearInputBtnEl + * @property {boolean} isPristine + * @property {boolean} disableFiltering + */ + +/** + * Get an object of elements belonging directly to the given + * combo box component. + * + * @param {HTMLElement} el the element within the combo box + * @returns {ComboBoxContext} elements + */ +const getComboBoxContext = el => { + const comboBoxEl = el.closest(COMBO_BOX); + if (!comboBoxEl) { + throw new Error(`Element is missing outer ${COMBO_BOX}`); + } + const selectEl = comboBoxEl.querySelector(SELECT); + const inputEl = comboBoxEl.querySelector(INPUT); + const listEl = comboBoxEl.querySelector(LIST); + const statusEl = comboBoxEl.querySelector(STATUS); + const focusedOptionEl = comboBoxEl.querySelector(LIST_OPTION_FOCUSED); + const selectedOptionEl = comboBoxEl.querySelector(LIST_OPTION_SELECTED); + const toggleListBtnEl = comboBoxEl.querySelector(TOGGLE_LIST_BUTTON); + const clearInputBtnEl = comboBoxEl.querySelector(CLEAR_INPUT_BUTTON); + const isPristine = comboBoxEl.classList.contains(COMBO_BOX_PRISTINE_CLASS); + const disableFiltering = comboBoxEl.dataset.disableFiltering === "true"; + return { + comboBoxEl, + selectEl, + inputEl, + listEl, + statusEl, + focusedOptionEl, + selectedOptionEl, + toggleListBtnEl, + clearInputBtnEl, + isPristine, + disableFiltering + }; +}; + +/** + * Disable the combo-box component + * + * @param {HTMLInputElement} el An element within the combo box component + */ +const disable = el => { + const { + inputEl, + toggleListBtnEl, + clearInputBtnEl + } = getComboBoxContext(el); + clearInputBtnEl.hidden = true; + clearInputBtnEl.disabled = true; + toggleListBtnEl.disabled = true; + inputEl.disabled = true; +}; + +/** + * Enable the combo-box component + * + * @param {HTMLInputElement} el An element within the combo box component + */ +const enable = el => { + const { + inputEl, + toggleListBtnEl, + clearInputBtnEl + } = getComboBoxContext(el); + clearInputBtnEl.hidden = false; + clearInputBtnEl.disabled = false; + toggleListBtnEl.disabled = false; + inputEl.disabled = false; +}; + +/** + * Enhance a select element into a combo box component. + * + * @param {HTMLElement} _comboBoxEl The initial element of the combo box component + */ +const enhanceComboBox = _comboBoxEl => { + const comboBoxEl = _comboBoxEl.closest(COMBO_BOX); + if (comboBoxEl.dataset.enhanced) return; + const selectEl = comboBoxEl.querySelector("select"); + if (!selectEl) { + throw new Error(`${COMBO_BOX} is missing inner select`); + } + const selectId = selectEl.id; + const selectLabel = document.querySelector(`label[for="${selectId}"]`); + const listId = `${selectId}--list`; + const listIdLabel = `${selectId}-label`; + const assistiveHintID = `${selectId}--assistiveHint`; + const additionalAttributes = []; + const { + defaultValue + } = comboBoxEl.dataset; + const { + placeholder + } = comboBoxEl.dataset; + let selectedOption; + if (placeholder) { + additionalAttributes.push({ + placeholder + }); + } + if (defaultValue) { + for (let i = 0, len = selectEl.options.length; i < len; i += 1) { + const optionEl = selectEl.options[i]; + if (optionEl.value === defaultValue) { + selectedOption = optionEl; + break; + } + } + } + + /** + * Throw error if combobox is missing a label or label is missing + * `for` attribute. Otherwise, set the ID to match the
    aria-labelledby + */ + if (!selectLabel || !selectLabel.matches(`label[for="${selectId}"]`)) { + throw new Error(`${COMBO_BOX} for ${selectId} is either missing a label or a "for" attribute`); + } else { + selectLabel.setAttribute("id", listIdLabel); + } + selectLabel.setAttribute("id", listIdLabel); + selectEl.setAttribute("aria-hidden", "true"); + selectEl.setAttribute("tabindex", "-1"); + selectEl.classList.add("usa-sr-only", SELECT_CLASS); + selectEl.id = ""; + selectEl.value = ""; + ["required", "aria-label", "aria-labelledby"].forEach(name => { + if (selectEl.hasAttribute(name)) { + const value = selectEl.getAttribute(name); + additionalAttributes.push({ + [name]: value + }); + selectEl.removeAttribute(name); + } + }); + + // sanitize doesn't like functions in template literals + const input = document.createElement("input"); + input.setAttribute("id", selectId); + input.setAttribute("aria-owns", listId); + input.setAttribute("aria-controls", listId); + input.setAttribute("aria-autocomplete", "list"); + input.setAttribute("aria-describedby", assistiveHintID); + input.setAttribute("aria-expanded", "false"); + input.setAttribute("autocapitalize", "off"); + input.setAttribute("autocomplete", "off"); + input.setAttribute("class", INPUT_CLASS); + input.setAttribute("type", "text"); + input.setAttribute("role", "combobox"); + additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => { + const value = Sanitizer.escapeHTML`${attr[key]}`; + input.setAttribute(key, value); + })); + comboBoxEl.insertAdjacentElement("beforeend", input); + comboBoxEl.insertAdjacentHTML("beforeend", Sanitizer.escapeHTML` + + + +   + + + + +
    + + When autocomplete results are available use up and down arrows to review and enter to select. + Touch device users, explore by touch or with swipe gestures. + `); + if (selectedOption) { + const { + inputEl + } = getComboBoxContext(comboBoxEl); + changeElementValue(selectEl, selectedOption.value); + changeElementValue(inputEl, selectedOption.text); + comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); + } + if (selectEl.disabled) { + disable(comboBoxEl); + selectEl.disabled = false; + } + comboBoxEl.dataset.enhanced = "true"; +}; + +/** + * Manage the focused element within the list options when + * navigating via keyboard. + * + * @param {HTMLElement} el An anchor element within the combo box component + * @param {HTMLElement} nextEl An element within the combo box component + * @param {Object} options options + * @param {boolean} options.skipFocus skip focus of highlighted item + * @param {boolean} options.preventScroll should skip procedure to scroll to element + */ +const highlightOption = function (el, nextEl) { + let { + skipFocus, + preventScroll + } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + const { + inputEl, + listEl, + focusedOptionEl + } = getComboBoxContext(el); + if (focusedOptionEl) { + focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS); + focusedOptionEl.setAttribute("tabIndex", "-1"); + } + if (nextEl) { + inputEl.setAttribute("aria-activedescendant", nextEl.id); + nextEl.setAttribute("tabIndex", "0"); + nextEl.classList.add(LIST_OPTION_FOCUSED_CLASS); + if (!preventScroll) { + const optionBottom = nextEl.offsetTop + nextEl.offsetHeight; + const currentBottom = listEl.scrollTop + listEl.offsetHeight; + if (optionBottom > currentBottom) { + listEl.scrollTop = optionBottom - listEl.offsetHeight; + } + if (nextEl.offsetTop < listEl.scrollTop) { + listEl.scrollTop = nextEl.offsetTop; + } + } + if (!skipFocus) { + nextEl.focus({ + preventScroll + }); + } + } else { + inputEl.setAttribute("aria-activedescendant", ""); + inputEl.focus(); + } +}; + +/** + * Generate a dynamic regular expression based off of a replaceable and possibly filtered value. + * + * @param {string} el An element within the combo box component + * @param {string} query The value to use in the regular expression + * @param {object} extras An object of regular expressions to replace and filter the query + */ +const generateDynamicRegExp = function (filter) { + let query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; + let extras = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + const escapeRegExp = text => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + let find = filter.replace(/{{(.*?)}}/g, (m, $1) => { + const key = $1.trim(); + const queryFilter = extras[key]; + if (key !== "query" && queryFilter) { + const matcher = new RegExp(queryFilter, "i"); + const matches = query.match(matcher); + if (matches) { + return escapeRegExp(matches[1]); + } + return ""; + } + return escapeRegExp(query); + }); + find = `^(?:${find})$`; + return new RegExp(find, "i"); +}; + +/** + * Display the option list of a combo box component. + * + * @param {HTMLElement} el An element within the combo box component + */ +const displayList = el => { + const { + comboBoxEl, + selectEl, + inputEl, + listEl, + statusEl, + isPristine, + disableFiltering + } = getComboBoxContext(el); + let selectedItemId; + let firstFoundId; + const listOptionBaseId = `${listEl.id}--option-`; + const inputValue = (inputEl.value || "").toLowerCase(); + const filter = comboBoxEl.dataset.filter || DEFAULT_FILTER; + const regex = generateDynamicRegExp(filter, inputValue, comboBoxEl.dataset); + const options = []; + for (let i = 0, len = selectEl.options.length; i < len; i += 1) { + const optionEl = selectEl.options[i]; + const optionId = `${listOptionBaseId}${options.length}`; + if (optionEl.value && (disableFiltering || isPristine || !inputValue || regex.test(optionEl.text))) { + if (selectEl.value && optionEl.value === selectEl.value) { + selectedItemId = optionId; + } + if (disableFiltering && !firstFoundId && regex.test(optionEl.text)) { + firstFoundId = optionId; + } + options.push(optionEl); + } + } + const numOptions = options.length; + const optionHtml = options.map((option, index) => { + const optionId = `${listOptionBaseId}${index}`; + const classes = [LIST_OPTION_CLASS]; + let tabindex = "-1"; + let ariaSelected = "false"; + if (optionId === selectedItemId) { + classes.push(LIST_OPTION_SELECTED_CLASS, LIST_OPTION_FOCUSED_CLASS); + tabindex = "0"; + ariaSelected = "true"; + } + if (!selectedItemId && index === 0) { + classes.push(LIST_OPTION_FOCUSED_CLASS); + tabindex = "0"; + } + const li = document.createElement("li"); + li.setAttribute("aria-setsize", options.length); + li.setAttribute("aria-posinset", index + 1); + li.setAttribute("aria-selected", ariaSelected); + li.setAttribute("id", optionId); + li.setAttribute("class", classes.join(" ")); + li.setAttribute("tabindex", tabindex); + li.setAttribute("role", "option"); + li.setAttribute("data-value", option.value); + li.textContent = option.text; + return li; + }); + const noResults = document.createElement("li"); + noResults.setAttribute("class", `${LIST_OPTION_CLASS}--no-results`); + noResults.textContent = "No results found"; + listEl.hidden = false; + if (numOptions) { + listEl.innerHTML = ""; + optionHtml.forEach(item => listEl.insertAdjacentElement("beforeend", item)); + } else { + listEl.innerHTML = ""; + listEl.insertAdjacentElement("beforeend", noResults); + } + inputEl.setAttribute("aria-expanded", "true"); + statusEl.textContent = numOptions ? `${numOptions} result${numOptions > 1 ? "s" : ""} available.` : "No results."; + let itemToFocus; + if (isPristine && selectedItemId) { + itemToFocus = listEl.querySelector(`#${selectedItemId}`); + } else if (disableFiltering && firstFoundId) { + itemToFocus = listEl.querySelector(`#${firstFoundId}`); + } + if (itemToFocus) { + highlightOption(listEl, itemToFocus, { + skipFocus: true + }); + } +}; + +/** + * Hide the option list of a combo box component. + * + * @param {HTMLElement} el An element within the combo box component + */ +const hideList = el => { + const { + inputEl, + listEl, + statusEl, + focusedOptionEl + } = getComboBoxContext(el); + statusEl.innerHTML = ""; + inputEl.setAttribute("aria-expanded", "false"); + inputEl.setAttribute("aria-activedescendant", ""); + if (focusedOptionEl) { + focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS); + } + listEl.scrollTop = 0; + listEl.hidden = true; +}; + +/** + * Select an option list of the combo box component. + * + * @param {HTMLElement} listOptionEl The list option being selected + */ +const selectItem = listOptionEl => { + const { + comboBoxEl, + selectEl, + inputEl + } = getComboBoxContext(listOptionEl); + changeElementValue(selectEl, listOptionEl.dataset.value); + changeElementValue(inputEl, listOptionEl.textContent); + comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); + hideList(comboBoxEl); + inputEl.focus(); +}; + +/** + * Clear the input of the combo box + * + * @param {HTMLButtonElement} clearButtonEl The clear input button + */ +const clearInput = clearButtonEl => { + const { + comboBoxEl, + listEl, + selectEl, + inputEl + } = getComboBoxContext(clearButtonEl); + const listShown = !listEl.hidden; + if (selectEl.value) changeElementValue(selectEl); + if (inputEl.value) changeElementValue(inputEl); + comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS); + if (listShown) displayList(comboBoxEl); + inputEl.focus(); +}; + +/** + * Reset the select based off of currently set select value + * + * @param {HTMLElement} el An element within the combo box component + */ +const resetSelection = el => { + const { + comboBoxEl, + selectEl, + inputEl + } = getComboBoxContext(el); + const selectValue = selectEl.value; + const inputValue = (inputEl.value || "").toLowerCase(); + if (selectValue) { + for (let i = 0, len = selectEl.options.length; i < len; i += 1) { + const optionEl = selectEl.options[i]; + if (optionEl.value === selectValue) { + if (inputValue !== optionEl.text) { + changeElementValue(inputEl, optionEl.text); + } + comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); + return; + } + } + } + if (inputValue) { + changeElementValue(inputEl); + } +}; + +/** + * Select an option list of the combo box component based off of + * having a current focused list option or + * having test that completely matches a list option. + * Otherwise it clears the input and select. + * + * @param {HTMLElement} el An element within the combo box component + */ +const completeSelection = el => { + const { + comboBoxEl, + selectEl, + inputEl, + statusEl + } = getComboBoxContext(el); + statusEl.textContent = ""; + const inputValue = (inputEl.value || "").toLowerCase(); + if (inputValue) { + for (let i = 0, len = selectEl.options.length; i < len; i += 1) { + const optionEl = selectEl.options[i]; + if (optionEl.text.toLowerCase() === inputValue) { + changeElementValue(selectEl, optionEl.value); + changeElementValue(inputEl, optionEl.text); + comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); + return; + } + } + } + resetSelection(comboBoxEl); +}; + +/** + * Handle the escape event within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleEscape = event => { + const { + comboBoxEl, + inputEl + } = getComboBoxContext(event.target); + hideList(comboBoxEl); + resetSelection(comboBoxEl); + inputEl.focus(); +}; + +/** + * Handle the down event within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleDownFromInput = event => { + const { + comboBoxEl, + listEl + } = getComboBoxContext(event.target); + if (listEl.hidden) { + displayList(comboBoxEl); + } + const nextOptionEl = listEl.querySelector(LIST_OPTION_FOCUSED) || listEl.querySelector(LIST_OPTION); + if (nextOptionEl) { + highlightOption(comboBoxEl, nextOptionEl); + } + event.preventDefault(); +}; + +/** + * Handle the enter event from an input element within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleEnterFromInput = event => { + const { + comboBoxEl, + listEl + } = getComboBoxContext(event.target); + const listShown = !listEl.hidden; + completeSelection(comboBoxEl); + if (listShown) { + hideList(comboBoxEl); + } + event.preventDefault(); +}; + +/** + * Handle the down event within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleDownFromListOption = event => { + const focusedOptionEl = event.target; + const nextOptionEl = focusedOptionEl.nextSibling; + if (nextOptionEl) { + highlightOption(focusedOptionEl, nextOptionEl); + } + event.preventDefault(); +}; + +/** + * Handle the tab event from an list option element within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleTabFromListOption = event => { + selectItem(event.target); + event.preventDefault(); +}; + +/** + * Handle the enter event from list option within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleEnterFromListOption = event => { + selectItem(event.target); + event.preventDefault(); +}; + +/** + * Handle the up event from list option within the combo box component. + * + * @param {KeyboardEvent} event An event within the combo box component + */ +const handleUpFromListOption = event => { + const { + comboBoxEl, + listEl, + focusedOptionEl + } = getComboBoxContext(event.target); + const nextOptionEl = focusedOptionEl && focusedOptionEl.previousSibling; + const listShown = !listEl.hidden; + highlightOption(comboBoxEl, nextOptionEl); + if (listShown) { + event.preventDefault(); + } + if (!nextOptionEl) { + hideList(comboBoxEl); + } +}; + +/** + * Select list option on the mouseover event. + * + * @param {MouseEvent} event The mouseover event + * @param {HTMLLIElement} listOptionEl An element within the combo box component + */ +const handleMouseover = listOptionEl => { + const isCurrentlyFocused = listOptionEl.classList.contains(LIST_OPTION_FOCUSED_CLASS); + if (isCurrentlyFocused) return; + highlightOption(listOptionEl, listOptionEl, { + preventScroll: true + }); +}; + +/** + * Toggle the list when the button is clicked + * + * @param {HTMLElement} el An element within the combo box component + */ +const toggleList = el => { + const { + comboBoxEl, + listEl, + inputEl + } = getComboBoxContext(el); + if (listEl.hidden) { + displayList(comboBoxEl); + } else { + hideList(comboBoxEl); + } + inputEl.focus(); +}; + +/** + * Handle click from input + * + * @param {HTMLInputElement} el An element within the combo box component + */ +const handleClickFromInput = el => { + const { + comboBoxEl, + listEl + } = getComboBoxContext(el); + if (listEl.hidden) { + displayList(comboBoxEl); + } +}; +const comboBox = behavior({ + [CLICK]: { + [INPUT]() { + if (this.disabled) return; + handleClickFromInput(this); + }, + [TOGGLE_LIST_BUTTON]() { + if (this.disabled) return; + toggleList(this); + }, + [LIST_OPTION]() { + if (this.disabled) return; + selectItem(this); + }, + [CLEAR_INPUT_BUTTON]() { + if (this.disabled) return; + clearInput(this); + } + }, + focusout: { + [COMBO_BOX](event) { + if (!this.contains(event.relatedTarget)) { + resetSelection(this); + hideList(this); + } + } + }, + keydown: { + [COMBO_BOX]: keymap({ + Escape: handleEscape + }), + [INPUT]: keymap({ + Enter: handleEnterFromInput, + ArrowDown: handleDownFromInput, + Down: handleDownFromInput + }), + [LIST_OPTION]: keymap({ + ArrowUp: handleUpFromListOption, + Up: handleUpFromListOption, + ArrowDown: handleDownFromListOption, + Down: handleDownFromListOption, + Enter: handleEnterFromListOption, + Tab: handleTabFromListOption, + "Shift+Tab": noop + }) + }, + input: { + [INPUT]() { + const comboBoxEl = this.closest(COMBO_BOX); + comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS); + displayList(this); + } + }, + mouseover: { + [LIST_OPTION]() { + handleMouseover(this); + } + } +}, { + init(root) { + select(COMBO_BOX, root).forEach(comboBoxEl => { + enhanceComboBox(comboBoxEl); + }); + }, + getComboBoxContext, + enhanceComboBox, + generateDynamicRegExp, + disable, + enable, + displayList, + hideList, + COMBO_BOX_CLASS +}); +module.exports = comboBox; + +},{"../config":34,"../events":35,"../utils/behavior":43,"../utils/sanitizer":47,"../utils/select":49,"receptor/keymap":12}],19:[function(require,module,exports){ +"use strict"; + +const keymap = require("receptor/keymap"); +const behavior = require("../utils/behavior"); +const select = require("../utils/select"); +const { + prefix: PREFIX +} = require("../config"); +const { + CLICK +} = require("../events"); +const activeElement = require("../utils/active-element"); +const isIosDevice = require("../utils/is-ios-device"); +const Sanitizer = require("../utils/sanitizer"); +const DATE_PICKER_CLASS = `${PREFIX}-date-picker`; +const DATE_PICKER_WRAPPER_CLASS = `${DATE_PICKER_CLASS}__wrapper`; +const DATE_PICKER_INITIALIZED_CLASS = `${DATE_PICKER_CLASS}--initialized`; +const DATE_PICKER_ACTIVE_CLASS = `${DATE_PICKER_CLASS}--active`; +const DATE_PICKER_INTERNAL_INPUT_CLASS = `${DATE_PICKER_CLASS}__internal-input`; +const DATE_PICKER_EXTERNAL_INPUT_CLASS = `${DATE_PICKER_CLASS}__external-input`; +const DATE_PICKER_BUTTON_CLASS = `${DATE_PICKER_CLASS}__button`; +const DATE_PICKER_CALENDAR_CLASS = `${DATE_PICKER_CLASS}__calendar`; +const DATE_PICKER_STATUS_CLASS = `${DATE_PICKER_CLASS}__status`; +const CALENDAR_DATE_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__date`; +const CALENDAR_DATE_FOCUSED_CLASS = `${CALENDAR_DATE_CLASS}--focused`; +const CALENDAR_DATE_SELECTED_CLASS = `${CALENDAR_DATE_CLASS}--selected`; +const CALENDAR_DATE_PREVIOUS_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--previous-month`; +const CALENDAR_DATE_CURRENT_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--current-month`; +const CALENDAR_DATE_NEXT_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--next-month`; +const CALENDAR_DATE_RANGE_DATE_CLASS = `${CALENDAR_DATE_CLASS}--range-date`; +const CALENDAR_DATE_TODAY_CLASS = `${CALENDAR_DATE_CLASS}--today`; +const CALENDAR_DATE_RANGE_DATE_START_CLASS = `${CALENDAR_DATE_CLASS}--range-date-start`; +const CALENDAR_DATE_RANGE_DATE_END_CLASS = `${CALENDAR_DATE_CLASS}--range-date-end`; +const CALENDAR_DATE_WITHIN_RANGE_CLASS = `${CALENDAR_DATE_CLASS}--within-range`; +const CALENDAR_PREVIOUS_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-year`; +const CALENDAR_PREVIOUS_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-month`; +const CALENDAR_NEXT_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-year`; +const CALENDAR_NEXT_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-month`; +const CALENDAR_MONTH_SELECTION_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-selection`; +const CALENDAR_YEAR_SELECTION_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year-selection`; +const CALENDAR_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month`; +const CALENDAR_MONTH_FOCUSED_CLASS = `${CALENDAR_MONTH_CLASS}--focused`; +const CALENDAR_MONTH_SELECTED_CLASS = `${CALENDAR_MONTH_CLASS}--selected`; +const CALENDAR_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year`; +const CALENDAR_YEAR_FOCUSED_CLASS = `${CALENDAR_YEAR_CLASS}--focused`; +const CALENDAR_YEAR_SELECTED_CLASS = `${CALENDAR_YEAR_CLASS}--selected`; +const CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-year-chunk`; +const CALENDAR_NEXT_YEAR_CHUNK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-year-chunk`; +const CALENDAR_DATE_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__date-picker`; +const CALENDAR_MONTH_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-picker`; +const CALENDAR_YEAR_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year-picker`; +const CALENDAR_TABLE_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__table`; +const CALENDAR_ROW_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__row`; +const CALENDAR_CELL_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__cell`; +const CALENDAR_CELL_CENTER_ITEMS_CLASS = `${CALENDAR_CELL_CLASS}--center-items`; +const CALENDAR_MONTH_LABEL_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-label`; +const CALENDAR_DAY_OF_WEEK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__day-of-week`; +const DATE_PICKER = `.${DATE_PICKER_CLASS}`; +const DATE_PICKER_BUTTON = `.${DATE_PICKER_BUTTON_CLASS}`; +const DATE_PICKER_INTERNAL_INPUT = `.${DATE_PICKER_INTERNAL_INPUT_CLASS}`; +const DATE_PICKER_EXTERNAL_INPUT = `.${DATE_PICKER_EXTERNAL_INPUT_CLASS}`; +const DATE_PICKER_CALENDAR = `.${DATE_PICKER_CALENDAR_CLASS}`; +const DATE_PICKER_STATUS = `.${DATE_PICKER_STATUS_CLASS}`; +const CALENDAR_DATE = `.${CALENDAR_DATE_CLASS}`; +const CALENDAR_DATE_FOCUSED = `.${CALENDAR_DATE_FOCUSED_CLASS}`; +const CALENDAR_DATE_CURRENT_MONTH = `.${CALENDAR_DATE_CURRENT_MONTH_CLASS}`; +const CALENDAR_PREVIOUS_YEAR = `.${CALENDAR_PREVIOUS_YEAR_CLASS}`; +const CALENDAR_PREVIOUS_MONTH = `.${CALENDAR_PREVIOUS_MONTH_CLASS}`; +const CALENDAR_NEXT_YEAR = `.${CALENDAR_NEXT_YEAR_CLASS}`; +const CALENDAR_NEXT_MONTH = `.${CALENDAR_NEXT_MONTH_CLASS}`; +const CALENDAR_YEAR_SELECTION = `.${CALENDAR_YEAR_SELECTION_CLASS}`; +const CALENDAR_MONTH_SELECTION = `.${CALENDAR_MONTH_SELECTION_CLASS}`; +const CALENDAR_MONTH = `.${CALENDAR_MONTH_CLASS}`; +const CALENDAR_YEAR = `.${CALENDAR_YEAR_CLASS}`; +const CALENDAR_PREVIOUS_YEAR_CHUNK = `.${CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS}`; +const CALENDAR_NEXT_YEAR_CHUNK = `.${CALENDAR_NEXT_YEAR_CHUNK_CLASS}`; +const CALENDAR_DATE_PICKER = `.${CALENDAR_DATE_PICKER_CLASS}`; +const CALENDAR_MONTH_PICKER = `.${CALENDAR_MONTH_PICKER_CLASS}`; +const CALENDAR_YEAR_PICKER = `.${CALENDAR_YEAR_PICKER_CLASS}`; +const CALENDAR_MONTH_FOCUSED = `.${CALENDAR_MONTH_FOCUSED_CLASS}`; +const CALENDAR_YEAR_FOCUSED = `.${CALENDAR_YEAR_FOCUSED_CLASS}`; +const VALIDATION_MESSAGE = "Please enter a valid date"; +const MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; +const DAY_OF_WEEK_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +const ENTER_KEYCODE = 13; +const YEAR_CHUNK = 12; +const DEFAULT_MIN_DATE = "0000-01-01"; +const DEFAULT_EXTERNAL_DATE_FORMAT = "MM/DD/YYYY"; +const INTERNAL_DATE_FORMAT = "YYYY-MM-DD"; +const NOT_DISABLED_SELECTOR = ":not([disabled])"; +const processFocusableSelectors = function () { + for (var _len = arguments.length, selectors = new Array(_len), _key = 0; _key < _len; _key++) { + selectors[_key] = arguments[_key]; + } + return selectors.map(query => query + NOT_DISABLED_SELECTOR).join(", "); +}; +const DATE_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR, CALENDAR_PREVIOUS_MONTH, CALENDAR_YEAR_SELECTION, CALENDAR_MONTH_SELECTION, CALENDAR_NEXT_YEAR, CALENDAR_NEXT_MONTH, CALENDAR_DATE_FOCUSED); +const MONTH_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_MONTH_FOCUSED); +const YEAR_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR_CHUNK, CALENDAR_NEXT_YEAR_CHUNK, CALENDAR_YEAR_FOCUSED); + +// #region Date Manipulation Functions + +/** + * Keep date within month. Month would only be over by 1 to 3 days + * + * @param {Date} dateToCheck the date object to check + * @param {number} month the correct month + * @returns {Date} the date, corrected if needed + */ +const keepDateWithinMonth = (dateToCheck, month) => { + if (month !== dateToCheck.getMonth()) { + dateToCheck.setDate(0); + } + return dateToCheck; +}; + +/** + * Set date from month day year + * + * @param {number} year the year to set + * @param {number} month the month to set (zero-indexed) + * @param {number} date the date to set + * @returns {Date} the set date + */ +const setDate = (year, month, date) => { + const newDate = new Date(0); + newDate.setFullYear(year, month, date); + return newDate; +}; + +/** + * todays date + * + * @returns {Date} todays date + */ +const today = () => { + const newDate = new Date(); + const day = newDate.getDate(); + const month = newDate.getMonth(); + const year = newDate.getFullYear(); + return setDate(year, month, day); +}; + +/** + * Set date to first day of the month + * + * @param {number} date the date to adjust + * @returns {Date} the adjusted date + */ +const startOfMonth = date => { + const newDate = new Date(0); + newDate.setFullYear(date.getFullYear(), date.getMonth(), 1); + return newDate; +}; + +/** + * Set date to last day of the month + * + * @param {number} date the date to adjust + * @returns {Date} the adjusted date + */ +const lastDayOfMonth = date => { + const newDate = new Date(0); + newDate.setFullYear(date.getFullYear(), date.getMonth() + 1, 0); + return newDate; +}; + +/** + * Add days to date + * + * @param {Date} _date the date to adjust + * @param {number} numDays the difference in days + * @returns {Date} the adjusted date + */ +const addDays = (_date, numDays) => { + const newDate = new Date(_date.getTime()); + newDate.setDate(newDate.getDate() + numDays); + return newDate; +}; + +/** + * Subtract days from date + * + * @param {Date} _date the date to adjust + * @param {number} numDays the difference in days + * @returns {Date} the adjusted date + */ +const subDays = (_date, numDays) => addDays(_date, -numDays); + +/** + * Add weeks to date + * + * @param {Date} _date the date to adjust + * @param {number} numWeeks the difference in weeks + * @returns {Date} the adjusted date + */ +const addWeeks = (_date, numWeeks) => addDays(_date, numWeeks * 7); + +/** + * Subtract weeks from date + * + * @param {Date} _date the date to adjust + * @param {number} numWeeks the difference in weeks + * @returns {Date} the adjusted date + */ +const subWeeks = (_date, numWeeks) => addWeeks(_date, -numWeeks); + +/** + * Set date to the start of the week (Sunday) + * + * @param {Date} _date the date to adjust + * @returns {Date} the adjusted date + */ +const startOfWeek = _date => { + const dayOfWeek = _date.getDay(); + return subDays(_date, dayOfWeek); +}; + +/** + * Set date to the end of the week (Saturday) + * + * @param {Date} _date the date to adjust + * @param {number} numWeeks the difference in weeks + * @returns {Date} the adjusted date + */ +const endOfWeek = _date => { + const dayOfWeek = _date.getDay(); + return addDays(_date, 6 - dayOfWeek); +}; + +/** + * Add months to date and keep date within month + * + * @param {Date} _date the date to adjust + * @param {number} numMonths the difference in months + * @returns {Date} the adjusted date + */ +const addMonths = (_date, numMonths) => { + const newDate = new Date(_date.getTime()); + const dateMonth = (newDate.getMonth() + 12 + numMonths) % 12; + newDate.setMonth(newDate.getMonth() + numMonths); + keepDateWithinMonth(newDate, dateMonth); + return newDate; +}; + +/** + * Subtract months from date + * + * @param {Date} _date the date to adjust + * @param {number} numMonths the difference in months + * @returns {Date} the adjusted date + */ +const subMonths = (_date, numMonths) => addMonths(_date, -numMonths); + +/** + * Add years to date and keep date within month + * + * @param {Date} _date the date to adjust + * @param {number} numYears the difference in years + * @returns {Date} the adjusted date + */ +const addYears = (_date, numYears) => addMonths(_date, numYears * 12); + +/** + * Subtract years from date + * + * @param {Date} _date the date to adjust + * @param {number} numYears the difference in years + * @returns {Date} the adjusted date + */ +const subYears = (_date, numYears) => addYears(_date, -numYears); + +/** + * Set months of date + * + * @param {Date} _date the date to adjust + * @param {number} month zero-indexed month to set + * @returns {Date} the adjusted date + */ +const setMonth = (_date, month) => { + const newDate = new Date(_date.getTime()); + newDate.setMonth(month); + keepDateWithinMonth(newDate, month); + return newDate; +}; + +/** + * Set year of date + * + * @param {Date} _date the date to adjust + * @param {number} year the year to set + * @returns {Date} the adjusted date + */ +const setYear = (_date, year) => { + const newDate = new Date(_date.getTime()); + const month = newDate.getMonth(); + newDate.setFullYear(year); + keepDateWithinMonth(newDate, month); + return newDate; +}; + +/** + * Return the earliest date + * + * @param {Date} dateA date to compare + * @param {Date} dateB date to compare + * @returns {Date} the earliest date + */ +const min = (dateA, dateB) => { + let newDate = dateA; + if (dateB < dateA) { + newDate = dateB; + } + return new Date(newDate.getTime()); +}; + +/** + * Return the latest date + * + * @param {Date} dateA date to compare + * @param {Date} dateB date to compare + * @returns {Date} the latest date + */ +const max = (dateA, dateB) => { + let newDate = dateA; + if (dateB > dateA) { + newDate = dateB; + } + return new Date(newDate.getTime()); +}; + +/** + * Check if dates are the in the same year + * + * @param {Date} dateA date to compare + * @param {Date} dateB date to compare + * @returns {boolean} are dates in the same year + */ +const isSameYear = (dateA, dateB) => dateA && dateB && dateA.getFullYear() === dateB.getFullYear(); + +/** + * Check if dates are the in the same month + * + * @param {Date} dateA date to compare + * @param {Date} dateB date to compare + * @returns {boolean} are dates in the same month + */ +const isSameMonth = (dateA, dateB) => isSameYear(dateA, dateB) && dateA.getMonth() === dateB.getMonth(); + +/** + * Check if dates are the same date + * + * @param {Date} dateA the date to compare + * @param {Date} dateA the date to compare + * @returns {boolean} are dates the same date + */ +const isSameDay = (dateA, dateB) => isSameMonth(dateA, dateB) && dateA.getDate() === dateB.getDate(); + +/** + * return a new date within minimum and maximum date + * + * @param {Date} date date to check + * @param {Date} minDate minimum date to allow + * @param {Date} maxDate maximum date to allow + * @returns {Date} the date between min and max + */ +const keepDateBetweenMinAndMax = (date, minDate, maxDate) => { + let newDate = date; + if (date < minDate) { + newDate = minDate; + } else if (maxDate && date > maxDate) { + newDate = maxDate; + } + return new Date(newDate.getTime()); +}; + +/** + * Check if dates is valid. + * + * @param {Date} date date to check + * @param {Date} minDate minimum date to allow + * @param {Date} maxDate maximum date to allow + * @return {boolean} is there a day within the month within min and max dates + */ +const isDateWithinMinAndMax = (date, minDate, maxDate) => date >= minDate && (!maxDate || date <= maxDate); + +/** + * Check if dates month is invalid. + * + * @param {Date} date date to check + * @param {Date} minDate minimum date to allow + * @param {Date} maxDate maximum date to allow + * @return {boolean} is the month outside min or max dates + */ +const isDatesMonthOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(date) < minDate || maxDate && startOfMonth(date) > maxDate; + +/** + * Check if dates year is invalid. + * + * @param {Date} date date to check + * @param {Date} minDate minimum date to allow + * @param {Date} maxDate maximum date to allow + * @return {boolean} is the month outside min or max dates + */ +const isDatesYearOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(setMonth(date, 11)) < minDate || maxDate && startOfMonth(setMonth(date, 0)) > maxDate; + +/** + * Parse a date with format M-D-YY + * + * @param {string} dateString the date string to parse + * @param {string} dateFormat the format of the date string + * @param {boolean} adjustDate should the date be adjusted + * @returns {Date} the parsed date + */ +const parseDateString = function (dateString) { + let dateFormat = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : INTERNAL_DATE_FORMAT; + let adjustDate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + let date; + let month; + let day; + let year; + let parsed; + if (dateString) { + let monthStr; + let dayStr; + let yearStr; + if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) { + [monthStr, dayStr, yearStr] = dateString.split("/"); + } else { + [yearStr, monthStr, dayStr] = dateString.split("-"); + } + if (yearStr) { + parsed = parseInt(yearStr, 10); + if (!Number.isNaN(parsed)) { + year = parsed; + if (adjustDate) { + year = Math.max(0, year); + if (yearStr.length < 3) { + const currentYear = today().getFullYear(); + const currentYearStub = currentYear - currentYear % 10 ** yearStr.length; + year = currentYearStub + parsed; + } + } + } + } + if (monthStr) { + parsed = parseInt(monthStr, 10); + if (!Number.isNaN(parsed)) { + month = parsed; + if (adjustDate) { + month = Math.max(1, month); + month = Math.min(12, month); + } + } + } + if (month && dayStr && year != null) { + parsed = parseInt(dayStr, 10); + if (!Number.isNaN(parsed)) { + day = parsed; + if (adjustDate) { + const lastDayOfTheMonth = setDate(year, month, 0).getDate(); + day = Math.max(1, day); + day = Math.min(lastDayOfTheMonth, day); + } + } + } + if (month && day && year != null) { + date = setDate(year, month - 1, day); + } + } + return date; +}; + +/** + * Format a date to format MM-DD-YYYY + * + * @param {Date} date the date to format + * @param {string} dateFormat the format of the date string + * @returns {string} the formatted date string + */ +const formatDate = function (date) { + let dateFormat = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : INTERNAL_DATE_FORMAT; + const padZeros = (value, length) => `0000${value}`.slice(-length); + const month = date.getMonth() + 1; + const day = date.getDate(); + const year = date.getFullYear(); + if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) { + return [padZeros(month, 2), padZeros(day, 2), padZeros(year, 4)].join("/"); + } + return [padZeros(year, 4), padZeros(month, 2), padZeros(day, 2)].join("-"); +}; + +// #endregion Date Manipulation Functions + +/** + * Create a grid string from an array of html strings + * + * @param {string[]} htmlArray the array of html items + * @param {number} rowSize the length of a row + * @returns {string} the grid string + */ +const listToGridHtml = (htmlArray, rowSize) => { + const grid = []; + let row = []; + let i = 0; + while (i < htmlArray.length) { + row = []; + const tr = document.createElement("tr"); + while (i < htmlArray.length && row.length < rowSize) { + const td = document.createElement("td"); + td.insertAdjacentElement("beforeend", htmlArray[i]); + row.push(td); + i += 1; + } + row.forEach(element => { + tr.insertAdjacentElement("beforeend", element); + }); + grid.push(tr); + } + return grid; +}; +const createTableBody = grid => { + const tableBody = document.createElement("tbody"); + grid.forEach(element => { + tableBody.insertAdjacentElement("beforeend", element); + }); + return tableBody; +}; + +/** + * set the value of the element and dispatch a change event + * + * @param {HTMLInputElement} el The element to update + * @param {string} value The new value of the element + */ +const changeElementValue = function (el) { + let value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; + const elementToChange = el; + elementToChange.value = value; + const event = new CustomEvent("change", { + bubbles: true, + cancelable: true, + detail: { + value + } + }); + elementToChange.dispatchEvent(event); +}; + +/** + * The properties and elements within the date picker. + * @typedef {Object} DatePickerContext + * @property {HTMLDivElement} calendarEl + * @property {HTMLElement} datePickerEl + * @property {HTMLInputElement} internalInputEl + * @property {HTMLInputElement} externalInputEl + * @property {HTMLDivElement} statusEl + * @property {HTMLDivElement} firstYearChunkEl + * @property {Date} calendarDate + * @property {Date} minDate + * @property {Date} maxDate + * @property {Date} selectedDate + * @property {Date} rangeDate + * @property {Date} defaultDate + */ + +/** + * Get an object of the properties and elements belonging directly to the given + * date picker component. + * + * @param {HTMLElement} el the element within the date picker + * @returns {DatePickerContext} elements + */ +const getDatePickerContext = el => { + const datePickerEl = el.closest(DATE_PICKER); + if (!datePickerEl) { + throw new Error(`Element is missing outer ${DATE_PICKER}`); + } + const internalInputEl = datePickerEl.querySelector(DATE_PICKER_INTERNAL_INPUT); + const externalInputEl = datePickerEl.querySelector(DATE_PICKER_EXTERNAL_INPUT); + const calendarEl = datePickerEl.querySelector(DATE_PICKER_CALENDAR); + const toggleBtnEl = datePickerEl.querySelector(DATE_PICKER_BUTTON); + const statusEl = datePickerEl.querySelector(DATE_PICKER_STATUS); + const firstYearChunkEl = datePickerEl.querySelector(CALENDAR_YEAR); + const inputDate = parseDateString(externalInputEl.value, DEFAULT_EXTERNAL_DATE_FORMAT, true); + const selectedDate = parseDateString(internalInputEl.value); + const calendarDate = parseDateString(calendarEl.dataset.value); + const minDate = parseDateString(datePickerEl.dataset.minDate); + const maxDate = parseDateString(datePickerEl.dataset.maxDate); + const rangeDate = parseDateString(datePickerEl.dataset.rangeDate); + const defaultDate = parseDateString(datePickerEl.dataset.defaultDate); + if (minDate && maxDate && minDate > maxDate) { + throw new Error("Minimum date cannot be after maximum date"); + } + return { + calendarDate, + minDate, + toggleBtnEl, + selectedDate, + maxDate, + firstYearChunkEl, + datePickerEl, + inputDate, + internalInputEl, + externalInputEl, + calendarEl, + rangeDate, + defaultDate, + statusEl + }; +}; + +/** + * Disable the date picker component + * + * @param {HTMLElement} el An element within the date picker component + */ +const disable = el => { + const { + externalInputEl, + toggleBtnEl + } = getDatePickerContext(el); + toggleBtnEl.disabled = true; + externalInputEl.disabled = true; +}; + +/** + * Enable the date picker component + * + * @param {HTMLElement} el An element within the date picker component + */ +const enable = el => { + const { + externalInputEl, + toggleBtnEl + } = getDatePickerContext(el); + toggleBtnEl.disabled = false; + externalInputEl.disabled = false; +}; + +// #region Validation + +/** + * Validate the value in the input as a valid date of format M/D/YYYY + * + * @param {HTMLElement} el An element within the date picker component + */ +const isDateInputInvalid = el => { + const { + externalInputEl, + minDate, + maxDate + } = getDatePickerContext(el); + const dateString = externalInputEl.value; + let isInvalid = false; + if (dateString) { + isInvalid = true; + const dateStringParts = dateString.split("/"); + const [month, day, year] = dateStringParts.map(str => { + let value; + const parsed = parseInt(str, 10); + if (!Number.isNaN(parsed)) value = parsed; + return value; + }); + if (month && day && year != null) { + const checkDate = setDate(year, month - 1, day); + if (checkDate.getMonth() === month - 1 && checkDate.getDate() === day && checkDate.getFullYear() === year && dateStringParts[2].length === 4 && isDateWithinMinAndMax(checkDate, minDate, maxDate)) { + isInvalid = false; + } + } + } + return isInvalid; +}; + +/** + * Validate the value in the input as a valid date of format M/D/YYYY + * + * @param {HTMLElement} el An element within the date picker component + */ +const validateDateInput = el => { + const { + externalInputEl + } = getDatePickerContext(el); + const isInvalid = isDateInputInvalid(externalInputEl); + if (isInvalid && !externalInputEl.validationMessage) { + externalInputEl.setCustomValidity(VALIDATION_MESSAGE); + } + if (!isInvalid && externalInputEl.validationMessage === VALIDATION_MESSAGE) { + externalInputEl.setCustomValidity(""); + } +}; + +// #endregion Validation + +/** + * Enable the date picker component + * + * @param {HTMLElement} el An element within the date picker component + */ +const reconcileInputValues = el => { + const { + internalInputEl, + inputDate + } = getDatePickerContext(el); + let newValue = ""; + if (inputDate && !isDateInputInvalid(el)) { + newValue = formatDate(inputDate); + } + if (internalInputEl.value !== newValue) { + changeElementValue(internalInputEl, newValue); + } +}; + +/** + * Select the value of the date picker inputs. + * + * @param {HTMLButtonElement} el An element within the date picker component + * @param {string} dateString The date string to update in YYYY-MM-DD format + */ +const setCalendarValue = (el, dateString) => { + const parsedDate = parseDateString(dateString); + if (parsedDate) { + const formattedDate = formatDate(parsedDate, DEFAULT_EXTERNAL_DATE_FORMAT); + const { + datePickerEl, + internalInputEl, + externalInputEl + } = getDatePickerContext(el); + changeElementValue(internalInputEl, dateString); + changeElementValue(externalInputEl, formattedDate); + validateDateInput(datePickerEl); + } +}; + +/** + * Enhance an input with the date picker elements + * + * @param {HTMLElement} el The initial wrapping element of the date picker component + */ +const enhanceDatePicker = el => { + const datePickerEl = el.closest(DATE_PICKER); + const { + defaultValue + } = datePickerEl.dataset; + const internalInputEl = datePickerEl.querySelector(`input`); + if (!internalInputEl) { + throw new Error(`${DATE_PICKER} is missing inner input`); + } + if (internalInputEl.value) { + internalInputEl.value = ""; + } + const minDate = parseDateString(datePickerEl.dataset.minDate || internalInputEl.getAttribute("min")); + datePickerEl.dataset.minDate = minDate ? formatDate(minDate) : DEFAULT_MIN_DATE; + const maxDate = parseDateString(datePickerEl.dataset.maxDate || internalInputEl.getAttribute("max")); + if (maxDate) { + datePickerEl.dataset.maxDate = formatDate(maxDate); + } + const calendarWrapper = document.createElement("div"); + calendarWrapper.classList.add(DATE_PICKER_WRAPPER_CLASS); + const externalInputEl = internalInputEl.cloneNode(); + externalInputEl.classList.add(DATE_PICKER_EXTERNAL_INPUT_CLASS); + externalInputEl.type = "text"; + calendarWrapper.appendChild(externalInputEl); + calendarWrapper.insertAdjacentHTML("beforeend", Sanitizer.escapeHTML` + + +
    `); + internalInputEl.setAttribute("aria-hidden", "true"); + internalInputEl.setAttribute("tabindex", "-1"); + internalInputEl.style.display = "none"; + internalInputEl.classList.add(DATE_PICKER_INTERNAL_INPUT_CLASS); + internalInputEl.removeAttribute("id"); + internalInputEl.removeAttribute("name"); + internalInputEl.required = false; + datePickerEl.appendChild(calendarWrapper); + datePickerEl.classList.add(DATE_PICKER_INITIALIZED_CLASS); + if (defaultValue) { + setCalendarValue(datePickerEl, defaultValue); + } + if (internalInputEl.disabled) { + disable(datePickerEl); + internalInputEl.disabled = false; + } +}; + +// #region Calendar - Date Selection View + +/** + * render the calendar. + * + * @param {HTMLElement} el An element within the date picker component + * @param {Date} _dateToDisplay a date to render on the calendar + * @returns {HTMLElement} a reference to the new calendar element + */ +const renderCalendar = (el, _dateToDisplay) => { + const { + datePickerEl, + calendarEl, + statusEl, + selectedDate, + maxDate, + minDate, + rangeDate + } = getDatePickerContext(el); + const todaysDate = today(); + let dateToDisplay = _dateToDisplay || todaysDate; + const calendarWasHidden = calendarEl.hidden; + const focusedDate = addDays(dateToDisplay, 0); + const focusedMonth = dateToDisplay.getMonth(); + const focusedYear = dateToDisplay.getFullYear(); + const prevMonth = subMonths(dateToDisplay, 1); + const nextMonth = addMonths(dateToDisplay, 1); + const currentFormattedDate = formatDate(dateToDisplay); + const firstOfMonth = startOfMonth(dateToDisplay); + const prevButtonsDisabled = isSameMonth(dateToDisplay, minDate); + const nextButtonsDisabled = isSameMonth(dateToDisplay, maxDate); + const rangeConclusionDate = selectedDate || dateToDisplay; + const rangeStartDate = rangeDate && min(rangeConclusionDate, rangeDate); + const rangeEndDate = rangeDate && max(rangeConclusionDate, rangeDate); + const withinRangeStartDate = rangeDate && addDays(rangeStartDate, 1); + const withinRangeEndDate = rangeDate && subDays(rangeEndDate, 1); + const monthLabel = MONTH_LABELS[focusedMonth]; + const generateDateHtml = dateToRender => { + const classes = [CALENDAR_DATE_CLASS]; + const day = dateToRender.getDate(); + const month = dateToRender.getMonth(); + const year = dateToRender.getFullYear(); + const dayOfWeek = dateToRender.getDay(); + const formattedDate = formatDate(dateToRender); + let tabindex = "-1"; + const isDisabled = !isDateWithinMinAndMax(dateToRender, minDate, maxDate); + const isSelected = isSameDay(dateToRender, selectedDate); + if (isSameMonth(dateToRender, prevMonth)) { + classes.push(CALENDAR_DATE_PREVIOUS_MONTH_CLASS); + } + if (isSameMonth(dateToRender, focusedDate)) { + classes.push(CALENDAR_DATE_CURRENT_MONTH_CLASS); + } + if (isSameMonth(dateToRender, nextMonth)) { + classes.push(CALENDAR_DATE_NEXT_MONTH_CLASS); + } + if (isSelected) { + classes.push(CALENDAR_DATE_SELECTED_CLASS); + } + if (isSameDay(dateToRender, todaysDate)) { + classes.push(CALENDAR_DATE_TODAY_CLASS); + } + if (rangeDate) { + if (isSameDay(dateToRender, rangeDate)) { + classes.push(CALENDAR_DATE_RANGE_DATE_CLASS); + } + if (isSameDay(dateToRender, rangeStartDate)) { + classes.push(CALENDAR_DATE_RANGE_DATE_START_CLASS); + } + if (isSameDay(dateToRender, rangeEndDate)) { + classes.push(CALENDAR_DATE_RANGE_DATE_END_CLASS); + } + if (isDateWithinMinAndMax(dateToRender, withinRangeStartDate, withinRangeEndDate)) { + classes.push(CALENDAR_DATE_WITHIN_RANGE_CLASS); + } + } + if (isSameDay(dateToRender, focusedDate)) { + tabindex = "0"; + classes.push(CALENDAR_DATE_FOCUSED_CLASS); + } + const monthStr = MONTH_LABELS[month]; + const dayStr = DAY_OF_WEEK_LABELS[dayOfWeek]; + const btn = document.createElement("button"); + btn.setAttribute("type", "button"); + btn.setAttribute("tabindex", tabindex); + btn.setAttribute("class", classes.join(" ")); + btn.setAttribute("data-day", day); + btn.setAttribute("data-month", month + 1); + btn.setAttribute("data-year", year); + btn.setAttribute("data-value", formattedDate); + btn.setAttribute("aria-label", Sanitizer.escapeHTML`${day} ${monthStr} ${year} ${dayStr}`); + btn.setAttribute("aria-selected", isSelected ? "true" : "false"); + if (isDisabled === true) { + btn.disabled = true; + } + btn.textContent = day; + return btn; + }; + + // set date to first rendered day + dateToDisplay = startOfWeek(firstOfMonth); + const days = []; + while (days.length < 28 || dateToDisplay.getMonth() === focusedMonth || days.length % 7 !== 0) { + days.push(generateDateHtml(dateToDisplay)); + dateToDisplay = addDays(dateToDisplay, 1); + } + const datesGrid = listToGridHtml(days, 7); + const newCalendar = calendarEl.cloneNode(); + newCalendar.dataset.value = currentFormattedDate; + newCalendar.style.top = `${datePickerEl.offsetHeight}px`; + newCalendar.hidden = false; + newCalendar.innerHTML = Sanitizer.escapeHTML` +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    + `; + const table = document.createElement("table"); + table.setAttribute("class", CALENDAR_TABLE_CLASS); + table.setAttribute("role", "presentation"); + const tableHead = document.createElement("thead"); + table.insertAdjacentElement("beforeend", tableHead); + const tableHeadRow = document.createElement("tr"); + tableHead.insertAdjacentElement("beforeend", tableHeadRow); + const daysOfWeek = { + Sunday: "S", + Monday: "M", + Tuesday: "T", + Wednesday: "W", + Thursday: "Th", + Friday: "Fr", + Saturday: "S" + }; + Object.keys(daysOfWeek).forEach(key => { + const th = document.createElement("th"); + th.setAttribute("class", CALENDAR_DAY_OF_WEEK_CLASS); + th.setAttribute("scope", "presentation"); + th.setAttribute("aria-label", key); + th.textContent = daysOfWeek[key]; + tableHeadRow.insertAdjacentElement("beforeend", th); + }); + const tableBody = createTableBody(datesGrid); + table.insertAdjacentElement("beforeend", tableBody); + + // Container for Years, Months, and Days + const datePickerCalendarContainer = newCalendar.querySelector(CALENDAR_DATE_PICKER); + datePickerCalendarContainer.insertAdjacentElement("beforeend", table); + calendarEl.parentNode.replaceChild(newCalendar, calendarEl); + datePickerEl.classList.add(DATE_PICKER_ACTIVE_CLASS); + const statuses = []; + if (isSameDay(selectedDate, focusedDate)) { + statuses.push("Selected date"); + } + if (calendarWasHidden) { + statuses.push("You can navigate by day using left and right arrows", "Weeks by using up and down arrows", "Months by using page up and page down keys", "Years by using shift plus page up and shift plus page down", "Home and end keys navigate to the beginning and end of a week"); + statusEl.textContent = ""; + } else { + statuses.push(`${monthLabel} ${focusedYear}`); + } + statusEl.textContent = statuses.join(". "); + return newCalendar; +}; + +/** + * Navigate back one year and display the calendar. + * + * @param {HTMLButtonElement} _buttonEl An element within the date picker component + */ +const displayPreviousYear = _buttonEl => { + if (_buttonEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(_buttonEl); + let date = subYears(calendarDate, 1); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Navigate back one month and display the calendar. + * + * @param {HTMLButtonElement} _buttonEl An element within the date picker component + */ +const displayPreviousMonth = _buttonEl => { + if (_buttonEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(_buttonEl); + let date = subMonths(calendarDate, 1); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_MONTH); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Navigate forward one month and display the calendar. + * + * @param {HTMLButtonElement} _buttonEl An element within the date picker component + */ +const displayNextMonth = _buttonEl => { + if (_buttonEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(_buttonEl); + let date = addMonths(calendarDate, 1); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_MONTH); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Navigate forward one year and display the calendar. + * + * @param {HTMLButtonElement} _buttonEl An element within the date picker component + */ +const displayNextYear = _buttonEl => { + if (_buttonEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(_buttonEl); + let date = addYears(calendarDate, 1); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Hide the calendar of a date picker component. + * + * @param {HTMLElement} el An element within the date picker component + */ +const hideCalendar = el => { + const { + datePickerEl, + calendarEl, + statusEl + } = getDatePickerContext(el); + datePickerEl.classList.remove(DATE_PICKER_ACTIVE_CLASS); + calendarEl.hidden = true; + statusEl.textContent = ""; +}; + +/** + * Select a date within the date picker component. + * + * @param {HTMLButtonElement} calendarDateEl A date element within the date picker component + */ +const selectDate = calendarDateEl => { + if (calendarDateEl.disabled) return; + const { + datePickerEl, + externalInputEl + } = getDatePickerContext(calendarDateEl); + setCalendarValue(calendarDateEl, calendarDateEl.dataset.value); + hideCalendar(datePickerEl); + externalInputEl.focus(); +}; + +/** + * Toggle the calendar. + * + * @param {HTMLButtonElement} el An element within the date picker component + */ +const toggleCalendar = el => { + if (el.disabled) return; + const { + calendarEl, + inputDate, + minDate, + maxDate, + defaultDate + } = getDatePickerContext(el); + if (calendarEl.hidden) { + const dateToDisplay = keepDateBetweenMinAndMax(inputDate || defaultDate || today(), minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, dateToDisplay); + newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); + } else { + hideCalendar(el); + } +}; + +/** + * Update the calendar when visible. + * + * @param {HTMLElement} el an element within the date picker + */ +const updateCalendarIfVisible = el => { + const { + calendarEl, + inputDate, + minDate, + maxDate + } = getDatePickerContext(el); + const calendarShown = !calendarEl.hidden; + if (calendarShown && inputDate) { + const dateToDisplay = keepDateBetweenMinAndMax(inputDate, minDate, maxDate); + renderCalendar(calendarEl, dateToDisplay); + } +}; + +// #endregion Calendar - Date Selection View + +// #region Calendar - Month Selection View +/** + * Display the month selection screen in the date picker. + * + * @param {HTMLButtonElement} el An element within the date picker component + * @returns {HTMLElement} a reference to the new calendar element + */ +const displayMonthSelection = (el, monthToDisplay) => { + const { + calendarEl, + statusEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(el); + const selectedMonth = calendarDate.getMonth(); + const focusedMonth = monthToDisplay == null ? selectedMonth : monthToDisplay; + const months = MONTH_LABELS.map((month, index) => { + const monthToCheck = setMonth(calendarDate, index); + const isDisabled = isDatesMonthOutsideMinOrMax(monthToCheck, minDate, maxDate); + let tabindex = "-1"; + const classes = [CALENDAR_MONTH_CLASS]; + const isSelected = index === selectedMonth; + if (index === focusedMonth) { + tabindex = "0"; + classes.push(CALENDAR_MONTH_FOCUSED_CLASS); + } + if (isSelected) { + classes.push(CALENDAR_MONTH_SELECTED_CLASS); + } + const btn = document.createElement("button"); + btn.setAttribute("type", "button"); + btn.setAttribute("tabindex", tabindex); + btn.setAttribute("class", classes.join(" ")); + btn.setAttribute("data-value", index); + btn.setAttribute("data-label", month); + btn.setAttribute("aria-selected", isSelected ? "true" : "false"); + if (isDisabled === true) { + btn.disabled = true; + } + btn.textContent = month; + return btn; + }); + const monthsHtml = document.createElement("div"); + monthsHtml.setAttribute("tabindex", "-1"); + monthsHtml.setAttribute("class", CALENDAR_MONTH_PICKER_CLASS); + const table = document.createElement("table"); + table.setAttribute("class", CALENDAR_TABLE_CLASS); + table.setAttribute("role", "presentation"); + const monthsGrid = listToGridHtml(months, 3); + const tableBody = createTableBody(monthsGrid); + table.insertAdjacentElement("beforeend", tableBody); + monthsHtml.insertAdjacentElement("beforeend", table); + const newCalendar = calendarEl.cloneNode(); + newCalendar.insertAdjacentElement("beforeend", monthsHtml); + calendarEl.parentNode.replaceChild(newCalendar, calendarEl); + statusEl.textContent = "Select a month."; + return newCalendar; +}; + +/** + * Select a month in the date picker component. + * + * @param {HTMLButtonElement} monthEl An month element within the date picker component + */ +const selectMonth = monthEl => { + if (monthEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(monthEl); + const selectedMonth = parseInt(monthEl.dataset.value, 10); + let date = setMonth(calendarDate, selectedMonth); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); +}; + +// #endregion Calendar - Month Selection View + +// #region Calendar - Year Selection View + +/** + * Display the year selection screen in the date picker. + * + * @param {HTMLButtonElement} el An element within the date picker component + * @param {number} yearToDisplay year to display in year selection + * @returns {HTMLElement} a reference to the new calendar element + */ +const displayYearSelection = (el, yearToDisplay) => { + const { + calendarEl, + statusEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(el); + const selectedYear = calendarDate.getFullYear(); + const focusedYear = yearToDisplay == null ? selectedYear : yearToDisplay; + let yearToChunk = focusedYear; + yearToChunk -= yearToChunk % YEAR_CHUNK; + yearToChunk = Math.max(0, yearToChunk); + const prevYearChunkDisabled = isDatesYearOutsideMinOrMax(setYear(calendarDate, yearToChunk - 1), minDate, maxDate); + const nextYearChunkDisabled = isDatesYearOutsideMinOrMax(setYear(calendarDate, yearToChunk + YEAR_CHUNK), minDate, maxDate); + const years = []; + let yearIndex = yearToChunk; + while (years.length < YEAR_CHUNK) { + const isDisabled = isDatesYearOutsideMinOrMax(setYear(calendarDate, yearIndex), minDate, maxDate); + let tabindex = "-1"; + const classes = [CALENDAR_YEAR_CLASS]; + const isSelected = yearIndex === selectedYear; + if (yearIndex === focusedYear) { + tabindex = "0"; + classes.push(CALENDAR_YEAR_FOCUSED_CLASS); + } + if (isSelected) { + classes.push(CALENDAR_YEAR_SELECTED_CLASS); + } + const btn = document.createElement("button"); + btn.setAttribute("type", "button"); + btn.setAttribute("tabindex", tabindex); + btn.setAttribute("class", classes.join(" ")); + btn.setAttribute("data-value", yearIndex); + btn.setAttribute("aria-selected", isSelected ? "true" : "false"); + if (isDisabled === true) { + btn.disabled = true; + } + btn.textContent = yearIndex; + years.push(btn); + yearIndex += 1; + } + const newCalendar = calendarEl.cloneNode(); + + // create the years calendar wrapper + const yearsCalendarWrapper = document.createElement("div"); + yearsCalendarWrapper.setAttribute("tabindex", "-1"); + yearsCalendarWrapper.setAttribute("class", CALENDAR_YEAR_PICKER_CLASS); + + // create table parent + const yearsTableParent = document.createElement("table"); + yearsTableParent.setAttribute("role", "presentation"); + yearsTableParent.setAttribute("class", CALENDAR_TABLE_CLASS); + + // create table body and table row + const yearsHTMLTableBody = document.createElement("tbody"); + const yearsHTMLTableBodyRow = document.createElement("tr"); + + // create previous button + const previousYearsBtn = document.createElement("button"); + previousYearsBtn.setAttribute("type", "button"); + previousYearsBtn.setAttribute("class", CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS); + previousYearsBtn.setAttribute("aria-label", `Navigate back ${YEAR_CHUNK} years`); + if (prevYearChunkDisabled === true) { + previousYearsBtn.disabled = true; + } + previousYearsBtn.innerHTML = Sanitizer.escapeHTML` `; + + // create next button + const nextYearsBtn = document.createElement("button"); + nextYearsBtn.setAttribute("type", "button"); + nextYearsBtn.setAttribute("class", CALENDAR_NEXT_YEAR_CHUNK_CLASS); + nextYearsBtn.setAttribute("aria-label", `Navigate forward ${YEAR_CHUNK} years`); + if (nextYearChunkDisabled === true) { + nextYearsBtn.disabled = true; + } + nextYearsBtn.innerHTML = Sanitizer.escapeHTML` `; + + // create the actual years table + const yearsTable = document.createElement("table"); + yearsTable.setAttribute("class", CALENDAR_TABLE_CLASS); + yearsTable.setAttribute("role", "presentation"); + + // create the years child table + const yearsGrid = listToGridHtml(years, 3); + const yearsTableBody = createTableBody(yearsGrid); + + // append the grid to the years child table + yearsTable.insertAdjacentElement("beforeend", yearsTableBody); + + // create the prev button td and append the prev button + const yearsHTMLTableBodyDetailPrev = document.createElement("td"); + yearsHTMLTableBodyDetailPrev.insertAdjacentElement("beforeend", previousYearsBtn); + + // create the years td and append the years child table + const yearsHTMLTableBodyYearsDetail = document.createElement("td"); + yearsHTMLTableBodyYearsDetail.setAttribute("colspan", "3"); + yearsHTMLTableBodyYearsDetail.insertAdjacentElement("beforeend", yearsTable); + + // create the next button td and append the next button + const yearsHTMLTableBodyDetailNext = document.createElement("td"); + yearsHTMLTableBodyDetailNext.insertAdjacentElement("beforeend", nextYearsBtn); + + // append the three td to the years child table row + yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyDetailPrev); + yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyYearsDetail); + yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyDetailNext); + + // append the table row to the years child table body + yearsHTMLTableBody.insertAdjacentElement("beforeend", yearsHTMLTableBodyRow); + + // append the years table body to the years parent table + yearsTableParent.insertAdjacentElement("beforeend", yearsHTMLTableBody); + + // append the parent table to the calendar wrapper + yearsCalendarWrapper.insertAdjacentElement("beforeend", yearsTableParent); + + // append the years calender to the new calendar + newCalendar.insertAdjacentElement("beforeend", yearsCalendarWrapper); + + // replace calendar + calendarEl.parentNode.replaceChild(newCalendar, calendarEl); + statusEl.textContent = Sanitizer.escapeHTML`Showing years ${yearToChunk} to ${yearToChunk + YEAR_CHUNK - 1}. Select a year.`; + return newCalendar; +}; + +/** + * Navigate back by years and display the year selection screen. + * + * @param {HTMLButtonElement} el An element within the date picker component + */ +const displayPreviousYearChunk = el => { + if (el.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(el); + const yearEl = calendarEl.querySelector(CALENDAR_YEAR_FOCUSED); + const selectedYear = parseInt(yearEl.textContent, 10); + let adjustedYear = selectedYear - YEAR_CHUNK; + adjustedYear = Math.max(0, adjustedYear); + const date = setYear(calendarDate, adjustedYear); + const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); + let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR_CHUNK); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Navigate forward by years and display the year selection screen. + * + * @param {HTMLButtonElement} el An element within the date picker component + */ +const displayNextYearChunk = el => { + if (el.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(el); + const yearEl = calendarEl.querySelector(CALENDAR_YEAR_FOCUSED); + const selectedYear = parseInt(yearEl.textContent, 10); + let adjustedYear = selectedYear + YEAR_CHUNK; + adjustedYear = Math.max(0, adjustedYear); + const date = setYear(calendarDate, adjustedYear); + const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); + let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR_CHUNK); + if (nextToFocus.disabled) { + nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER); + } + nextToFocus.focus(); +}; + +/** + * Select a year in the date picker component. + * + * @param {HTMLButtonElement} yearEl A year element within the date picker component + */ +const selectYear = yearEl => { + if (yearEl.disabled) return; + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(yearEl); + const selectedYear = parseInt(yearEl.innerHTML, 10); + let date = setYear(calendarDate, selectedYear); + date = keepDateBetweenMinAndMax(date, minDate, maxDate); + const newCalendar = renderCalendar(calendarEl, date); + newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); +}; + +// #endregion Calendar - Year Selection View + +// #region Calendar Event Handling + +/** + * Hide the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleEscapeFromCalendar = event => { + const { + datePickerEl, + externalInputEl + } = getDatePickerContext(event.target); + hideCalendar(datePickerEl); + externalInputEl.focus(); + event.preventDefault(); +}; + +// #endregion Calendar Event Handling + +// #region Calendar Date Event Handling + +/** + * Adjust the date and display the calendar if needed. + * + * @param {function} adjustDateFn function that returns the adjusted date + */ +const adjustCalendar = adjustDateFn => event => { + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(event.target); + const date = adjustDateFn(calendarDate); + const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); + if (!isSameDay(calendarDate, cappedDate)) { + const newCalendar = renderCalendar(calendarEl, cappedDate); + newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); + } + event.preventDefault(); +}; + +/** + * Navigate back one week and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleUpFromDate = adjustCalendar(date => subWeeks(date, 1)); + +/** + * Navigate forward one week and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleDownFromDate = adjustCalendar(date => addWeeks(date, 1)); + +/** + * Navigate back one day and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleLeftFromDate = adjustCalendar(date => subDays(date, 1)); + +/** + * Navigate forward one day and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleRightFromDate = adjustCalendar(date => addDays(date, 1)); + +/** + * Navigate to the start of the week and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleHomeFromDate = adjustCalendar(date => startOfWeek(date)); + +/** + * Navigate to the end of the week and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleEndFromDate = adjustCalendar(date => endOfWeek(date)); + +/** + * Navigate forward one month and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageDownFromDate = adjustCalendar(date => addMonths(date, 1)); + +/** + * Navigate back one month and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageUpFromDate = adjustCalendar(date => subMonths(date, 1)); + +/** + * Navigate forward one year and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleShiftPageDownFromDate = adjustCalendar(date => addYears(date, 1)); + +/** + * Navigate back one year and display the calendar. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleShiftPageUpFromDate = adjustCalendar(date => subYears(date, 1)); + +/** + * display the calendar for the mouseover date. + * + * @param {MouseEvent} event The mouseover event + * @param {HTMLButtonElement} dateEl A date element within the date picker component + */ +const handleMouseoverFromDate = dateEl => { + if (dateEl.disabled) return; + const calendarEl = dateEl.closest(DATE_PICKER_CALENDAR); + const currentCalendarDate = calendarEl.dataset.value; + const hoverDate = dateEl.dataset.value; + if (hoverDate === currentCalendarDate) return; + const dateToDisplay = parseDateString(hoverDate); + const newCalendar = renderCalendar(calendarEl, dateToDisplay); + newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); +}; + +// #endregion Calendar Date Event Handling + +// #region Calendar Month Event Handling + +/** + * Adjust the month and display the month selection screen if needed. + * + * @param {function} adjustMonthFn function that returns the adjusted month + */ +const adjustMonthSelectionScreen = adjustMonthFn => event => { + const monthEl = event.target; + const selectedMonth = parseInt(monthEl.dataset.value, 10); + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(monthEl); + const currentDate = setMonth(calendarDate, selectedMonth); + let adjustedMonth = adjustMonthFn(selectedMonth); + adjustedMonth = Math.max(0, Math.min(11, adjustedMonth)); + const date = setMonth(calendarDate, adjustedMonth); + const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); + if (!isSameMonth(currentDate, cappedDate)) { + const newCalendar = displayMonthSelection(calendarEl, cappedDate.getMonth()); + newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); + } + event.preventDefault(); +}; + +/** + * Navigate back three months and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleUpFromMonth = adjustMonthSelectionScreen(month => month - 3); + +/** + * Navigate forward three months and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleDownFromMonth = adjustMonthSelectionScreen(month => month + 3); + +/** + * Navigate back one month and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleLeftFromMonth = adjustMonthSelectionScreen(month => month - 1); + +/** + * Navigate forward one month and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleRightFromMonth = adjustMonthSelectionScreen(month => month + 1); + +/** + * Navigate to the start of the row of months and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleHomeFromMonth = adjustMonthSelectionScreen(month => month - month % 3); + +/** + * Navigate to the end of the row of months and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleEndFromMonth = adjustMonthSelectionScreen(month => month + 2 - month % 3); + +/** + * Navigate to the last month (December) and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageDownFromMonth = adjustMonthSelectionScreen(() => 11); + +/** + * Navigate to the first month (January) and display the month selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageUpFromMonth = adjustMonthSelectionScreen(() => 0); + +/** + * update the focus on a month when the mouse moves. + * + * @param {MouseEvent} event The mouseover event + * @param {HTMLButtonElement} monthEl A month element within the date picker component + */ +const handleMouseoverFromMonth = monthEl => { + if (monthEl.disabled) return; + if (monthEl.classList.contains(CALENDAR_MONTH_FOCUSED_CLASS)) return; + const focusMonth = parseInt(monthEl.dataset.value, 10); + const newCalendar = displayMonthSelection(monthEl, focusMonth); + newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); +}; + +// #endregion Calendar Month Event Handling + +// #region Calendar Year Event Handling + +/** + * Adjust the year and display the year selection screen if needed. + * + * @param {function} adjustYearFn function that returns the adjusted year + */ +const adjustYearSelectionScreen = adjustYearFn => event => { + const yearEl = event.target; + const selectedYear = parseInt(yearEl.dataset.value, 10); + const { + calendarEl, + calendarDate, + minDate, + maxDate + } = getDatePickerContext(yearEl); + const currentDate = setYear(calendarDate, selectedYear); + let adjustedYear = adjustYearFn(selectedYear); + adjustedYear = Math.max(0, adjustedYear); + const date = setYear(calendarDate, adjustedYear); + const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); + if (!isSameYear(currentDate, cappedDate)) { + const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); + newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); + } + event.preventDefault(); +}; + +/** + * Navigate back three years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleUpFromYear = adjustYearSelectionScreen(year => year - 3); + +/** + * Navigate forward three years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleDownFromYear = adjustYearSelectionScreen(year => year + 3); + +/** + * Navigate back one year and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleLeftFromYear = adjustYearSelectionScreen(year => year - 1); + +/** + * Navigate forward one year and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleRightFromYear = adjustYearSelectionScreen(year => year + 1); + +/** + * Navigate to the start of the row of years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleHomeFromYear = adjustYearSelectionScreen(year => year - year % 3); + +/** + * Navigate to the end of the row of years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handleEndFromYear = adjustYearSelectionScreen(year => year + 2 - year % 3); + +/** + * Navigate to back 12 years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageUpFromYear = adjustYearSelectionScreen(year => year - YEAR_CHUNK); + +/** + * Navigate forward 12 years and display the year selection screen. + * + * @param {KeyboardEvent} event the keydown event + */ +const handlePageDownFromYear = adjustYearSelectionScreen(year => year + YEAR_CHUNK); + +/** + * update the focus on a year when the mouse moves. + * + * @param {MouseEvent} event The mouseover event + * @param {HTMLButtonElement} dateEl A year element within the date picker component + */ +const handleMouseoverFromYear = yearEl => { + if (yearEl.disabled) return; + if (yearEl.classList.contains(CALENDAR_YEAR_FOCUSED_CLASS)) return; + const focusYear = parseInt(yearEl.dataset.value, 10); + const newCalendar = displayYearSelection(yearEl, focusYear); + newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); +}; + +// #endregion Calendar Year Event Handling + +// #region Focus Handling Event Handling + +const tabHandler = focusable => { + const getFocusableContext = el => { + const { + calendarEl + } = getDatePickerContext(el); + const focusableElements = select(focusable, calendarEl); + const firstTabIndex = 0; + const lastTabIndex = focusableElements.length - 1; + const firstTabStop = focusableElements[firstTabIndex]; + const lastTabStop = focusableElements[lastTabIndex]; + const focusIndex = focusableElements.indexOf(activeElement()); + const isLastTab = focusIndex === lastTabIndex; + const isFirstTab = focusIndex === firstTabIndex; + const isNotFound = focusIndex === -1; + return { + focusableElements, + isNotFound, + firstTabStop, + isFirstTab, + lastTabStop, + isLastTab + }; + }; + return { + tabAhead(event) { + const { + firstTabStop, + isLastTab, + isNotFound + } = getFocusableContext(event.target); + if (isLastTab || isNotFound) { + event.preventDefault(); + firstTabStop.focus(); + } + }, + tabBack(event) { + const { + lastTabStop, + isFirstTab, + isNotFound + } = getFocusableContext(event.target); + if (isFirstTab || isNotFound) { + event.preventDefault(); + lastTabStop.focus(); + } + } + }; +}; +const datePickerTabEventHandler = tabHandler(DATE_PICKER_FOCUSABLE); +const monthPickerTabEventHandler = tabHandler(MONTH_PICKER_FOCUSABLE); +const yearPickerTabEventHandler = tabHandler(YEAR_PICKER_FOCUSABLE); + +// #endregion Focus Handling Event Handling + +// #region Date Picker Event Delegation Registration / Component + +const datePickerEvents = { + [CLICK]: { + [DATE_PICKER_BUTTON]() { + toggleCalendar(this); + }, + [CALENDAR_DATE]() { + selectDate(this); + }, + [CALENDAR_MONTH]() { + selectMonth(this); + }, + [CALENDAR_YEAR]() { + selectYear(this); + }, + [CALENDAR_PREVIOUS_MONTH]() { + displayPreviousMonth(this); + }, + [CALENDAR_NEXT_MONTH]() { + displayNextMonth(this); + }, + [CALENDAR_PREVIOUS_YEAR]() { + displayPreviousYear(this); + }, + [CALENDAR_NEXT_YEAR]() { + displayNextYear(this); + }, + [CALENDAR_PREVIOUS_YEAR_CHUNK]() { + displayPreviousYearChunk(this); + }, + [CALENDAR_NEXT_YEAR_CHUNK]() { + displayNextYearChunk(this); + }, + [CALENDAR_MONTH_SELECTION]() { + const newCalendar = displayMonthSelection(this); + newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); + }, + [CALENDAR_YEAR_SELECTION]() { + const newCalendar = displayYearSelection(this); + newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); + } + }, + keyup: { + [DATE_PICKER_CALENDAR](event) { + const keydown = this.dataset.keydownKeyCode; + if (`${event.keyCode}` !== keydown) { + event.preventDefault(); + } + } + }, + keydown: { + [DATE_PICKER_EXTERNAL_INPUT](event) { + if (event.keyCode === ENTER_KEYCODE) { + validateDateInput(this); + } + }, + [CALENDAR_DATE]: keymap({ + Up: handleUpFromDate, + ArrowUp: handleUpFromDate, + Down: handleDownFromDate, + ArrowDown: handleDownFromDate, + Left: handleLeftFromDate, + ArrowLeft: handleLeftFromDate, + Right: handleRightFromDate, + ArrowRight: handleRightFromDate, + Home: handleHomeFromDate, + End: handleEndFromDate, + PageDown: handlePageDownFromDate, + PageUp: handlePageUpFromDate, + "Shift+PageDown": handleShiftPageDownFromDate, + "Shift+PageUp": handleShiftPageUpFromDate, + Tab: datePickerTabEventHandler.tabAhead + }), + [CALENDAR_DATE_PICKER]: keymap({ + Tab: datePickerTabEventHandler.tabAhead, + "Shift+Tab": datePickerTabEventHandler.tabBack + }), + [CALENDAR_MONTH]: keymap({ + Up: handleUpFromMonth, + ArrowUp: handleUpFromMonth, + Down: handleDownFromMonth, + ArrowDown: handleDownFromMonth, + Left: handleLeftFromMonth, + ArrowLeft: handleLeftFromMonth, + Right: handleRightFromMonth, + ArrowRight: handleRightFromMonth, + Home: handleHomeFromMonth, + End: handleEndFromMonth, + PageDown: handlePageDownFromMonth, + PageUp: handlePageUpFromMonth + }), + [CALENDAR_MONTH_PICKER]: keymap({ + Tab: monthPickerTabEventHandler.tabAhead, + "Shift+Tab": monthPickerTabEventHandler.tabBack + }), + [CALENDAR_YEAR]: keymap({ + Up: handleUpFromYear, + ArrowUp: handleUpFromYear, + Down: handleDownFromYear, + ArrowDown: handleDownFromYear, + Left: handleLeftFromYear, + ArrowLeft: handleLeftFromYear, + Right: handleRightFromYear, + ArrowRight: handleRightFromYear, + Home: handleHomeFromYear, + End: handleEndFromYear, + PageDown: handlePageDownFromYear, + PageUp: handlePageUpFromYear + }), + [CALENDAR_YEAR_PICKER]: keymap({ + Tab: yearPickerTabEventHandler.tabAhead, + "Shift+Tab": yearPickerTabEventHandler.tabBack + }), + [DATE_PICKER_CALENDAR](event) { + this.dataset.keydownKeyCode = event.keyCode; + }, + [DATE_PICKER](event) { + const keyMap = keymap({ + Escape: handleEscapeFromCalendar + }); + keyMap(event); + } + }, + focusout: { + [DATE_PICKER_EXTERNAL_INPUT]() { + validateDateInput(this); + }, + [DATE_PICKER](event) { + if (!this.contains(event.relatedTarget)) { + hideCalendar(this); + } + } + }, + input: { + [DATE_PICKER_EXTERNAL_INPUT]() { + reconcileInputValues(this); + updateCalendarIfVisible(this); + } + } +}; +if (!isIosDevice()) { + datePickerEvents.mouseover = { + [CALENDAR_DATE_CURRENT_MONTH]() { + handleMouseoverFromDate(this); + }, + [CALENDAR_MONTH]() { + handleMouseoverFromMonth(this); + }, + [CALENDAR_YEAR]() { + handleMouseoverFromYear(this); + } + }; +} +const datePicker = behavior(datePickerEvents, { + init(root) { + select(DATE_PICKER, root).forEach(datePickerEl => { + enhanceDatePicker(datePickerEl); + }); + }, + getDatePickerContext, + disable, + enable, + isDateInputInvalid, + setCalendarValue, + validateDateInput, + renderCalendar, + updateCalendarIfVisible +}); + +// #endregion Date Picker Event Delegation Registration / Component + +module.exports = datePicker; + +},{"../config":34,"../events":35,"../utils/active-element":42,"../utils/behavior":43,"../utils/is-ios-device":46,"../utils/sanitizer":47,"../utils/select":49,"receptor/keymap":12}],20:[function(require,module,exports){ +"use strict"; + +const behavior = require("../utils/behavior"); +const select = require("../utils/select"); +const { + prefix: PREFIX +} = require("../config"); +const { + getDatePickerContext, + isDateInputInvalid, + updateCalendarIfVisible +} = require("./date-picker"); +const DATE_PICKER_CLASS = `${PREFIX}-date-picker`; +const DATE_RANGE_PICKER_CLASS = `${PREFIX}-date-range-picker`; +const DATE_RANGE_PICKER_RANGE_START_CLASS = `${DATE_RANGE_PICKER_CLASS}__range-start`; +const DATE_RANGE_PICKER_RANGE_END_CLASS = `${DATE_RANGE_PICKER_CLASS}__range-end`; +const DATE_PICKER = `.${DATE_PICKER_CLASS}`; +const DATE_RANGE_PICKER = `.${DATE_RANGE_PICKER_CLASS}`; +const DATE_RANGE_PICKER_RANGE_START = `.${DATE_RANGE_PICKER_RANGE_START_CLASS}`; +const DATE_RANGE_PICKER_RANGE_END = `.${DATE_RANGE_PICKER_RANGE_END_CLASS}`; +const DEFAULT_MIN_DATE = "0000-01-01"; + +/** + * The properties and elements within the date range picker. + * @typedef {Object} DateRangePickerContext + * @property {HTMLElement} dateRangePickerEl + * @property {HTMLElement} rangeStartEl + * @property {HTMLElement} rangeEndEl + */ + +/** + * Get an object of the properties and elements belonging directly to the given + * date picker component. + * + * @param {HTMLElement} el the element within the date picker + * @returns {DateRangePickerContext} elements + */ +const getDateRangePickerContext = el => { + const dateRangePickerEl = el.closest(DATE_RANGE_PICKER); + if (!dateRangePickerEl) { + throw new Error(`Element is missing outer ${DATE_RANGE_PICKER}`); + } + const rangeStartEl = dateRangePickerEl.querySelector(DATE_RANGE_PICKER_RANGE_START); + const rangeEndEl = dateRangePickerEl.querySelector(DATE_RANGE_PICKER_RANGE_END); + return { + dateRangePickerEl, + rangeStartEl, + rangeEndEl + }; +}; + +/** + * handle update from range start date picker + * + * @param {HTMLElement} el an element within the date range picker + */ +const handleRangeStartUpdate = el => { + const { + dateRangePickerEl, + rangeStartEl, + rangeEndEl + } = getDateRangePickerContext(el); + const { + internalInputEl + } = getDatePickerContext(rangeStartEl); + const updatedDate = internalInputEl.value; + if (updatedDate && !isDateInputInvalid(internalInputEl)) { + rangeEndEl.dataset.minDate = updatedDate; + rangeEndEl.dataset.rangeDate = updatedDate; + rangeEndEl.dataset.defaultDate = updatedDate; + } else { + rangeEndEl.dataset.minDate = dateRangePickerEl.dataset.minDate || ""; + rangeEndEl.dataset.rangeDate = ""; + rangeEndEl.dataset.defaultDate = ""; + } + updateCalendarIfVisible(rangeEndEl); +}; + +/** + * handle update from range start date picker + * + * @param {HTMLElement} el an element within the date range picker + */ +const handleRangeEndUpdate = el => { + const { + dateRangePickerEl, + rangeStartEl, + rangeEndEl + } = getDateRangePickerContext(el); + const { + internalInputEl + } = getDatePickerContext(rangeEndEl); + const updatedDate = internalInputEl.value; + if (updatedDate && !isDateInputInvalid(internalInputEl)) { + rangeStartEl.dataset.maxDate = updatedDate; + rangeStartEl.dataset.rangeDate = updatedDate; + rangeStartEl.dataset.defaultDate = updatedDate; + } else { + rangeStartEl.dataset.maxDate = dateRangePickerEl.dataset.maxDate || ""; + rangeStartEl.dataset.rangeDate = ""; + rangeStartEl.dataset.defaultDate = ""; + } + updateCalendarIfVisible(rangeStartEl); +}; + +/** + * Enhance an input with the date picker elements + * + * @param {HTMLElement} el The initial wrapping element of the date range picker component + */ +const enhanceDateRangePicker = el => { + const dateRangePickerEl = el.closest(DATE_RANGE_PICKER); + const [rangeStart, rangeEnd] = select(DATE_PICKER, dateRangePickerEl); + if (!rangeStart) { + throw new Error(`${DATE_RANGE_PICKER} is missing inner two '${DATE_PICKER}' elements`); + } + if (!rangeEnd) { + throw new Error(`${DATE_RANGE_PICKER} is missing second '${DATE_PICKER}' element`); + } + rangeStart.classList.add(DATE_RANGE_PICKER_RANGE_START_CLASS); + rangeEnd.classList.add(DATE_RANGE_PICKER_RANGE_END_CLASS); + if (!dateRangePickerEl.dataset.minDate) { + dateRangePickerEl.dataset.minDate = DEFAULT_MIN_DATE; + } + const { + minDate + } = dateRangePickerEl.dataset; + rangeStart.dataset.minDate = minDate; + rangeEnd.dataset.minDate = minDate; + const { + maxDate + } = dateRangePickerEl.dataset; + if (maxDate) { + rangeStart.dataset.maxDate = maxDate; + rangeEnd.dataset.maxDate = maxDate; + } + handleRangeStartUpdate(dateRangePickerEl); + handleRangeEndUpdate(dateRangePickerEl); +}; +const dateRangePicker = behavior({ + "input change": { + [DATE_RANGE_PICKER_RANGE_START]() { + handleRangeStartUpdate(this); + }, + [DATE_RANGE_PICKER_RANGE_END]() { + handleRangeEndUpdate(this); + } + } +}, { + init(root) { + select(DATE_RANGE_PICKER, root).forEach(dateRangePickerEl => { + enhanceDateRangePicker(dateRangePickerEl); + }); + } +}); +module.exports = dateRangePicker; + +},{"../config":34,"../utils/behavior":43,"../utils/select":49,"./date-picker":19}],21:[function(require,module,exports){ +"use strict"; + +const select = require("../utils/select"); +const behavior = require("../utils/behavior"); +const { + prefix: PREFIX +} = require("../config"); +const Sanitizer = require("../utils/sanitizer"); +const DROPZONE_CLASS = `${PREFIX}-file-input`; +const DROPZONE = `.${DROPZONE_CLASS}`; +const INPUT_CLASS = `${PREFIX}-file-input__input`; +const TARGET_CLASS = `${PREFIX}-file-input__target`; +const INPUT = `.${INPUT_CLASS}`; +const BOX_CLASS = `${PREFIX}-file-input__box`; +const INSTRUCTIONS_CLASS = `${PREFIX}-file-input__instructions`; +const PREVIEW_CLASS = `${PREFIX}-file-input__preview`; +const PREVIEW_HEADING_CLASS = `${PREFIX}-file-input__preview-heading`; +const DISABLED_CLASS = `${PREFIX}-file-input--disabled`; +const CHOOSE_CLASS = `${PREFIX}-file-input__choose`; +const ACCEPTED_FILE_MESSAGE_CLASS = `${PREFIX}-file-input__accepted-files-message`; +const DRAG_TEXT_CLASS = `${PREFIX}-file-input__drag-text`; +const DRAG_CLASS = `${PREFIX}-file-input--drag`; +const LOADING_CLASS = "is-loading"; +const HIDDEN_CLASS = "display-none"; +const INVALID_FILE_CLASS = "has-invalid-file"; +const GENERIC_PREVIEW_CLASS_NAME = `${PREFIX}-file-input__preview-image`; +const GENERIC_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--generic`; +const PDF_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--pdf`; +const WORD_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--word`; +const VIDEO_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--video`; +const EXCEL_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--excel`; +const SPACER_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; +let TYPE_IS_VALID = Boolean(true); // logic gate for change listener + +/** + * The properties and elements within the file input. + * @typedef {Object} FileInputContext + * @property {HTMLDivElement} dropZoneEl + * @property {HTMLInputElement} inputEl + */ + +/** + * Get an object of the properties and elements belonging directly to the given + * file input component. + * + * @param {HTMLElement} el the element within the file input + * @returns {FileInputContext} elements + */ +const getFileInputContext = el => { + const dropZoneEl = el.closest(DROPZONE); + if (!dropZoneEl) { + throw new Error(`Element is missing outer ${DROPZONE}`); + } + const inputEl = dropZoneEl.querySelector(INPUT); + return { + dropZoneEl, + inputEl + }; +}; + +/** + * Disable the file input component + * + * @param {HTMLElement} el An element within the file input component + */ +const disable = el => { + const { + dropZoneEl, + inputEl + } = getFileInputContext(el); + inputEl.disabled = true; + dropZoneEl.classList.add(DISABLED_CLASS); + dropZoneEl.setAttribute("aria-disabled", "true"); +}; + +/** + * Enable the file input component + * + * @param {HTMLElement} el An element within the file input component + */ +const enable = el => { + const { + dropZoneEl, + inputEl + } = getFileInputContext(el); + inputEl.disabled = false; + dropZoneEl.classList.remove(DISABLED_CLASS); + dropZoneEl.removeAttribute("aria-disabled"); +}; + +/** + * + * @param {String} s special characters + * @returns {String} replaces specified values + */ +const replaceName = s => { + const c = s.charCodeAt(0); + if (c === 32) return "-"; + if (c >= 65 && c <= 90) return `img_${s.toLowerCase()}`; + return `__${("000", c.toString(16)).slice(-4)}`; +}; + +/** + * Creates an ID name for each file that strips all invalid characters. + * @param {String} name - name of the file added to file input (searchvalue) + * @returns {String} same characters as the name with invalid chars removed (newvalue) + */ +const makeSafeForID = name => name.replace(/[^a-z0-9]/g, replaceName); + +// Takes a generated safe ID and creates a unique ID. +const createUniqueID = name => `${name}-${Math.floor(Date.now().toString() / 1000)}`; + +/** + * Builds full file input component + * @param {HTMLElement} fileInputEl - original file input on page + * @returns {HTMLElement|HTMLElement} - Instructions, target area div + */ +const buildFileInput = fileInputEl => { + const acceptsMultiple = fileInputEl.hasAttribute("multiple"); + const fileInputParent = document.createElement("div"); + const dropTarget = document.createElement("div"); + const box = document.createElement("div"); + const instructions = document.createElement("div"); + const disabled = fileInputEl.hasAttribute("disabled"); + let defaultAriaLabel; + + // Adds class names and other attributes + fileInputEl.classList.remove(DROPZONE_CLASS); + fileInputEl.classList.add(INPUT_CLASS); + fileInputParent.classList.add(DROPZONE_CLASS); + box.classList.add(BOX_CLASS); + instructions.classList.add(INSTRUCTIONS_CLASS); + instructions.setAttribute("aria-hidden", "true"); + dropTarget.classList.add(TARGET_CLASS); + // Encourage screenreader to read out aria changes immediately following upload status change + fileInputEl.setAttribute("aria-live", "polite"); + + // Adds child elements to the DOM + fileInputEl.parentNode.insertBefore(dropTarget, fileInputEl); + fileInputEl.parentNode.insertBefore(fileInputParent, dropTarget); + dropTarget.appendChild(fileInputEl); + fileInputParent.appendChild(dropTarget); + fileInputEl.parentNode.insertBefore(instructions, fileInputEl); + fileInputEl.parentNode.insertBefore(box, fileInputEl); + + // Disabled styling + if (disabled) { + disable(fileInputEl); + } + + // Sets instruction test and aria-label based on whether or not multiple files are accepted + if (acceptsMultiple) { + defaultAriaLabel = "No files selected"; + instructions.innerHTML = Sanitizer.escapeHTML`Drag files here or choose from folder`; + fileInputEl.setAttribute("aria-label", defaultAriaLabel); + fileInputEl.setAttribute("data-default-aria-label", defaultAriaLabel); + } else { + defaultAriaLabel = "No file selected"; + instructions.innerHTML = Sanitizer.escapeHTML`Drag file here or choose from folder`; + fileInputEl.setAttribute("aria-label", defaultAriaLabel); + fileInputEl.setAttribute("data-default-aria-label", defaultAriaLabel); + } + + // IE11 and Edge do not support drop files on file inputs, so we've removed text that indicates that + if (/rv:11.0/i.test(navigator.userAgent) || /Edge\/\d./i.test(navigator.userAgent)) { + fileInputParent.querySelector(`.${DRAG_TEXT_CLASS}`).outerHTML = ""; + } + return { + instructions, + dropTarget + }; +}; + +/** + * Removes image previews, we want to start with a clean list every time files are added to the file input + * @param {HTMLElement} dropTarget - target area div that encases the input + * @param {HTMLElement} instructions - text to inform users to drag or select files + */ +const removeOldPreviews = (dropTarget, instructions, inputAriaLabel) => { + const filePreviews = dropTarget.querySelectorAll(`.${PREVIEW_CLASS}`); + const fileInputElement = dropTarget.querySelector(INPUT); + const currentPreviewHeading = dropTarget.querySelector(`.${PREVIEW_HEADING_CLASS}`); + const currentErrorMessage = dropTarget.querySelector(`.${ACCEPTED_FILE_MESSAGE_CLASS}`); + + /** + * finds the parent of the passed node and removes the child + * @param {HTMLElement} node + */ + const removeImages = node => { + node.parentNode.removeChild(node); + }; + + // Remove the heading above the previews + if (currentPreviewHeading) { + currentPreviewHeading.outerHTML = ""; + } + + // Remove existing error messages + if (currentErrorMessage) { + currentErrorMessage.outerHTML = ""; + dropTarget.classList.remove(INVALID_FILE_CLASS); + } + + // Get rid of existing previews if they exist, show instructions + if (filePreviews !== null) { + if (instructions) { + instructions.classList.remove(HIDDEN_CLASS); + } + fileInputElement.setAttribute("aria-label", inputAriaLabel); + Array.prototype.forEach.call(filePreviews, removeImages); + } +}; + +/** + * When new files are applied to file input, this function generates previews + * and removes old ones. + * @param {event} e + * @param {HTMLElement} fileInputEl - file input element + * @param {HTMLElement} instructions - text to inform users to drag or select files + * @param {HTMLElement} dropTarget - target area div that encases the input + */ + +const handleChange = (e, fileInputEl, instructions, dropTarget) => { + const fileNames = e.target.files; + const filePreviewsHeading = document.createElement("div"); + const inputAriaLabel = fileInputEl.dataset.defaultAriaLabel; + const fileStore = []; + + // First, get rid of existing previews + removeOldPreviews(dropTarget, instructions, inputAriaLabel); + + // Then, iterate through files list and: + // 1. Add selected file list names to aria-label + // 2. Create previews + for (let i = 0; i < fileNames.length; i += 1) { + const reader = new FileReader(); + const fileName = fileNames[i].name; + + // Push updated file names into the store array + fileStore.push(fileName); + + // read out the store array via aria-label, wording options vary based on file count + if (i === 0) { + fileInputEl.setAttribute("aria-label", `You have selected the file: ${fileName}`); + } else if (i >= 1) { + fileInputEl.setAttribute("aria-label", `You have selected ${fileNames.length} files: ${fileStore.join(", ")}`); + } + + // Starts with a loading image while preview is created + reader.onloadstart = function createLoadingImage() { + const imageId = createUniqueID(makeSafeForID(fileName)); + instructions.insertAdjacentHTML("afterend", Sanitizer.escapeHTML`
\n
\n \n When autocomplete results are available use up and down arrows to review and enter to select.\n Touch device users, explore by touch or with swipe gestures.\n `\n );\n\n if (selectedOption) {\n const { inputEl } = getComboBoxContext(comboBoxEl);\n changeElementValue(selectEl, selectedOption.value);\n changeElementValue(inputEl, selectedOption.text);\n comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);\n }\n\n if (selectEl.disabled) {\n disable(comboBoxEl);\n selectEl.disabled = false;\n }\n\n comboBoxEl.dataset.enhanced = \"true\";\n};\n\n/**\n * Manage the focused element within the list options when\n * navigating via keyboard.\n *\n * @param {HTMLElement} el An anchor element within the combo box component\n * @param {HTMLElement} nextEl An element within the combo box component\n * @param {Object} options options\n * @param {boolean} options.skipFocus skip focus of highlighted item\n * @param {boolean} options.preventScroll should skip procedure to scroll to element\n */\nconst highlightOption = (el, nextEl, { skipFocus, preventScroll } = {}) => {\n const { inputEl, listEl, focusedOptionEl } = getComboBoxContext(el);\n\n if (focusedOptionEl) {\n focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS);\n focusedOptionEl.setAttribute(\"tabIndex\", \"-1\");\n }\n\n if (nextEl) {\n inputEl.setAttribute(\"aria-activedescendant\", nextEl.id);\n nextEl.setAttribute(\"tabIndex\", \"0\");\n nextEl.classList.add(LIST_OPTION_FOCUSED_CLASS);\n\n if (!preventScroll) {\n const optionBottom = nextEl.offsetTop + nextEl.offsetHeight;\n const currentBottom = listEl.scrollTop + listEl.offsetHeight;\n\n if (optionBottom > currentBottom) {\n listEl.scrollTop = optionBottom - listEl.offsetHeight;\n }\n\n if (nextEl.offsetTop < listEl.scrollTop) {\n listEl.scrollTop = nextEl.offsetTop;\n }\n }\n\n if (!skipFocus) {\n nextEl.focus({ preventScroll });\n }\n } else {\n inputEl.setAttribute(\"aria-activedescendant\", \"\");\n inputEl.focus();\n }\n};\n\n/**\n * Generate a dynamic regular expression based off of a replaceable and possibly filtered value.\n *\n * @param {string} el An element within the combo box component\n * @param {string} query The value to use in the regular expression\n * @param {object} extras An object of regular expressions to replace and filter the query\n */\nconst generateDynamicRegExp = (filter, query = \"\", extras = {}) => {\n const escapeRegExp = (text) =>\n text.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, \"\\\\$&\");\n\n let find = filter.replace(/{{(.*?)}}/g, (m, $1) => {\n const key = $1.trim();\n const queryFilter = extras[key];\n if (key !== \"query\" && queryFilter) {\n const matcher = new RegExp(queryFilter, \"i\");\n const matches = query.match(matcher);\n\n if (matches) {\n return escapeRegExp(matches[1]);\n }\n\n return \"\";\n }\n return escapeRegExp(query);\n });\n\n find = `^(?:${find})$`;\n\n return new RegExp(find, \"i\");\n};\n\n/**\n * Display the option list of a combo box component.\n *\n * @param {HTMLElement} el An element within the combo box component\n */\nconst displayList = (el) => {\n const {\n comboBoxEl,\n selectEl,\n inputEl,\n listEl,\n statusEl,\n isPristine,\n disableFiltering,\n } = getComboBoxContext(el);\n let selectedItemId;\n let firstFoundId;\n\n const listOptionBaseId = `${listEl.id}--option-`;\n\n const inputValue = (inputEl.value || \"\").toLowerCase();\n const filter = comboBoxEl.dataset.filter || DEFAULT_FILTER;\n const regex = generateDynamicRegExp(filter, inputValue, comboBoxEl.dataset);\n\n const options = [];\n for (let i = 0, len = selectEl.options.length; i < len; i += 1) {\n const optionEl = selectEl.options[i];\n const optionId = `${listOptionBaseId}${options.length}`;\n\n if (\n optionEl.value &&\n (disableFiltering ||\n isPristine ||\n !inputValue ||\n regex.test(optionEl.text))\n ) {\n if (selectEl.value && optionEl.value === selectEl.value) {\n selectedItemId = optionId;\n }\n\n if (disableFiltering && !firstFoundId && regex.test(optionEl.text)) {\n firstFoundId = optionId;\n }\n options.push(optionEl);\n }\n }\n\n const numOptions = options.length;\n const optionHtml = options.map((option, index) => {\n const optionId = `${listOptionBaseId}${index}`;\n const classes = [LIST_OPTION_CLASS];\n let tabindex = \"-1\";\n let ariaSelected = \"false\";\n\n if (optionId === selectedItemId) {\n classes.push(LIST_OPTION_SELECTED_CLASS, LIST_OPTION_FOCUSED_CLASS);\n tabindex = \"0\";\n ariaSelected = \"true\";\n }\n\n if (!selectedItemId && index === 0) {\n classes.push(LIST_OPTION_FOCUSED_CLASS);\n tabindex = \"0\";\n }\n\n const li = document.createElement(\"li\");\n\n li.setAttribute(\"aria-setsize\", options.length);\n li.setAttribute(\"aria-posinset\", index + 1);\n li.setAttribute(\"aria-selected\", ariaSelected);\n li.setAttribute(\"id\", optionId);\n li.setAttribute(\"class\", classes.join(\" \"));\n li.setAttribute(\"tabindex\", tabindex);\n li.setAttribute(\"role\", \"option\");\n li.setAttribute(\"data-value\", option.value);\n li.textContent = option.text;\n\n return li;\n });\n\n const noResults = document.createElement(\"li\");\n noResults.setAttribute(\"class\", `${LIST_OPTION_CLASS}--no-results`);\n noResults.textContent = \"No results found\";\n\n listEl.hidden = false;\n\n if (numOptions) {\n listEl.innerHTML = \"\";\n optionHtml.forEach((item) =>\n listEl.insertAdjacentElement(\"beforeend\", item)\n );\n } else {\n listEl.innerHTML = \"\";\n listEl.insertAdjacentElement(\"beforeend\", noResults);\n }\n\n inputEl.setAttribute(\"aria-expanded\", \"true\");\n\n statusEl.textContent = numOptions\n ? `${numOptions} result${numOptions > 1 ? \"s\" : \"\"} available.`\n : \"No results.\";\n\n let itemToFocus;\n\n if (isPristine && selectedItemId) {\n itemToFocus = listEl.querySelector(`#${selectedItemId}`);\n } else if (disableFiltering && firstFoundId) {\n itemToFocus = listEl.querySelector(`#${firstFoundId}`);\n }\n\n if (itemToFocus) {\n highlightOption(listEl, itemToFocus, {\n skipFocus: true,\n });\n }\n};\n\n/**\n * Hide the option list of a combo box component.\n *\n * @param {HTMLElement} el An element within the combo box component\n */\nconst hideList = (el) => {\n const { inputEl, listEl, statusEl, focusedOptionEl } = getComboBoxContext(el);\n\n statusEl.innerHTML = \"\";\n\n inputEl.setAttribute(\"aria-expanded\", \"false\");\n inputEl.setAttribute(\"aria-activedescendant\", \"\");\n\n if (focusedOptionEl) {\n focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS);\n }\n\n listEl.scrollTop = 0;\n listEl.hidden = true;\n};\n\n/**\n * Select an option list of the combo box component.\n *\n * @param {HTMLElement} listOptionEl The list option being selected\n */\nconst selectItem = (listOptionEl) => {\n const { comboBoxEl, selectEl, inputEl } = getComboBoxContext(listOptionEl);\n\n changeElementValue(selectEl, listOptionEl.dataset.value);\n changeElementValue(inputEl, listOptionEl.textContent);\n comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);\n hideList(comboBoxEl);\n inputEl.focus();\n};\n\n/**\n * Clear the input of the combo box\n *\n * @param {HTMLButtonElement} clearButtonEl The clear input button\n */\nconst clearInput = (clearButtonEl) => {\n const { comboBoxEl, listEl, selectEl, inputEl } =\n getComboBoxContext(clearButtonEl);\n const listShown = !listEl.hidden;\n\n if (selectEl.value) changeElementValue(selectEl);\n if (inputEl.value) changeElementValue(inputEl);\n comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS);\n\n if (listShown) displayList(comboBoxEl);\n inputEl.focus();\n};\n\n/**\n * Reset the select based off of currently set select value\n *\n * @param {HTMLElement} el An element within the combo box component\n */\nconst resetSelection = (el) => {\n const { comboBoxEl, selectEl, inputEl } = getComboBoxContext(el);\n\n const selectValue = selectEl.value;\n const inputValue = (inputEl.value || \"\").toLowerCase();\n\n if (selectValue) {\n for (let i = 0, len = selectEl.options.length; i < len; i += 1) {\n const optionEl = selectEl.options[i];\n if (optionEl.value === selectValue) {\n if (inputValue !== optionEl.text) {\n changeElementValue(inputEl, optionEl.text);\n }\n comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);\n return;\n }\n }\n }\n\n if (inputValue) {\n changeElementValue(inputEl);\n }\n};\n\n/**\n * Select an option list of the combo box component based off of\n * having a current focused list option or\n * having test that completely matches a list option.\n * Otherwise it clears the input and select.\n *\n * @param {HTMLElement} el An element within the combo box component\n */\nconst completeSelection = (el) => {\n const { comboBoxEl, selectEl, inputEl, statusEl } = getComboBoxContext(el);\n\n statusEl.textContent = \"\";\n\n const inputValue = (inputEl.value || \"\").toLowerCase();\n\n if (inputValue) {\n for (let i = 0, len = selectEl.options.length; i < len; i += 1) {\n const optionEl = selectEl.options[i];\n if (optionEl.text.toLowerCase() === inputValue) {\n changeElementValue(selectEl, optionEl.value);\n changeElementValue(inputEl, optionEl.text);\n comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);\n return;\n }\n }\n }\n\n resetSelection(comboBoxEl);\n};\n\n/**\n * Handle the escape event within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleEscape = (event) => {\n const { comboBoxEl, inputEl } = getComboBoxContext(event.target);\n\n hideList(comboBoxEl);\n resetSelection(comboBoxEl);\n inputEl.focus();\n};\n\n/**\n * Handle the down event within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleDownFromInput = (event) => {\n const { comboBoxEl, listEl } = getComboBoxContext(event.target);\n\n if (listEl.hidden) {\n displayList(comboBoxEl);\n }\n\n const nextOptionEl =\n listEl.querySelector(LIST_OPTION_FOCUSED) ||\n listEl.querySelector(LIST_OPTION);\n\n if (nextOptionEl) {\n highlightOption(comboBoxEl, nextOptionEl);\n }\n\n event.preventDefault();\n};\n\n/**\n * Handle the enter event from an input element within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleEnterFromInput = (event) => {\n const { comboBoxEl, listEl } = getComboBoxContext(event.target);\n const listShown = !listEl.hidden;\n\n completeSelection(comboBoxEl);\n\n if (listShown) {\n hideList(comboBoxEl);\n }\n\n event.preventDefault();\n};\n\n/**\n * Handle the down event within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleDownFromListOption = (event) => {\n const focusedOptionEl = event.target;\n const nextOptionEl = focusedOptionEl.nextSibling;\n\n if (nextOptionEl) {\n highlightOption(focusedOptionEl, nextOptionEl);\n }\n\n event.preventDefault();\n};\n\n/**\n * Handle the tab event from an list option element within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleTabFromListOption = (event) => {\n selectItem(event.target);\n event.preventDefault();\n};\n\n/**\n * Handle the enter event from list option within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleEnterFromListOption = (event) => {\n selectItem(event.target);\n event.preventDefault();\n};\n\n/**\n * Handle the up event from list option within the combo box component.\n *\n * @param {KeyboardEvent} event An event within the combo box component\n */\nconst handleUpFromListOption = (event) => {\n const { comboBoxEl, listEl, focusedOptionEl } = getComboBoxContext(\n event.target\n );\n const nextOptionEl = focusedOptionEl && focusedOptionEl.previousSibling;\n const listShown = !listEl.hidden;\n\n highlightOption(comboBoxEl, nextOptionEl);\n\n if (listShown) {\n event.preventDefault();\n }\n\n if (!nextOptionEl) {\n hideList(comboBoxEl);\n }\n};\n\n/**\n * Select list option on the mouseover event.\n *\n * @param {MouseEvent} event The mouseover event\n * @param {HTMLLIElement} listOptionEl An element within the combo box component\n */\nconst handleMouseover = (listOptionEl) => {\n const isCurrentlyFocused = listOptionEl.classList.contains(\n LIST_OPTION_FOCUSED_CLASS\n );\n\n if (isCurrentlyFocused) return;\n\n highlightOption(listOptionEl, listOptionEl, {\n preventScroll: true,\n });\n};\n\n/**\n * Toggle the list when the button is clicked\n *\n * @param {HTMLElement} el An element within the combo box component\n */\nconst toggleList = (el) => {\n const { comboBoxEl, listEl, inputEl } = getComboBoxContext(el);\n\n if (listEl.hidden) {\n displayList(comboBoxEl);\n } else {\n hideList(comboBoxEl);\n }\n\n inputEl.focus();\n};\n\n/**\n * Handle click from input\n *\n * @param {HTMLInputElement} el An element within the combo box component\n */\nconst handleClickFromInput = (el) => {\n const { comboBoxEl, listEl } = getComboBoxContext(el);\n\n if (listEl.hidden) {\n displayList(comboBoxEl);\n }\n};\n\nconst comboBox = behavior(\n {\n [CLICK]: {\n [INPUT]() {\n if (this.disabled) return;\n handleClickFromInput(this);\n },\n [TOGGLE_LIST_BUTTON]() {\n if (this.disabled) return;\n toggleList(this);\n },\n [LIST_OPTION]() {\n if (this.disabled) return;\n selectItem(this);\n },\n [CLEAR_INPUT_BUTTON]() {\n if (this.disabled) return;\n clearInput(this);\n },\n },\n focusout: {\n [COMBO_BOX](event) {\n if (!this.contains(event.relatedTarget)) {\n resetSelection(this);\n hideList(this);\n }\n },\n },\n keydown: {\n [COMBO_BOX]: keymap({\n Escape: handleEscape,\n }),\n [INPUT]: keymap({\n Enter: handleEnterFromInput,\n ArrowDown: handleDownFromInput,\n Down: handleDownFromInput,\n }),\n [LIST_OPTION]: keymap({\n ArrowUp: handleUpFromListOption,\n Up: handleUpFromListOption,\n ArrowDown: handleDownFromListOption,\n Down: handleDownFromListOption,\n Enter: handleEnterFromListOption,\n Tab: handleTabFromListOption,\n \"Shift+Tab\": noop,\n }),\n },\n input: {\n [INPUT]() {\n const comboBoxEl = this.closest(COMBO_BOX);\n comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS);\n displayList(this);\n },\n },\n mouseover: {\n [LIST_OPTION]() {\n handleMouseover(this);\n },\n },\n },\n {\n init(root) {\n select(COMBO_BOX, root).forEach((comboBoxEl) => {\n enhanceComboBox(comboBoxEl);\n });\n },\n getComboBoxContext,\n enhanceComboBox,\n generateDynamicRegExp,\n disable,\n enable,\n displayList,\n hideList,\n COMBO_BOX_CLASS,\n }\n);\n\nmodule.exports = comboBox;\n","const keymap = require(\"receptor/keymap\");\nconst behavior = require(\"../utils/behavior\");\nconst select = require(\"../utils/select\");\nconst { prefix: PREFIX } = require(\"../config\");\nconst { CLICK } = require(\"../events\");\nconst activeElement = require(\"../utils/active-element\");\nconst isIosDevice = require(\"../utils/is-ios-device\");\nconst Sanitizer = require(\"../utils/sanitizer\");\n\nconst DATE_PICKER_CLASS = `${PREFIX}-date-picker`;\nconst DATE_PICKER_WRAPPER_CLASS = `${DATE_PICKER_CLASS}__wrapper`;\nconst DATE_PICKER_INITIALIZED_CLASS = `${DATE_PICKER_CLASS}--initialized`;\nconst DATE_PICKER_ACTIVE_CLASS = `${DATE_PICKER_CLASS}--active`;\nconst DATE_PICKER_INTERNAL_INPUT_CLASS = `${DATE_PICKER_CLASS}__internal-input`;\nconst DATE_PICKER_EXTERNAL_INPUT_CLASS = `${DATE_PICKER_CLASS}__external-input`;\nconst DATE_PICKER_BUTTON_CLASS = `${DATE_PICKER_CLASS}__button`;\nconst DATE_PICKER_CALENDAR_CLASS = `${DATE_PICKER_CLASS}__calendar`;\nconst DATE_PICKER_STATUS_CLASS = `${DATE_PICKER_CLASS}__status`;\nconst CALENDAR_DATE_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__date`;\n\nconst CALENDAR_DATE_FOCUSED_CLASS = `${CALENDAR_DATE_CLASS}--focused`;\nconst CALENDAR_DATE_SELECTED_CLASS = `${CALENDAR_DATE_CLASS}--selected`;\nconst CALENDAR_DATE_PREVIOUS_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--previous-month`;\nconst CALENDAR_DATE_CURRENT_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--current-month`;\nconst CALENDAR_DATE_NEXT_MONTH_CLASS = `${CALENDAR_DATE_CLASS}--next-month`;\nconst CALENDAR_DATE_RANGE_DATE_CLASS = `${CALENDAR_DATE_CLASS}--range-date`;\nconst CALENDAR_DATE_TODAY_CLASS = `${CALENDAR_DATE_CLASS}--today`;\nconst CALENDAR_DATE_RANGE_DATE_START_CLASS = `${CALENDAR_DATE_CLASS}--range-date-start`;\nconst CALENDAR_DATE_RANGE_DATE_END_CLASS = `${CALENDAR_DATE_CLASS}--range-date-end`;\nconst CALENDAR_DATE_WITHIN_RANGE_CLASS = `${CALENDAR_DATE_CLASS}--within-range`;\nconst CALENDAR_PREVIOUS_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-year`;\nconst CALENDAR_PREVIOUS_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-month`;\nconst CALENDAR_NEXT_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-year`;\nconst CALENDAR_NEXT_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-month`;\nconst CALENDAR_MONTH_SELECTION_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-selection`;\nconst CALENDAR_YEAR_SELECTION_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year-selection`;\nconst CALENDAR_MONTH_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month`;\nconst CALENDAR_MONTH_FOCUSED_CLASS = `${CALENDAR_MONTH_CLASS}--focused`;\nconst CALENDAR_MONTH_SELECTED_CLASS = `${CALENDAR_MONTH_CLASS}--selected`;\nconst CALENDAR_YEAR_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year`;\nconst CALENDAR_YEAR_FOCUSED_CLASS = `${CALENDAR_YEAR_CLASS}--focused`;\nconst CALENDAR_YEAR_SELECTED_CLASS = `${CALENDAR_YEAR_CLASS}--selected`;\nconst CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__previous-year-chunk`;\nconst CALENDAR_NEXT_YEAR_CHUNK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__next-year-chunk`;\nconst CALENDAR_DATE_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__date-picker`;\nconst CALENDAR_MONTH_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-picker`;\nconst CALENDAR_YEAR_PICKER_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__year-picker`;\nconst CALENDAR_TABLE_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__table`;\nconst CALENDAR_ROW_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__row`;\nconst CALENDAR_CELL_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__cell`;\nconst CALENDAR_CELL_CENTER_ITEMS_CLASS = `${CALENDAR_CELL_CLASS}--center-items`;\nconst CALENDAR_MONTH_LABEL_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__month-label`;\nconst CALENDAR_DAY_OF_WEEK_CLASS = `${DATE_PICKER_CALENDAR_CLASS}__day-of-week`;\n\nconst DATE_PICKER = `.${DATE_PICKER_CLASS}`;\nconst DATE_PICKER_BUTTON = `.${DATE_PICKER_BUTTON_CLASS}`;\nconst DATE_PICKER_INTERNAL_INPUT = `.${DATE_PICKER_INTERNAL_INPUT_CLASS}`;\nconst DATE_PICKER_EXTERNAL_INPUT = `.${DATE_PICKER_EXTERNAL_INPUT_CLASS}`;\nconst DATE_PICKER_CALENDAR = `.${DATE_PICKER_CALENDAR_CLASS}`;\nconst DATE_PICKER_STATUS = `.${DATE_PICKER_STATUS_CLASS}`;\nconst CALENDAR_DATE = `.${CALENDAR_DATE_CLASS}`;\nconst CALENDAR_DATE_FOCUSED = `.${CALENDAR_DATE_FOCUSED_CLASS}`;\nconst CALENDAR_DATE_CURRENT_MONTH = `.${CALENDAR_DATE_CURRENT_MONTH_CLASS}`;\nconst CALENDAR_PREVIOUS_YEAR = `.${CALENDAR_PREVIOUS_YEAR_CLASS}`;\nconst CALENDAR_PREVIOUS_MONTH = `.${CALENDAR_PREVIOUS_MONTH_CLASS}`;\nconst CALENDAR_NEXT_YEAR = `.${CALENDAR_NEXT_YEAR_CLASS}`;\nconst CALENDAR_NEXT_MONTH = `.${CALENDAR_NEXT_MONTH_CLASS}`;\nconst CALENDAR_YEAR_SELECTION = `.${CALENDAR_YEAR_SELECTION_CLASS}`;\nconst CALENDAR_MONTH_SELECTION = `.${CALENDAR_MONTH_SELECTION_CLASS}`;\nconst CALENDAR_MONTH = `.${CALENDAR_MONTH_CLASS}`;\nconst CALENDAR_YEAR = `.${CALENDAR_YEAR_CLASS}`;\nconst CALENDAR_PREVIOUS_YEAR_CHUNK = `.${CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS}`;\nconst CALENDAR_NEXT_YEAR_CHUNK = `.${CALENDAR_NEXT_YEAR_CHUNK_CLASS}`;\nconst CALENDAR_DATE_PICKER = `.${CALENDAR_DATE_PICKER_CLASS}`;\nconst CALENDAR_MONTH_PICKER = `.${CALENDAR_MONTH_PICKER_CLASS}`;\nconst CALENDAR_YEAR_PICKER = `.${CALENDAR_YEAR_PICKER_CLASS}`;\nconst CALENDAR_MONTH_FOCUSED = `.${CALENDAR_MONTH_FOCUSED_CLASS}`;\nconst CALENDAR_YEAR_FOCUSED = `.${CALENDAR_YEAR_FOCUSED_CLASS}`;\n\nconst VALIDATION_MESSAGE = \"Please enter a valid date\";\n\nconst MONTH_LABELS = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n];\n\nconst DAY_OF_WEEK_LABELS = [\n \"Sunday\",\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n];\n\nconst ENTER_KEYCODE = 13;\n\nconst YEAR_CHUNK = 12;\n\nconst DEFAULT_MIN_DATE = \"0000-01-01\";\nconst DEFAULT_EXTERNAL_DATE_FORMAT = \"MM/DD/YYYY\";\nconst INTERNAL_DATE_FORMAT = \"YYYY-MM-DD\";\n\nconst NOT_DISABLED_SELECTOR = \":not([disabled])\";\n\nconst processFocusableSelectors = (...selectors) =>\n selectors.map((query) => query + NOT_DISABLED_SELECTOR).join(\", \");\n\nconst DATE_PICKER_FOCUSABLE = processFocusableSelectors(\n CALENDAR_PREVIOUS_YEAR,\n CALENDAR_PREVIOUS_MONTH,\n CALENDAR_YEAR_SELECTION,\n CALENDAR_MONTH_SELECTION,\n CALENDAR_NEXT_YEAR,\n CALENDAR_NEXT_MONTH,\n CALENDAR_DATE_FOCUSED\n);\n\nconst MONTH_PICKER_FOCUSABLE = processFocusableSelectors(\n CALENDAR_MONTH_FOCUSED\n);\n\nconst YEAR_PICKER_FOCUSABLE = processFocusableSelectors(\n CALENDAR_PREVIOUS_YEAR_CHUNK,\n CALENDAR_NEXT_YEAR_CHUNK,\n CALENDAR_YEAR_FOCUSED\n);\n\n// #region Date Manipulation Functions\n\n/**\n * Keep date within month. Month would only be over by 1 to 3 days\n *\n * @param {Date} dateToCheck the date object to check\n * @param {number} month the correct month\n * @returns {Date} the date, corrected if needed\n */\nconst keepDateWithinMonth = (dateToCheck, month) => {\n if (month !== dateToCheck.getMonth()) {\n dateToCheck.setDate(0);\n }\n\n return dateToCheck;\n};\n\n/**\n * Set date from month day year\n *\n * @param {number} year the year to set\n * @param {number} month the month to set (zero-indexed)\n * @param {number} date the date to set\n * @returns {Date} the set date\n */\nconst setDate = (year, month, date) => {\n const newDate = new Date(0);\n newDate.setFullYear(year, month, date);\n return newDate;\n};\n\n/**\n * todays date\n *\n * @returns {Date} todays date\n */\nconst today = () => {\n const newDate = new Date();\n const day = newDate.getDate();\n const month = newDate.getMonth();\n const year = newDate.getFullYear();\n return setDate(year, month, day);\n};\n\n/**\n * Set date to first day of the month\n *\n * @param {number} date the date to adjust\n * @returns {Date} the adjusted date\n */\nconst startOfMonth = (date) => {\n const newDate = new Date(0);\n newDate.setFullYear(date.getFullYear(), date.getMonth(), 1);\n return newDate;\n};\n\n/**\n * Set date to last day of the month\n *\n * @param {number} date the date to adjust\n * @returns {Date} the adjusted date\n */\nconst lastDayOfMonth = (date) => {\n const newDate = new Date(0);\n newDate.setFullYear(date.getFullYear(), date.getMonth() + 1, 0);\n return newDate;\n};\n\n/**\n * Add days to date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numDays the difference in days\n * @returns {Date} the adjusted date\n */\nconst addDays = (_date, numDays) => {\n const newDate = new Date(_date.getTime());\n newDate.setDate(newDate.getDate() + numDays);\n return newDate;\n};\n\n/**\n * Subtract days from date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numDays the difference in days\n * @returns {Date} the adjusted date\n */\nconst subDays = (_date, numDays) => addDays(_date, -numDays);\n\n/**\n * Add weeks to date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numWeeks the difference in weeks\n * @returns {Date} the adjusted date\n */\nconst addWeeks = (_date, numWeeks) => addDays(_date, numWeeks * 7);\n\n/**\n * Subtract weeks from date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numWeeks the difference in weeks\n * @returns {Date} the adjusted date\n */\nconst subWeeks = (_date, numWeeks) => addWeeks(_date, -numWeeks);\n\n/**\n * Set date to the start of the week (Sunday)\n *\n * @param {Date} _date the date to adjust\n * @returns {Date} the adjusted date\n */\nconst startOfWeek = (_date) => {\n const dayOfWeek = _date.getDay();\n return subDays(_date, dayOfWeek);\n};\n\n/**\n * Set date to the end of the week (Saturday)\n *\n * @param {Date} _date the date to adjust\n * @param {number} numWeeks the difference in weeks\n * @returns {Date} the adjusted date\n */\nconst endOfWeek = (_date) => {\n const dayOfWeek = _date.getDay();\n return addDays(_date, 6 - dayOfWeek);\n};\n\n/**\n * Add months to date and keep date within month\n *\n * @param {Date} _date the date to adjust\n * @param {number} numMonths the difference in months\n * @returns {Date} the adjusted date\n */\nconst addMonths = (_date, numMonths) => {\n const newDate = new Date(_date.getTime());\n\n const dateMonth = (newDate.getMonth() + 12 + numMonths) % 12;\n newDate.setMonth(newDate.getMonth() + numMonths);\n keepDateWithinMonth(newDate, dateMonth);\n\n return newDate;\n};\n\n/**\n * Subtract months from date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numMonths the difference in months\n * @returns {Date} the adjusted date\n */\nconst subMonths = (_date, numMonths) => addMonths(_date, -numMonths);\n\n/**\n * Add years to date and keep date within month\n *\n * @param {Date} _date the date to adjust\n * @param {number} numYears the difference in years\n * @returns {Date} the adjusted date\n */\nconst addYears = (_date, numYears) => addMonths(_date, numYears * 12);\n\n/**\n * Subtract years from date\n *\n * @param {Date} _date the date to adjust\n * @param {number} numYears the difference in years\n * @returns {Date} the adjusted date\n */\nconst subYears = (_date, numYears) => addYears(_date, -numYears);\n\n/**\n * Set months of date\n *\n * @param {Date} _date the date to adjust\n * @param {number} month zero-indexed month to set\n * @returns {Date} the adjusted date\n */\nconst setMonth = (_date, month) => {\n const newDate = new Date(_date.getTime());\n\n newDate.setMonth(month);\n keepDateWithinMonth(newDate, month);\n\n return newDate;\n};\n\n/**\n * Set year of date\n *\n * @param {Date} _date the date to adjust\n * @param {number} year the year to set\n * @returns {Date} the adjusted date\n */\nconst setYear = (_date, year) => {\n const newDate = new Date(_date.getTime());\n\n const month = newDate.getMonth();\n newDate.setFullYear(year);\n keepDateWithinMonth(newDate, month);\n\n return newDate;\n};\n\n/**\n * Return the earliest date\n *\n * @param {Date} dateA date to compare\n * @param {Date} dateB date to compare\n * @returns {Date} the earliest date\n */\nconst min = (dateA, dateB) => {\n let newDate = dateA;\n\n if (dateB < dateA) {\n newDate = dateB;\n }\n\n return new Date(newDate.getTime());\n};\n\n/**\n * Return the latest date\n *\n * @param {Date} dateA date to compare\n * @param {Date} dateB date to compare\n * @returns {Date} the latest date\n */\nconst max = (dateA, dateB) => {\n let newDate = dateA;\n\n if (dateB > dateA) {\n newDate = dateB;\n }\n\n return new Date(newDate.getTime());\n};\n\n/**\n * Check if dates are the in the same year\n *\n * @param {Date} dateA date to compare\n * @param {Date} dateB date to compare\n * @returns {boolean} are dates in the same year\n */\nconst isSameYear = (dateA, dateB) =>\n dateA && dateB && dateA.getFullYear() === dateB.getFullYear();\n\n/**\n * Check if dates are the in the same month\n *\n * @param {Date} dateA date to compare\n * @param {Date} dateB date to compare\n * @returns {boolean} are dates in the same month\n */\nconst isSameMonth = (dateA, dateB) =>\n isSameYear(dateA, dateB) && dateA.getMonth() === dateB.getMonth();\n\n/**\n * Check if dates are the same date\n *\n * @param {Date} dateA the date to compare\n * @param {Date} dateA the date to compare\n * @returns {boolean} are dates the same date\n */\nconst isSameDay = (dateA, dateB) =>\n isSameMonth(dateA, dateB) && dateA.getDate() === dateB.getDate();\n\n/**\n * return a new date within minimum and maximum date\n *\n * @param {Date} date date to check\n * @param {Date} minDate minimum date to allow\n * @param {Date} maxDate maximum date to allow\n * @returns {Date} the date between min and max\n */\nconst keepDateBetweenMinAndMax = (date, minDate, maxDate) => {\n let newDate = date;\n\n if (date < minDate) {\n newDate = minDate;\n } else if (maxDate && date > maxDate) {\n newDate = maxDate;\n }\n\n return new Date(newDate.getTime());\n};\n\n/**\n * Check if dates is valid.\n *\n * @param {Date} date date to check\n * @param {Date} minDate minimum date to allow\n * @param {Date} maxDate maximum date to allow\n * @return {boolean} is there a day within the month within min and max dates\n */\nconst isDateWithinMinAndMax = (date, minDate, maxDate) =>\n date >= minDate && (!maxDate || date <= maxDate);\n\n/**\n * Check if dates month is invalid.\n *\n * @param {Date} date date to check\n * @param {Date} minDate minimum date to allow\n * @param {Date} maxDate maximum date to allow\n * @return {boolean} is the month outside min or max dates\n */\nconst isDatesMonthOutsideMinOrMax = (date, minDate, maxDate) =>\n lastDayOfMonth(date) < minDate || (maxDate && startOfMonth(date) > maxDate);\n\n/**\n * Check if dates year is invalid.\n *\n * @param {Date} date date to check\n * @param {Date} minDate minimum date to allow\n * @param {Date} maxDate maximum date to allow\n * @return {boolean} is the month outside min or max dates\n */\nconst isDatesYearOutsideMinOrMax = (date, minDate, maxDate) =>\n lastDayOfMonth(setMonth(date, 11)) < minDate ||\n (maxDate && startOfMonth(setMonth(date, 0)) > maxDate);\n\n/**\n * Parse a date with format M-D-YY\n *\n * @param {string} dateString the date string to parse\n * @param {string} dateFormat the format of the date string\n * @param {boolean} adjustDate should the date be adjusted\n * @returns {Date} the parsed date\n */\nconst parseDateString = (\n dateString,\n dateFormat = INTERNAL_DATE_FORMAT,\n adjustDate = false\n) => {\n let date;\n let month;\n let day;\n let year;\n let parsed;\n\n if (dateString) {\n let monthStr;\n let dayStr;\n let yearStr;\n\n if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) {\n [monthStr, dayStr, yearStr] = dateString.split(\"/\");\n } else {\n [yearStr, monthStr, dayStr] = dateString.split(\"-\");\n }\n\n if (yearStr) {\n parsed = parseInt(yearStr, 10);\n if (!Number.isNaN(parsed)) {\n year = parsed;\n if (adjustDate) {\n year = Math.max(0, year);\n if (yearStr.length < 3) {\n const currentYear = today().getFullYear();\n const currentYearStub =\n currentYear - (currentYear % 10 ** yearStr.length);\n year = currentYearStub + parsed;\n }\n }\n }\n }\n\n if (monthStr) {\n parsed = parseInt(monthStr, 10);\n if (!Number.isNaN(parsed)) {\n month = parsed;\n if (adjustDate) {\n month = Math.max(1, month);\n month = Math.min(12, month);\n }\n }\n }\n\n if (month && dayStr && year != null) {\n parsed = parseInt(dayStr, 10);\n if (!Number.isNaN(parsed)) {\n day = parsed;\n if (adjustDate) {\n const lastDayOfTheMonth = setDate(year, month, 0).getDate();\n day = Math.max(1, day);\n day = Math.min(lastDayOfTheMonth, day);\n }\n }\n }\n\n if (month && day && year != null) {\n date = setDate(year, month - 1, day);\n }\n }\n\n return date;\n};\n\n/**\n * Format a date to format MM-DD-YYYY\n *\n * @param {Date} date the date to format\n * @param {string} dateFormat the format of the date string\n * @returns {string} the formatted date string\n */\nconst formatDate = (date, dateFormat = INTERNAL_DATE_FORMAT) => {\n const padZeros = (value, length) => `0000${value}`.slice(-length);\n\n const month = date.getMonth() + 1;\n const day = date.getDate();\n const year = date.getFullYear();\n\n if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) {\n return [padZeros(month, 2), padZeros(day, 2), padZeros(year, 4)].join(\"/\");\n }\n\n return [padZeros(year, 4), padZeros(month, 2), padZeros(day, 2)].join(\"-\");\n};\n\n// #endregion Date Manipulation Functions\n\n/**\n * Create a grid string from an array of html strings\n *\n * @param {string[]} htmlArray the array of html items\n * @param {number} rowSize the length of a row\n * @returns {string} the grid string\n */\nconst listToGridHtml = (htmlArray, rowSize) => {\n const grid = [];\n let row = [];\n\n let i = 0;\n while (i < htmlArray.length) {\n row = [];\n\n const tr = document.createElement(\"tr\");\n while (i < htmlArray.length && row.length < rowSize) {\n const td = document.createElement(\"td\");\n td.insertAdjacentElement(\"beforeend\", htmlArray[i]);\n row.push(td);\n i += 1;\n }\n\n row.forEach((element) => {\n tr.insertAdjacentElement(\"beforeend\", element);\n });\n\n grid.push(tr);\n }\n\n return grid;\n};\n\nconst createTableBody = (grid) => {\n const tableBody = document.createElement(\"tbody\");\n grid.forEach((element) => {\n tableBody.insertAdjacentElement(\"beforeend\", element);\n });\n\n return tableBody;\n};\n\n/**\n * set the value of the element and dispatch a change event\n *\n * @param {HTMLInputElement} el The element to update\n * @param {string} value The new value of the element\n */\nconst changeElementValue = (el, value = \"\") => {\n const elementToChange = el;\n elementToChange.value = value;\n\n const event = new CustomEvent(\"change\", {\n bubbles: true,\n cancelable: true,\n detail: { value },\n });\n elementToChange.dispatchEvent(event);\n};\n\n/**\n * The properties and elements within the date picker.\n * @typedef {Object} DatePickerContext\n * @property {HTMLDivElement} calendarEl\n * @property {HTMLElement} datePickerEl\n * @property {HTMLInputElement} internalInputEl\n * @property {HTMLInputElement} externalInputEl\n * @property {HTMLDivElement} statusEl\n * @property {HTMLDivElement} firstYearChunkEl\n * @property {Date} calendarDate\n * @property {Date} minDate\n * @property {Date} maxDate\n * @property {Date} selectedDate\n * @property {Date} rangeDate\n * @property {Date} defaultDate\n */\n\n/**\n * Get an object of the properties and elements belonging directly to the given\n * date picker component.\n *\n * @param {HTMLElement} el the element within the date picker\n * @returns {DatePickerContext} elements\n */\nconst getDatePickerContext = (el) => {\n const datePickerEl = el.closest(DATE_PICKER);\n\n if (!datePickerEl) {\n throw new Error(`Element is missing outer ${DATE_PICKER}`);\n }\n\n const internalInputEl = datePickerEl.querySelector(\n DATE_PICKER_INTERNAL_INPUT\n );\n const externalInputEl = datePickerEl.querySelector(\n DATE_PICKER_EXTERNAL_INPUT\n );\n const calendarEl = datePickerEl.querySelector(DATE_PICKER_CALENDAR);\n const toggleBtnEl = datePickerEl.querySelector(DATE_PICKER_BUTTON);\n const statusEl = datePickerEl.querySelector(DATE_PICKER_STATUS);\n const firstYearChunkEl = datePickerEl.querySelector(CALENDAR_YEAR);\n\n const inputDate = parseDateString(\n externalInputEl.value,\n DEFAULT_EXTERNAL_DATE_FORMAT,\n true\n );\n const selectedDate = parseDateString(internalInputEl.value);\n\n const calendarDate = parseDateString(calendarEl.dataset.value);\n const minDate = parseDateString(datePickerEl.dataset.minDate);\n const maxDate = parseDateString(datePickerEl.dataset.maxDate);\n const rangeDate = parseDateString(datePickerEl.dataset.rangeDate);\n const defaultDate = parseDateString(datePickerEl.dataset.defaultDate);\n\n if (minDate && maxDate && minDate > maxDate) {\n throw new Error(\"Minimum date cannot be after maximum date\");\n }\n\n return {\n calendarDate,\n minDate,\n toggleBtnEl,\n selectedDate,\n maxDate,\n firstYearChunkEl,\n datePickerEl,\n inputDate,\n internalInputEl,\n externalInputEl,\n calendarEl,\n rangeDate,\n defaultDate,\n statusEl,\n };\n};\n\n/**\n * Disable the date picker component\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst disable = (el) => {\n const { externalInputEl, toggleBtnEl } = getDatePickerContext(el);\n\n toggleBtnEl.disabled = true;\n externalInputEl.disabled = true;\n};\n\n/**\n * Enable the date picker component\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst enable = (el) => {\n const { externalInputEl, toggleBtnEl } = getDatePickerContext(el);\n\n toggleBtnEl.disabled = false;\n externalInputEl.disabled = false;\n};\n\n// #region Validation\n\n/**\n * Validate the value in the input as a valid date of format M/D/YYYY\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst isDateInputInvalid = (el) => {\n const { externalInputEl, minDate, maxDate } = getDatePickerContext(el);\n\n const dateString = externalInputEl.value;\n let isInvalid = false;\n\n if (dateString) {\n isInvalid = true;\n\n const dateStringParts = dateString.split(\"/\");\n const [month, day, year] = dateStringParts.map((str) => {\n let value;\n const parsed = parseInt(str, 10);\n if (!Number.isNaN(parsed)) value = parsed;\n return value;\n });\n\n if (month && day && year != null) {\n const checkDate = setDate(year, month - 1, day);\n\n if (\n checkDate.getMonth() === month - 1 &&\n checkDate.getDate() === day &&\n checkDate.getFullYear() === year &&\n dateStringParts[2].length === 4 &&\n isDateWithinMinAndMax(checkDate, minDate, maxDate)\n ) {\n isInvalid = false;\n }\n }\n }\n\n return isInvalid;\n};\n\n/**\n * Validate the value in the input as a valid date of format M/D/YYYY\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst validateDateInput = (el) => {\n const { externalInputEl } = getDatePickerContext(el);\n const isInvalid = isDateInputInvalid(externalInputEl);\n\n if (isInvalid && !externalInputEl.validationMessage) {\n externalInputEl.setCustomValidity(VALIDATION_MESSAGE);\n }\n\n if (!isInvalid && externalInputEl.validationMessage === VALIDATION_MESSAGE) {\n externalInputEl.setCustomValidity(\"\");\n }\n};\n\n// #endregion Validation\n\n/**\n * Enable the date picker component\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst reconcileInputValues = (el) => {\n const { internalInputEl, inputDate } = getDatePickerContext(el);\n let newValue = \"\";\n\n if (inputDate && !isDateInputInvalid(el)) {\n newValue = formatDate(inputDate);\n }\n\n if (internalInputEl.value !== newValue) {\n changeElementValue(internalInputEl, newValue);\n }\n};\n\n/**\n * Select the value of the date picker inputs.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n * @param {string} dateString The date string to update in YYYY-MM-DD format\n */\nconst setCalendarValue = (el, dateString) => {\n const parsedDate = parseDateString(dateString);\n\n if (parsedDate) {\n const formattedDate = formatDate(parsedDate, DEFAULT_EXTERNAL_DATE_FORMAT);\n\n const { datePickerEl, internalInputEl, externalInputEl } =\n getDatePickerContext(el);\n\n changeElementValue(internalInputEl, dateString);\n changeElementValue(externalInputEl, formattedDate);\n\n validateDateInput(datePickerEl);\n }\n};\n\n/**\n * Enhance an input with the date picker elements\n *\n * @param {HTMLElement} el The initial wrapping element of the date picker component\n */\nconst enhanceDatePicker = (el) => {\n const datePickerEl = el.closest(DATE_PICKER);\n const { defaultValue } = datePickerEl.dataset;\n\n const internalInputEl = datePickerEl.querySelector(`input`);\n\n if (!internalInputEl) {\n throw new Error(`${DATE_PICKER} is missing inner input`);\n }\n\n if (internalInputEl.value) {\n internalInputEl.value = \"\";\n }\n\n const minDate = parseDateString(\n datePickerEl.dataset.minDate || internalInputEl.getAttribute(\"min\")\n );\n datePickerEl.dataset.minDate = minDate\n ? formatDate(minDate)\n : DEFAULT_MIN_DATE;\n\n const maxDate = parseDateString(\n datePickerEl.dataset.maxDate || internalInputEl.getAttribute(\"max\")\n );\n if (maxDate) {\n datePickerEl.dataset.maxDate = formatDate(maxDate);\n }\n\n const calendarWrapper = document.createElement(\"div\");\n calendarWrapper.classList.add(DATE_PICKER_WRAPPER_CLASS);\n\n const externalInputEl = internalInputEl.cloneNode();\n externalInputEl.classList.add(DATE_PICKER_EXTERNAL_INPUT_CLASS);\n externalInputEl.type = \"text\";\n\n calendarWrapper.appendChild(externalInputEl);\n calendarWrapper.insertAdjacentHTML(\n \"beforeend\",\n Sanitizer.escapeHTML`\n \n \n
`\n );\n\n internalInputEl.setAttribute(\"aria-hidden\", \"true\");\n internalInputEl.setAttribute(\"tabindex\", \"-1\");\n internalInputEl.style.display = \"none\";\n internalInputEl.classList.add(DATE_PICKER_INTERNAL_INPUT_CLASS);\n internalInputEl.removeAttribute(\"id\");\n internalInputEl.removeAttribute(\"name\");\n internalInputEl.required = false;\n\n datePickerEl.appendChild(calendarWrapper);\n datePickerEl.classList.add(DATE_PICKER_INITIALIZED_CLASS);\n\n if (defaultValue) {\n setCalendarValue(datePickerEl, defaultValue);\n }\n\n if (internalInputEl.disabled) {\n disable(datePickerEl);\n internalInputEl.disabled = false;\n }\n};\n\n// #region Calendar - Date Selection View\n\n/**\n * render the calendar.\n *\n * @param {HTMLElement} el An element within the date picker component\n * @param {Date} _dateToDisplay a date to render on the calendar\n * @returns {HTMLElement} a reference to the new calendar element\n */\nconst renderCalendar = (el, _dateToDisplay) => {\n const {\n datePickerEl,\n calendarEl,\n statusEl,\n selectedDate,\n maxDate,\n minDate,\n rangeDate,\n } = getDatePickerContext(el);\n const todaysDate = today();\n let dateToDisplay = _dateToDisplay || todaysDate;\n\n const calendarWasHidden = calendarEl.hidden;\n\n const focusedDate = addDays(dateToDisplay, 0);\n const focusedMonth = dateToDisplay.getMonth();\n const focusedYear = dateToDisplay.getFullYear();\n\n const prevMonth = subMonths(dateToDisplay, 1);\n const nextMonth = addMonths(dateToDisplay, 1);\n\n const currentFormattedDate = formatDate(dateToDisplay);\n\n const firstOfMonth = startOfMonth(dateToDisplay);\n const prevButtonsDisabled = isSameMonth(dateToDisplay, minDate);\n const nextButtonsDisabled = isSameMonth(dateToDisplay, maxDate);\n\n const rangeConclusionDate = selectedDate || dateToDisplay;\n const rangeStartDate = rangeDate && min(rangeConclusionDate, rangeDate);\n const rangeEndDate = rangeDate && max(rangeConclusionDate, rangeDate);\n\n const withinRangeStartDate = rangeDate && addDays(rangeStartDate, 1);\n const withinRangeEndDate = rangeDate && subDays(rangeEndDate, 1);\n\n const monthLabel = MONTH_LABELS[focusedMonth];\n\n const generateDateHtml = (dateToRender) => {\n const classes = [CALENDAR_DATE_CLASS];\n const day = dateToRender.getDate();\n const month = dateToRender.getMonth();\n const year = dateToRender.getFullYear();\n const dayOfWeek = dateToRender.getDay();\n\n const formattedDate = formatDate(dateToRender);\n\n let tabindex = \"-1\";\n\n const isDisabled = !isDateWithinMinAndMax(dateToRender, minDate, maxDate);\n const isSelected = isSameDay(dateToRender, selectedDate);\n\n if (isSameMonth(dateToRender, prevMonth)) {\n classes.push(CALENDAR_DATE_PREVIOUS_MONTH_CLASS);\n }\n\n if (isSameMonth(dateToRender, focusedDate)) {\n classes.push(CALENDAR_DATE_CURRENT_MONTH_CLASS);\n }\n\n if (isSameMonth(dateToRender, nextMonth)) {\n classes.push(CALENDAR_DATE_NEXT_MONTH_CLASS);\n }\n\n if (isSelected) {\n classes.push(CALENDAR_DATE_SELECTED_CLASS);\n }\n\n if (isSameDay(dateToRender, todaysDate)) {\n classes.push(CALENDAR_DATE_TODAY_CLASS);\n }\n\n if (rangeDate) {\n if (isSameDay(dateToRender, rangeDate)) {\n classes.push(CALENDAR_DATE_RANGE_DATE_CLASS);\n }\n\n if (isSameDay(dateToRender, rangeStartDate)) {\n classes.push(CALENDAR_DATE_RANGE_DATE_START_CLASS);\n }\n\n if (isSameDay(dateToRender, rangeEndDate)) {\n classes.push(CALENDAR_DATE_RANGE_DATE_END_CLASS);\n }\n\n if (\n isDateWithinMinAndMax(\n dateToRender,\n withinRangeStartDate,\n withinRangeEndDate\n )\n ) {\n classes.push(CALENDAR_DATE_WITHIN_RANGE_CLASS);\n }\n }\n\n if (isSameDay(dateToRender, focusedDate)) {\n tabindex = \"0\";\n classes.push(CALENDAR_DATE_FOCUSED_CLASS);\n }\n\n const monthStr = MONTH_LABELS[month];\n const dayStr = DAY_OF_WEEK_LABELS[dayOfWeek];\n\n const btn = document.createElement(\"button\");\n btn.setAttribute(\"type\", \"button\");\n btn.setAttribute(\"tabindex\", tabindex);\n btn.setAttribute(\"class\", classes.join(\" \"));\n btn.setAttribute(\"data-day\", day);\n btn.setAttribute(\"data-month\", month + 1);\n btn.setAttribute(\"data-year\", year);\n btn.setAttribute(\"data-value\", formattedDate);\n btn.setAttribute(\n \"aria-label\",\n Sanitizer.escapeHTML`${day} ${monthStr} ${year} ${dayStr}`\n );\n btn.setAttribute(\"aria-selected\", isSelected ? \"true\" : \"false\");\n if (isDisabled === true) {\n btn.disabled = true;\n }\n btn.textContent = day;\n\n return btn;\n };\n\n // set date to first rendered day\n dateToDisplay = startOfWeek(firstOfMonth);\n\n const days = [];\n\n while (\n days.length < 28 ||\n dateToDisplay.getMonth() === focusedMonth ||\n days.length % 7 !== 0\n ) {\n days.push(generateDateHtml(dateToDisplay));\n dateToDisplay = addDays(dateToDisplay, 1);\n }\n\n const datesGrid = listToGridHtml(days, 7);\n\n const newCalendar = calendarEl.cloneNode();\n newCalendar.dataset.value = currentFormattedDate;\n newCalendar.style.top = `${datePickerEl.offsetHeight}px`;\n newCalendar.hidden = false;\n newCalendar.innerHTML = Sanitizer.escapeHTML`\n
\n
\n
\n \n
\n
\n \n
\n
\n ${monthLabel}\n ${focusedYear}\n
\n
\n \n
\n
\n \n
\n
\n
\n `;\n\n const table = document.createElement(\"table\");\n table.setAttribute(\"class\", CALENDAR_TABLE_CLASS);\n table.setAttribute(\"role\", \"presentation\");\n\n const tableHead = document.createElement(\"thead\");\n table.insertAdjacentElement(\"beforeend\", tableHead);\n const tableHeadRow = document.createElement(\"tr\");\n tableHead.insertAdjacentElement(\"beforeend\", tableHeadRow);\n\n const daysOfWeek = {\n Sunday: \"S\",\n Monday: \"M\",\n Tuesday: \"T\",\n Wednesday: \"W\",\n Thursday: \"Th\",\n Friday: \"Fr\",\n Saturday: \"S\",\n };\n\n Object.keys(daysOfWeek).forEach((key) => {\n const th = document.createElement(\"th\");\n th.setAttribute(\"class\", CALENDAR_DAY_OF_WEEK_CLASS);\n th.setAttribute(\"scope\", \"presentation\");\n th.setAttribute(\"aria-label\", key);\n th.textContent = daysOfWeek[key];\n tableHeadRow.insertAdjacentElement(\"beforeend\", th);\n });\n\n const tableBody = createTableBody(datesGrid);\n table.insertAdjacentElement(\"beforeend\", tableBody);\n\n // Container for Years, Months, and Days\n const datePickerCalendarContainer =\n newCalendar.querySelector(CALENDAR_DATE_PICKER);\n\n datePickerCalendarContainer.insertAdjacentElement(\"beforeend\", table);\n\n calendarEl.parentNode.replaceChild(newCalendar, calendarEl);\n\n datePickerEl.classList.add(DATE_PICKER_ACTIVE_CLASS);\n\n const statuses = [];\n\n if (isSameDay(selectedDate, focusedDate)) {\n statuses.push(\"Selected date\");\n }\n\n if (calendarWasHidden) {\n statuses.push(\n \"You can navigate by day using left and right arrows\",\n \"Weeks by using up and down arrows\",\n \"Months by using page up and page down keys\",\n \"Years by using shift plus page up and shift plus page down\",\n \"Home and end keys navigate to the beginning and end of a week\"\n );\n statusEl.textContent = \"\";\n } else {\n statuses.push(`${monthLabel} ${focusedYear}`);\n }\n statusEl.textContent = statuses.join(\". \");\n\n return newCalendar;\n};\n\n/**\n * Navigate back one year and display the calendar.\n *\n * @param {HTMLButtonElement} _buttonEl An element within the date picker component\n */\nconst displayPreviousYear = (_buttonEl) => {\n if (_buttonEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(_buttonEl);\n let date = subYears(calendarDate, 1);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Navigate back one month and display the calendar.\n *\n * @param {HTMLButtonElement} _buttonEl An element within the date picker component\n */\nconst displayPreviousMonth = (_buttonEl) => {\n if (_buttonEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(_buttonEl);\n let date = subMonths(calendarDate, 1);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_MONTH);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Navigate forward one month and display the calendar.\n *\n * @param {HTMLButtonElement} _buttonEl An element within the date picker component\n */\nconst displayNextMonth = (_buttonEl) => {\n if (_buttonEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(_buttonEl);\n let date = addMonths(calendarDate, 1);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_MONTH);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Navigate forward one year and display the calendar.\n *\n * @param {HTMLButtonElement} _buttonEl An element within the date picker component\n */\nconst displayNextYear = (_buttonEl) => {\n if (_buttonEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(_buttonEl);\n let date = addYears(calendarDate, 1);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Hide the calendar of a date picker component.\n *\n * @param {HTMLElement} el An element within the date picker component\n */\nconst hideCalendar = (el) => {\n const { datePickerEl, calendarEl, statusEl } = getDatePickerContext(el);\n\n datePickerEl.classList.remove(DATE_PICKER_ACTIVE_CLASS);\n calendarEl.hidden = true;\n statusEl.textContent = \"\";\n};\n\n/**\n * Select a date within the date picker component.\n *\n * @param {HTMLButtonElement} calendarDateEl A date element within the date picker component\n */\nconst selectDate = (calendarDateEl) => {\n if (calendarDateEl.disabled) return;\n\n const { datePickerEl, externalInputEl } =\n getDatePickerContext(calendarDateEl);\n\n setCalendarValue(calendarDateEl, calendarDateEl.dataset.value);\n hideCalendar(datePickerEl);\n\n externalInputEl.focus();\n};\n\n/**\n * Toggle the calendar.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n */\nconst toggleCalendar = (el) => {\n if (el.disabled) return;\n const { calendarEl, inputDate, minDate, maxDate, defaultDate } =\n getDatePickerContext(el);\n\n if (calendarEl.hidden) {\n const dateToDisplay = keepDateBetweenMinAndMax(\n inputDate || defaultDate || today(),\n minDate,\n maxDate\n );\n const newCalendar = renderCalendar(calendarEl, dateToDisplay);\n newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus();\n } else {\n hideCalendar(el);\n }\n};\n\n/**\n * Update the calendar when visible.\n *\n * @param {HTMLElement} el an element within the date picker\n */\nconst updateCalendarIfVisible = (el) => {\n const { calendarEl, inputDate, minDate, maxDate } = getDatePickerContext(el);\n const calendarShown = !calendarEl.hidden;\n\n if (calendarShown && inputDate) {\n const dateToDisplay = keepDateBetweenMinAndMax(inputDate, minDate, maxDate);\n renderCalendar(calendarEl, dateToDisplay);\n }\n};\n\n// #endregion Calendar - Date Selection View\n\n// #region Calendar - Month Selection View\n/**\n * Display the month selection screen in the date picker.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n * @returns {HTMLElement} a reference to the new calendar element\n */\nconst displayMonthSelection = (el, monthToDisplay) => {\n const { calendarEl, statusEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(el);\n\n const selectedMonth = calendarDate.getMonth();\n const focusedMonth = monthToDisplay == null ? selectedMonth : monthToDisplay;\n\n const months = MONTH_LABELS.map((month, index) => {\n const monthToCheck = setMonth(calendarDate, index);\n\n const isDisabled = isDatesMonthOutsideMinOrMax(\n monthToCheck,\n minDate,\n maxDate\n );\n\n let tabindex = \"-1\";\n\n const classes = [CALENDAR_MONTH_CLASS];\n const isSelected = index === selectedMonth;\n\n if (index === focusedMonth) {\n tabindex = \"0\";\n classes.push(CALENDAR_MONTH_FOCUSED_CLASS);\n }\n\n if (isSelected) {\n classes.push(CALENDAR_MONTH_SELECTED_CLASS);\n }\n\n const btn = document.createElement(\"button\");\n btn.setAttribute(\"type\", \"button\");\n btn.setAttribute(\"tabindex\", tabindex);\n btn.setAttribute(\"class\", classes.join(\" \"));\n btn.setAttribute(\"data-value\", index);\n btn.setAttribute(\"data-label\", month);\n btn.setAttribute(\"aria-selected\", isSelected ? \"true\" : \"false\");\n if (isDisabled === true) {\n btn.disabled = true;\n }\n btn.textContent = month;\n\n return btn;\n });\n\n const monthsHtml = document.createElement(\"div\");\n monthsHtml.setAttribute(\"tabindex\", \"-1\");\n monthsHtml.setAttribute(\"class\", CALENDAR_MONTH_PICKER_CLASS);\n\n const table = document.createElement(\"table\");\n table.setAttribute(\"class\", CALENDAR_TABLE_CLASS);\n table.setAttribute(\"role\", \"presentation\");\n\n const monthsGrid = listToGridHtml(months, 3);\n const tableBody = createTableBody(monthsGrid);\n table.insertAdjacentElement(\"beforeend\", tableBody);\n monthsHtml.insertAdjacentElement(\"beforeend\", table);\n\n const newCalendar = calendarEl.cloneNode();\n newCalendar.insertAdjacentElement(\"beforeend\", monthsHtml);\n calendarEl.parentNode.replaceChild(newCalendar, calendarEl);\n\n statusEl.textContent = \"Select a month.\";\n\n return newCalendar;\n};\n\n/**\n * Select a month in the date picker component.\n *\n * @param {HTMLButtonElement} monthEl An month element within the date picker component\n */\nconst selectMonth = (monthEl) => {\n if (monthEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(monthEl);\n const selectedMonth = parseInt(monthEl.dataset.value, 10);\n let date = setMonth(calendarDate, selectedMonth);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus();\n};\n\n// #endregion Calendar - Month Selection View\n\n// #region Calendar - Year Selection View\n\n/**\n * Display the year selection screen in the date picker.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n * @param {number} yearToDisplay year to display in year selection\n * @returns {HTMLElement} a reference to the new calendar element\n */\nconst displayYearSelection = (el, yearToDisplay) => {\n const { calendarEl, statusEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(el);\n\n const selectedYear = calendarDate.getFullYear();\n const focusedYear = yearToDisplay == null ? selectedYear : yearToDisplay;\n\n let yearToChunk = focusedYear;\n yearToChunk -= yearToChunk % YEAR_CHUNK;\n yearToChunk = Math.max(0, yearToChunk);\n\n const prevYearChunkDisabled = isDatesYearOutsideMinOrMax(\n setYear(calendarDate, yearToChunk - 1),\n minDate,\n maxDate\n );\n\n const nextYearChunkDisabled = isDatesYearOutsideMinOrMax(\n setYear(calendarDate, yearToChunk + YEAR_CHUNK),\n minDate,\n maxDate\n );\n\n const years = [];\n let yearIndex = yearToChunk;\n while (years.length < YEAR_CHUNK) {\n const isDisabled = isDatesYearOutsideMinOrMax(\n setYear(calendarDate, yearIndex),\n minDate,\n maxDate\n );\n\n let tabindex = \"-1\";\n\n const classes = [CALENDAR_YEAR_CLASS];\n const isSelected = yearIndex === selectedYear;\n\n if (yearIndex === focusedYear) {\n tabindex = \"0\";\n classes.push(CALENDAR_YEAR_FOCUSED_CLASS);\n }\n\n if (isSelected) {\n classes.push(CALENDAR_YEAR_SELECTED_CLASS);\n }\n\n const btn = document.createElement(\"button\");\n btn.setAttribute(\"type\", \"button\");\n btn.setAttribute(\"tabindex\", tabindex);\n btn.setAttribute(\"class\", classes.join(\" \"));\n btn.setAttribute(\"data-value\", yearIndex);\n btn.setAttribute(\"aria-selected\", isSelected ? \"true\" : \"false\");\n if (isDisabled === true) {\n btn.disabled = true;\n }\n btn.textContent = yearIndex;\n\n years.push(btn);\n yearIndex += 1;\n }\n\n const newCalendar = calendarEl.cloneNode();\n\n // create the years calendar wrapper\n const yearsCalendarWrapper = document.createElement(\"div\");\n yearsCalendarWrapper.setAttribute(\"tabindex\", \"-1\");\n yearsCalendarWrapper.setAttribute(\"class\", CALENDAR_YEAR_PICKER_CLASS);\n\n // create table parent\n const yearsTableParent = document.createElement(\"table\");\n yearsTableParent.setAttribute(\"role\", \"presentation\");\n yearsTableParent.setAttribute(\"class\", CALENDAR_TABLE_CLASS);\n\n // create table body and table row\n const yearsHTMLTableBody = document.createElement(\"tbody\");\n const yearsHTMLTableBodyRow = document.createElement(\"tr\");\n\n // create previous button\n const previousYearsBtn = document.createElement(\"button\");\n previousYearsBtn.setAttribute(\"type\", \"button\");\n previousYearsBtn.setAttribute(\"class\", CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS);\n previousYearsBtn.setAttribute(\n \"aria-label\",\n `Navigate back ${YEAR_CHUNK} years`\n );\n if (prevYearChunkDisabled === true) {\n previousYearsBtn.disabled = true;\n }\n previousYearsBtn.innerHTML = Sanitizer.escapeHTML` `;\n\n // create next button\n const nextYearsBtn = document.createElement(\"button\");\n nextYearsBtn.setAttribute(\"type\", \"button\");\n nextYearsBtn.setAttribute(\"class\", CALENDAR_NEXT_YEAR_CHUNK_CLASS);\n nextYearsBtn.setAttribute(\n \"aria-label\",\n `Navigate forward ${YEAR_CHUNK} years`\n );\n if (nextYearChunkDisabled === true) {\n nextYearsBtn.disabled = true;\n }\n nextYearsBtn.innerHTML = Sanitizer.escapeHTML` `;\n\n // create the actual years table\n const yearsTable = document.createElement(\"table\");\n yearsTable.setAttribute(\"class\", CALENDAR_TABLE_CLASS);\n yearsTable.setAttribute(\"role\", \"presentation\");\n\n // create the years child table\n const yearsGrid = listToGridHtml(years, 3);\n const yearsTableBody = createTableBody(yearsGrid);\n\n // append the grid to the years child table\n yearsTable.insertAdjacentElement(\"beforeend\", yearsTableBody);\n\n // create the prev button td and append the prev button\n const yearsHTMLTableBodyDetailPrev = document.createElement(\"td\");\n yearsHTMLTableBodyDetailPrev.insertAdjacentElement(\n \"beforeend\",\n previousYearsBtn\n );\n\n // create the years td and append the years child table\n const yearsHTMLTableBodyYearsDetail = document.createElement(\"td\");\n yearsHTMLTableBodyYearsDetail.setAttribute(\"colspan\", \"3\");\n yearsHTMLTableBodyYearsDetail.insertAdjacentElement(\"beforeend\", yearsTable);\n\n // create the next button td and append the next button\n const yearsHTMLTableBodyDetailNext = document.createElement(\"td\");\n yearsHTMLTableBodyDetailNext.insertAdjacentElement(\"beforeend\", nextYearsBtn);\n\n // append the three td to the years child table row\n yearsHTMLTableBodyRow.insertAdjacentElement(\n \"beforeend\",\n yearsHTMLTableBodyDetailPrev\n );\n yearsHTMLTableBodyRow.insertAdjacentElement(\n \"beforeend\",\n yearsHTMLTableBodyYearsDetail\n );\n yearsHTMLTableBodyRow.insertAdjacentElement(\n \"beforeend\",\n yearsHTMLTableBodyDetailNext\n );\n\n // append the table row to the years child table body\n yearsHTMLTableBody.insertAdjacentElement(\"beforeend\", yearsHTMLTableBodyRow);\n\n // append the years table body to the years parent table\n yearsTableParent.insertAdjacentElement(\"beforeend\", yearsHTMLTableBody);\n\n // append the parent table to the calendar wrapper\n yearsCalendarWrapper.insertAdjacentElement(\"beforeend\", yearsTableParent);\n\n // append the years calender to the new calendar\n newCalendar.insertAdjacentElement(\"beforeend\", yearsCalendarWrapper);\n\n // replace calendar\n calendarEl.parentNode.replaceChild(newCalendar, calendarEl);\n\n statusEl.textContent = Sanitizer.escapeHTML`Showing years ${yearToChunk} to ${\n yearToChunk + YEAR_CHUNK - 1\n }. Select a year.`;\n\n return newCalendar;\n};\n\n/**\n * Navigate back by years and display the year selection screen.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n */\nconst displayPreviousYearChunk = (el) => {\n if (el.disabled) return;\n\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(el);\n const yearEl = calendarEl.querySelector(CALENDAR_YEAR_FOCUSED);\n const selectedYear = parseInt(yearEl.textContent, 10);\n\n let adjustedYear = selectedYear - YEAR_CHUNK;\n adjustedYear = Math.max(0, adjustedYear);\n\n const date = setYear(calendarDate, adjustedYear);\n const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = displayYearSelection(\n calendarEl,\n cappedDate.getFullYear()\n );\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR_CHUNK);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Navigate forward by years and display the year selection screen.\n *\n * @param {HTMLButtonElement} el An element within the date picker component\n */\nconst displayNextYearChunk = (el) => {\n if (el.disabled) return;\n\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(el);\n const yearEl = calendarEl.querySelector(CALENDAR_YEAR_FOCUSED);\n const selectedYear = parseInt(yearEl.textContent, 10);\n\n let adjustedYear = selectedYear + YEAR_CHUNK;\n adjustedYear = Math.max(0, adjustedYear);\n\n const date = setYear(calendarDate, adjustedYear);\n const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = displayYearSelection(\n calendarEl,\n cappedDate.getFullYear()\n );\n\n let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR_CHUNK);\n if (nextToFocus.disabled) {\n nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER);\n }\n nextToFocus.focus();\n};\n\n/**\n * Select a year in the date picker component.\n *\n * @param {HTMLButtonElement} yearEl A year element within the date picker component\n */\nconst selectYear = (yearEl) => {\n if (yearEl.disabled) return;\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(yearEl);\n const selectedYear = parseInt(yearEl.innerHTML, 10);\n let date = setYear(calendarDate, selectedYear);\n date = keepDateBetweenMinAndMax(date, minDate, maxDate);\n const newCalendar = renderCalendar(calendarEl, date);\n newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus();\n};\n\n// #endregion Calendar - Year Selection View\n\n// #region Calendar Event Handling\n\n/**\n * Hide the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleEscapeFromCalendar = (event) => {\n const { datePickerEl, externalInputEl } = getDatePickerContext(event.target);\n\n hideCalendar(datePickerEl);\n externalInputEl.focus();\n\n event.preventDefault();\n};\n\n// #endregion Calendar Event Handling\n\n// #region Calendar Date Event Handling\n\n/**\n * Adjust the date and display the calendar if needed.\n *\n * @param {function} adjustDateFn function that returns the adjusted date\n */\nconst adjustCalendar = (adjustDateFn) => (event) => {\n const { calendarEl, calendarDate, minDate, maxDate } = getDatePickerContext(\n event.target\n );\n\n const date = adjustDateFn(calendarDate);\n\n const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate);\n if (!isSameDay(calendarDate, cappedDate)) {\n const newCalendar = renderCalendar(calendarEl, cappedDate);\n newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus();\n }\n event.preventDefault();\n};\n\n/**\n * Navigate back one week and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleUpFromDate = adjustCalendar((date) => subWeeks(date, 1));\n\n/**\n * Navigate forward one week and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleDownFromDate = adjustCalendar((date) => addWeeks(date, 1));\n\n/**\n * Navigate back one day and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleLeftFromDate = adjustCalendar((date) => subDays(date, 1));\n\n/**\n * Navigate forward one day and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleRightFromDate = adjustCalendar((date) => addDays(date, 1));\n\n/**\n * Navigate to the start of the week and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleHomeFromDate = adjustCalendar((date) => startOfWeek(date));\n\n/**\n * Navigate to the end of the week and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleEndFromDate = adjustCalendar((date) => endOfWeek(date));\n\n/**\n * Navigate forward one month and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageDownFromDate = adjustCalendar((date) => addMonths(date, 1));\n\n/**\n * Navigate back one month and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageUpFromDate = adjustCalendar((date) => subMonths(date, 1));\n\n/**\n * Navigate forward one year and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleShiftPageDownFromDate = adjustCalendar((date) => addYears(date, 1));\n\n/**\n * Navigate back one year and display the calendar.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleShiftPageUpFromDate = adjustCalendar((date) => subYears(date, 1));\n\n/**\n * display the calendar for the mouseover date.\n *\n * @param {MouseEvent} event The mouseover event\n * @param {HTMLButtonElement} dateEl A date element within the date picker component\n */\nconst handleMouseoverFromDate = (dateEl) => {\n if (dateEl.disabled) return;\n\n const calendarEl = dateEl.closest(DATE_PICKER_CALENDAR);\n\n const currentCalendarDate = calendarEl.dataset.value;\n const hoverDate = dateEl.dataset.value;\n\n if (hoverDate === currentCalendarDate) return;\n\n const dateToDisplay = parseDateString(hoverDate);\n const newCalendar = renderCalendar(calendarEl, dateToDisplay);\n newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus();\n};\n\n// #endregion Calendar Date Event Handling\n\n// #region Calendar Month Event Handling\n\n/**\n * Adjust the month and display the month selection screen if needed.\n *\n * @param {function} adjustMonthFn function that returns the adjusted month\n */\nconst adjustMonthSelectionScreen = (adjustMonthFn) => (event) => {\n const monthEl = event.target;\n const selectedMonth = parseInt(monthEl.dataset.value, 10);\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(monthEl);\n const currentDate = setMonth(calendarDate, selectedMonth);\n\n let adjustedMonth = adjustMonthFn(selectedMonth);\n adjustedMonth = Math.max(0, Math.min(11, adjustedMonth));\n\n const date = setMonth(calendarDate, adjustedMonth);\n const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate);\n if (!isSameMonth(currentDate, cappedDate)) {\n const newCalendar = displayMonthSelection(\n calendarEl,\n cappedDate.getMonth()\n );\n newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus();\n }\n event.preventDefault();\n};\n\n/**\n * Navigate back three months and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleUpFromMonth = adjustMonthSelectionScreen((month) => month - 3);\n\n/**\n * Navigate forward three months and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleDownFromMonth = adjustMonthSelectionScreen((month) => month + 3);\n\n/**\n * Navigate back one month and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleLeftFromMonth = adjustMonthSelectionScreen((month) => month - 1);\n\n/**\n * Navigate forward one month and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleRightFromMonth = adjustMonthSelectionScreen((month) => month + 1);\n\n/**\n * Navigate to the start of the row of months and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleHomeFromMonth = adjustMonthSelectionScreen(\n (month) => month - (month % 3)\n);\n\n/**\n * Navigate to the end of the row of months and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleEndFromMonth = adjustMonthSelectionScreen(\n (month) => month + 2 - (month % 3)\n);\n\n/**\n * Navigate to the last month (December) and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageDownFromMonth = adjustMonthSelectionScreen(() => 11);\n\n/**\n * Navigate to the first month (January) and display the month selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageUpFromMonth = adjustMonthSelectionScreen(() => 0);\n\n/**\n * update the focus on a month when the mouse moves.\n *\n * @param {MouseEvent} event The mouseover event\n * @param {HTMLButtonElement} monthEl A month element within the date picker component\n */\nconst handleMouseoverFromMonth = (monthEl) => {\n if (monthEl.disabled) return;\n if (monthEl.classList.contains(CALENDAR_MONTH_FOCUSED_CLASS)) return;\n\n const focusMonth = parseInt(monthEl.dataset.value, 10);\n\n const newCalendar = displayMonthSelection(monthEl, focusMonth);\n newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus();\n};\n\n// #endregion Calendar Month Event Handling\n\n// #region Calendar Year Event Handling\n\n/**\n * Adjust the year and display the year selection screen if needed.\n *\n * @param {function} adjustYearFn function that returns the adjusted year\n */\nconst adjustYearSelectionScreen = (adjustYearFn) => (event) => {\n const yearEl = event.target;\n const selectedYear = parseInt(yearEl.dataset.value, 10);\n const { calendarEl, calendarDate, minDate, maxDate } =\n getDatePickerContext(yearEl);\n const currentDate = setYear(calendarDate, selectedYear);\n\n let adjustedYear = adjustYearFn(selectedYear);\n adjustedYear = Math.max(0, adjustedYear);\n\n const date = setYear(calendarDate, adjustedYear);\n const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate);\n if (!isSameYear(currentDate, cappedDate)) {\n const newCalendar = displayYearSelection(\n calendarEl,\n cappedDate.getFullYear()\n );\n newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus();\n }\n event.preventDefault();\n};\n\n/**\n * Navigate back three years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleUpFromYear = adjustYearSelectionScreen((year) => year - 3);\n\n/**\n * Navigate forward three years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleDownFromYear = adjustYearSelectionScreen((year) => year + 3);\n\n/**\n * Navigate back one year and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleLeftFromYear = adjustYearSelectionScreen((year) => year - 1);\n\n/**\n * Navigate forward one year and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleRightFromYear = adjustYearSelectionScreen((year) => year + 1);\n\n/**\n * Navigate to the start of the row of years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleHomeFromYear = adjustYearSelectionScreen(\n (year) => year - (year % 3)\n);\n\n/**\n * Navigate to the end of the row of years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handleEndFromYear = adjustYearSelectionScreen(\n (year) => year + 2 - (year % 3)\n);\n\n/**\n * Navigate to back 12 years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageUpFromYear = adjustYearSelectionScreen(\n (year) => year - YEAR_CHUNK\n);\n\n/**\n * Navigate forward 12 years and display the year selection screen.\n *\n * @param {KeyboardEvent} event the keydown event\n */\nconst handlePageDownFromYear = adjustYearSelectionScreen(\n (year) => year + YEAR_CHUNK\n);\n\n/**\n * update the focus on a year when the mouse moves.\n *\n * @param {MouseEvent} event The mouseover event\n * @param {HTMLButtonElement} dateEl A year element within the date picker component\n */\nconst handleMouseoverFromYear = (yearEl) => {\n if (yearEl.disabled) return;\n if (yearEl.classList.contains(CALENDAR_YEAR_FOCUSED_CLASS)) return;\n\n const focusYear = parseInt(yearEl.dataset.value, 10);\n\n const newCalendar = displayYearSelection(yearEl, focusYear);\n newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus();\n};\n\n// #endregion Calendar Year Event Handling\n\n// #region Focus Handling Event Handling\n\nconst tabHandler = (focusable) => {\n const getFocusableContext = (el) => {\n const { calendarEl } = getDatePickerContext(el);\n const focusableElements = select(focusable, calendarEl);\n\n const firstTabIndex = 0;\n const lastTabIndex = focusableElements.length - 1;\n const firstTabStop = focusableElements[firstTabIndex];\n const lastTabStop = focusableElements[lastTabIndex];\n const focusIndex = focusableElements.indexOf(activeElement());\n\n const isLastTab = focusIndex === lastTabIndex;\n const isFirstTab = focusIndex === firstTabIndex;\n const isNotFound = focusIndex === -1;\n\n return {\n focusableElements,\n isNotFound,\n firstTabStop,\n isFirstTab,\n lastTabStop,\n isLastTab,\n };\n };\n\n return {\n tabAhead(event) {\n const { firstTabStop, isLastTab, isNotFound } = getFocusableContext(\n event.target\n );\n\n if (isLastTab || isNotFound) {\n event.preventDefault();\n firstTabStop.focus();\n }\n },\n tabBack(event) {\n const { lastTabStop, isFirstTab, isNotFound } = getFocusableContext(\n event.target\n );\n\n if (isFirstTab || isNotFound) {\n event.preventDefault();\n lastTabStop.focus();\n }\n },\n };\n};\n\nconst datePickerTabEventHandler = tabHandler(DATE_PICKER_FOCUSABLE);\nconst monthPickerTabEventHandler = tabHandler(MONTH_PICKER_FOCUSABLE);\nconst yearPickerTabEventHandler = tabHandler(YEAR_PICKER_FOCUSABLE);\n\n// #endregion Focus Handling Event Handling\n\n// #region Date Picker Event Delegation Registration / Component\n\nconst datePickerEvents = {\n [CLICK]: {\n [DATE_PICKER_BUTTON]() {\n toggleCalendar(this);\n },\n [CALENDAR_DATE]() {\n selectDate(this);\n },\n [CALENDAR_MONTH]() {\n selectMonth(this);\n },\n [CALENDAR_YEAR]() {\n selectYear(this);\n },\n [CALENDAR_PREVIOUS_MONTH]() {\n displayPreviousMonth(this);\n },\n [CALENDAR_NEXT_MONTH]() {\n displayNextMonth(this);\n },\n [CALENDAR_PREVIOUS_YEAR]() {\n displayPreviousYear(this);\n },\n [CALENDAR_NEXT_YEAR]() {\n displayNextYear(this);\n },\n [CALENDAR_PREVIOUS_YEAR_CHUNK]() {\n displayPreviousYearChunk(this);\n },\n [CALENDAR_NEXT_YEAR_CHUNK]() {\n displayNextYearChunk(this);\n },\n [CALENDAR_MONTH_SELECTION]() {\n const newCalendar = displayMonthSelection(this);\n newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus();\n },\n [CALENDAR_YEAR_SELECTION]() {\n const newCalendar = displayYearSelection(this);\n newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus();\n },\n },\n keyup: {\n [DATE_PICKER_CALENDAR](event) {\n const keydown = this.dataset.keydownKeyCode;\n if (`${event.keyCode}` !== keydown) {\n event.preventDefault();\n }\n },\n },\n keydown: {\n [DATE_PICKER_EXTERNAL_INPUT](event) {\n if (event.keyCode === ENTER_KEYCODE) {\n validateDateInput(this);\n }\n },\n [CALENDAR_DATE]: keymap({\n Up: handleUpFromDate,\n ArrowUp: handleUpFromDate,\n Down: handleDownFromDate,\n ArrowDown: handleDownFromDate,\n Left: handleLeftFromDate,\n ArrowLeft: handleLeftFromDate,\n Right: handleRightFromDate,\n ArrowRight: handleRightFromDate,\n Home: handleHomeFromDate,\n End: handleEndFromDate,\n PageDown: handlePageDownFromDate,\n PageUp: handlePageUpFromDate,\n \"Shift+PageDown\": handleShiftPageDownFromDate,\n \"Shift+PageUp\": handleShiftPageUpFromDate,\n Tab: datePickerTabEventHandler.tabAhead,\n }),\n [CALENDAR_DATE_PICKER]: keymap({\n Tab: datePickerTabEventHandler.tabAhead,\n \"Shift+Tab\": datePickerTabEventHandler.tabBack,\n }),\n [CALENDAR_MONTH]: keymap({\n Up: handleUpFromMonth,\n ArrowUp: handleUpFromMonth,\n Down: handleDownFromMonth,\n ArrowDown: handleDownFromMonth,\n Left: handleLeftFromMonth,\n ArrowLeft: handleLeftFromMonth,\n Right: handleRightFromMonth,\n ArrowRight: handleRightFromMonth,\n Home: handleHomeFromMonth,\n End: handleEndFromMonth,\n PageDown: handlePageDownFromMonth,\n PageUp: handlePageUpFromMonth,\n }),\n [CALENDAR_MONTH_PICKER]: keymap({\n Tab: monthPickerTabEventHandler.tabAhead,\n \"Shift+Tab\": monthPickerTabEventHandler.tabBack,\n }),\n [CALENDAR_YEAR]: keymap({\n Up: handleUpFromYear,\n ArrowUp: handleUpFromYear,\n Down: handleDownFromYear,\n ArrowDown: handleDownFromYear,\n Left: handleLeftFromYear,\n ArrowLeft: handleLeftFromYear,\n Right: handleRightFromYear,\n ArrowRight: handleRightFromYear,\n Home: handleHomeFromYear,\n End: handleEndFromYear,\n PageDown: handlePageDownFromYear,\n PageUp: handlePageUpFromYear,\n }),\n [CALENDAR_YEAR_PICKER]: keymap({\n Tab: yearPickerTabEventHandler.tabAhead,\n \"Shift+Tab\": yearPickerTabEventHandler.tabBack,\n }),\n [DATE_PICKER_CALENDAR](event) {\n this.dataset.keydownKeyCode = event.keyCode;\n },\n [DATE_PICKER](event) {\n const keyMap = keymap({\n Escape: handleEscapeFromCalendar,\n });\n\n keyMap(event);\n },\n },\n focusout: {\n [DATE_PICKER_EXTERNAL_INPUT]() {\n validateDateInput(this);\n },\n [DATE_PICKER](event) {\n if (!this.contains(event.relatedTarget)) {\n hideCalendar(this);\n }\n },\n },\n input: {\n [DATE_PICKER_EXTERNAL_INPUT]() {\n reconcileInputValues(this);\n updateCalendarIfVisible(this);\n },\n },\n};\n\nif (!isIosDevice()) {\n datePickerEvents.mouseover = {\n [CALENDAR_DATE_CURRENT_MONTH]() {\n handleMouseoverFromDate(this);\n },\n [CALENDAR_MONTH]() {\n handleMouseoverFromMonth(this);\n },\n [CALENDAR_YEAR]() {\n handleMouseoverFromYear(this);\n },\n };\n}\n\nconst datePicker = behavior(datePickerEvents, {\n init(root) {\n select(DATE_PICKER, root).forEach((datePickerEl) => {\n enhanceDatePicker(datePickerEl);\n });\n },\n getDatePickerContext,\n disable,\n enable,\n isDateInputInvalid,\n setCalendarValue,\n validateDateInput,\n renderCalendar,\n updateCalendarIfVisible,\n});\n\n// #endregion Date Picker Event Delegation Registration / Component\n\nmodule.exports = datePicker;\n","const behavior = require(\"../utils/behavior\");\nconst select = require(\"../utils/select\");\nconst { prefix: PREFIX } = require(\"../config\");\nconst {\n getDatePickerContext,\n isDateInputInvalid,\n updateCalendarIfVisible,\n} = require(\"./date-picker\");\n\nconst DATE_PICKER_CLASS = `${PREFIX}-date-picker`;\nconst DATE_RANGE_PICKER_CLASS = `${PREFIX}-date-range-picker`;\nconst DATE_RANGE_PICKER_RANGE_START_CLASS = `${DATE_RANGE_PICKER_CLASS}__range-start`;\nconst DATE_RANGE_PICKER_RANGE_END_CLASS = `${DATE_RANGE_PICKER_CLASS}__range-end`;\n\nconst DATE_PICKER = `.${DATE_PICKER_CLASS}`;\nconst DATE_RANGE_PICKER = `.${DATE_RANGE_PICKER_CLASS}`;\nconst DATE_RANGE_PICKER_RANGE_START = `.${DATE_RANGE_PICKER_RANGE_START_CLASS}`;\nconst DATE_RANGE_PICKER_RANGE_END = `.${DATE_RANGE_PICKER_RANGE_END_CLASS}`;\n\nconst DEFAULT_MIN_DATE = \"0000-01-01\";\n\n/**\n * The properties and elements within the date range picker.\n * @typedef {Object} DateRangePickerContext\n * @property {HTMLElement} dateRangePickerEl\n * @property {HTMLElement} rangeStartEl\n * @property {HTMLElement} rangeEndEl\n */\n\n/**\n * Get an object of the properties and elements belonging directly to the given\n * date picker component.\n *\n * @param {HTMLElement} el the element within the date picker\n * @returns {DateRangePickerContext} elements\n */\nconst getDateRangePickerContext = (el) => {\n const dateRangePickerEl = el.closest(DATE_RANGE_PICKER);\n\n if (!dateRangePickerEl) {\n throw new Error(`Element is missing outer ${DATE_RANGE_PICKER}`);\n }\n\n const rangeStartEl = dateRangePickerEl.querySelector(\n DATE_RANGE_PICKER_RANGE_START\n );\n const rangeEndEl = dateRangePickerEl.querySelector(\n DATE_RANGE_PICKER_RANGE_END\n );\n\n return {\n dateRangePickerEl,\n rangeStartEl,\n rangeEndEl,\n };\n};\n\n/**\n * handle update from range start date picker\n *\n * @param {HTMLElement} el an element within the date range picker\n */\nconst handleRangeStartUpdate = (el) => {\n const { dateRangePickerEl, rangeStartEl, rangeEndEl } =\n getDateRangePickerContext(el);\n const { internalInputEl } = getDatePickerContext(rangeStartEl);\n const updatedDate = internalInputEl.value;\n\n if (updatedDate && !isDateInputInvalid(internalInputEl)) {\n rangeEndEl.dataset.minDate = updatedDate;\n rangeEndEl.dataset.rangeDate = updatedDate;\n rangeEndEl.dataset.defaultDate = updatedDate;\n } else {\n rangeEndEl.dataset.minDate = dateRangePickerEl.dataset.minDate || \"\";\n rangeEndEl.dataset.rangeDate = \"\";\n rangeEndEl.dataset.defaultDate = \"\";\n }\n\n updateCalendarIfVisible(rangeEndEl);\n};\n\n/**\n * handle update from range start date picker\n *\n * @param {HTMLElement} el an element within the date range picker\n */\nconst handleRangeEndUpdate = (el) => {\n const { dateRangePickerEl, rangeStartEl, rangeEndEl } =\n getDateRangePickerContext(el);\n const { internalInputEl } = getDatePickerContext(rangeEndEl);\n const updatedDate = internalInputEl.value;\n\n if (updatedDate && !isDateInputInvalid(internalInputEl)) {\n rangeStartEl.dataset.maxDate = updatedDate;\n rangeStartEl.dataset.rangeDate = updatedDate;\n rangeStartEl.dataset.defaultDate = updatedDate;\n } else {\n rangeStartEl.dataset.maxDate = dateRangePickerEl.dataset.maxDate || \"\";\n rangeStartEl.dataset.rangeDate = \"\";\n rangeStartEl.dataset.defaultDate = \"\";\n }\n\n updateCalendarIfVisible(rangeStartEl);\n};\n\n/**\n * Enhance an input with the date picker elements\n *\n * @param {HTMLElement} el The initial wrapping element of the date range picker component\n */\nconst enhanceDateRangePicker = (el) => {\n const dateRangePickerEl = el.closest(DATE_RANGE_PICKER);\n\n const [rangeStart, rangeEnd] = select(DATE_PICKER, dateRangePickerEl);\n\n if (!rangeStart) {\n throw new Error(\n `${DATE_RANGE_PICKER} is missing inner two '${DATE_PICKER}' elements`\n );\n }\n\n if (!rangeEnd) {\n throw new Error(\n `${DATE_RANGE_PICKER} is missing second '${DATE_PICKER}' element`\n );\n }\n\n rangeStart.classList.add(DATE_RANGE_PICKER_RANGE_START_CLASS);\n rangeEnd.classList.add(DATE_RANGE_PICKER_RANGE_END_CLASS);\n\n if (!dateRangePickerEl.dataset.minDate) {\n dateRangePickerEl.dataset.minDate = DEFAULT_MIN_DATE;\n }\n\n const { minDate } = dateRangePickerEl.dataset;\n rangeStart.dataset.minDate = minDate;\n rangeEnd.dataset.minDate = minDate;\n\n const { maxDate } = dateRangePickerEl.dataset;\n if (maxDate) {\n rangeStart.dataset.maxDate = maxDate;\n rangeEnd.dataset.maxDate = maxDate;\n }\n\n handleRangeStartUpdate(dateRangePickerEl);\n handleRangeEndUpdate(dateRangePickerEl);\n};\n\nconst dateRangePicker = behavior(\n {\n \"input change\": {\n [DATE_RANGE_PICKER_RANGE_START]() {\n handleRangeStartUpdate(this);\n },\n [DATE_RANGE_PICKER_RANGE_END]() {\n handleRangeEndUpdate(this);\n },\n },\n },\n {\n init(root) {\n select(DATE_RANGE_PICKER, root).forEach((dateRangePickerEl) => {\n enhanceDateRangePicker(dateRangePickerEl);\n });\n },\n }\n);\n\nmodule.exports = dateRangePicker;\n","const select = require(\"../utils/select\");\nconst behavior = require(\"../utils/behavior\");\nconst { prefix: PREFIX } = require(\"../config\");\nconst Sanitizer = require(\"../utils/sanitizer\");\n\nconst DROPZONE_CLASS = `${PREFIX}-file-input`;\nconst DROPZONE = `.${DROPZONE_CLASS}`;\nconst INPUT_CLASS = `${PREFIX}-file-input__input`;\nconst TARGET_CLASS = `${PREFIX}-file-input__target`;\nconst INPUT = `.${INPUT_CLASS}`;\nconst BOX_CLASS = `${PREFIX}-file-input__box`;\nconst INSTRUCTIONS_CLASS = `${PREFIX}-file-input__instructions`;\nconst PREVIEW_CLASS = `${PREFIX}-file-input__preview`;\nconst PREVIEW_HEADING_CLASS = `${PREFIX}-file-input__preview-heading`;\nconst DISABLED_CLASS = `${PREFIX}-file-input--disabled`;\nconst CHOOSE_CLASS = `${PREFIX}-file-input__choose`;\nconst ACCEPTED_FILE_MESSAGE_CLASS = `${PREFIX}-file-input__accepted-files-message`;\nconst DRAG_TEXT_CLASS = `${PREFIX}-file-input__drag-text`;\nconst DRAG_CLASS = `${PREFIX}-file-input--drag`;\nconst LOADING_CLASS = \"is-loading\";\nconst HIDDEN_CLASS = \"display-none\";\nconst INVALID_FILE_CLASS = \"has-invalid-file\";\nconst GENERIC_PREVIEW_CLASS_NAME = `${PREFIX}-file-input__preview-image`;\nconst GENERIC_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--generic`;\nconst PDF_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--pdf`;\nconst WORD_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--word`;\nconst VIDEO_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--video`;\nconst EXCEL_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--excel`;\nconst SPACER_GIF =\n \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\";\n\nlet TYPE_IS_VALID = Boolean(true); // logic gate for change listener\n\n/**\n * The properties and elements within the file input.\n * @typedef {Object} FileInputContext\n * @property {HTMLDivElement} dropZoneEl\n * @property {HTMLInputElement} inputEl\n */\n\n/**\n * Get an object of the properties and elements belonging directly to the given\n * file input component.\n *\n * @param {HTMLElement} el the element within the file input\n * @returns {FileInputContext} elements\n */\nconst getFileInputContext = (el) => {\n const dropZoneEl = el.closest(DROPZONE);\n\n if (!dropZoneEl) {\n throw new Error(`Element is missing outer ${DROPZONE}`);\n }\n\n const inputEl = dropZoneEl.querySelector(INPUT);\n\n return {\n dropZoneEl,\n inputEl,\n };\n};\n\n/**\n * Disable the file input component\n *\n * @param {HTMLElement} el An element within the file input component\n */\nconst disable = (el) => {\n const { dropZoneEl, inputEl } = getFileInputContext(el);\n\n inputEl.disabled = true;\n dropZoneEl.classList.add(DISABLED_CLASS);\n dropZoneEl.setAttribute(\"aria-disabled\", \"true\");\n};\n\n/**\n * Enable the file input component\n *\n * @param {HTMLElement} el An element within the file input component\n */\nconst enable = (el) => {\n const { dropZoneEl, inputEl } = getFileInputContext(el);\n\n inputEl.disabled = false;\n dropZoneEl.classList.remove(DISABLED_CLASS);\n dropZoneEl.removeAttribute(\"aria-disabled\");\n};\n\n/**\n *\n * @param {String} s special characters\n * @returns {String} replaces specified values\n */\nconst replaceName = (s) => {\n const c = s.charCodeAt(0);\n if (c === 32) return \"-\";\n if (c >= 65 && c <= 90) return `img_${s.toLowerCase()}`;\n return `__${(\"000\", c.toString(16)).slice(-4)}`;\n};\n\n/**\n * Creates an ID name for each file that strips all invalid characters.\n * @param {String} name - name of the file added to file input (searchvalue)\n * @returns {String} same characters as the name with invalid chars removed (newvalue)\n */\nconst makeSafeForID = (name) => name.replace(/[^a-z0-9]/g, replaceName);\n\n// Takes a generated safe ID and creates a unique ID.\nconst createUniqueID = (name) =>\n `${name}-${Math.floor(Date.now().toString() / 1000)}`;\n\n/**\n * Builds full file input component\n * @param {HTMLElement} fileInputEl - original file input on page\n * @returns {HTMLElement|HTMLElement} - Instructions, target area div\n */\nconst buildFileInput = (fileInputEl) => {\n const acceptsMultiple = fileInputEl.hasAttribute(\"multiple\");\n const fileInputParent = document.createElement(\"div\");\n const dropTarget = document.createElement(\"div\");\n const box = document.createElement(\"div\");\n const instructions = document.createElement(\"div\");\n const disabled = fileInputEl.hasAttribute(\"disabled\");\n let defaultAriaLabel;\n\n // Adds class names and other attributes\n fileInputEl.classList.remove(DROPZONE_CLASS);\n fileInputEl.classList.add(INPUT_CLASS);\n fileInputParent.classList.add(DROPZONE_CLASS);\n box.classList.add(BOX_CLASS);\n instructions.classList.add(INSTRUCTIONS_CLASS);\n instructions.setAttribute(\"aria-hidden\", \"true\");\n dropTarget.classList.add(TARGET_CLASS);\n // Encourage screenreader to read out aria changes immediately following upload status change\n fileInputEl.setAttribute(\"aria-live\", \"polite\");\n\n // Adds child elements to the DOM\n fileInputEl.parentNode.insertBefore(dropTarget, fileInputEl);\n fileInputEl.parentNode.insertBefore(fileInputParent, dropTarget);\n dropTarget.appendChild(fileInputEl);\n fileInputParent.appendChild(dropTarget);\n fileInputEl.parentNode.insertBefore(instructions, fileInputEl);\n fileInputEl.parentNode.insertBefore(box, fileInputEl);\n\n // Disabled styling\n if (disabled) {\n disable(fileInputEl);\n }\n\n // Sets instruction test and aria-label based on whether or not multiple files are accepted\n if (acceptsMultiple) {\n defaultAriaLabel = \"No files selected\";\n instructions.innerHTML = Sanitizer.escapeHTML`Drag files here or choose from folder`;\n fileInputEl.setAttribute(\"aria-label\", defaultAriaLabel);\n fileInputEl.setAttribute(\"data-default-aria-label\", defaultAriaLabel);\n } else {\n defaultAriaLabel = \"No file selected\";\n instructions.innerHTML = Sanitizer.escapeHTML`Drag file here or choose from folder`;\n fileInputEl.setAttribute(\"aria-label\", defaultAriaLabel);\n fileInputEl.setAttribute(\"data-default-aria-label\", defaultAriaLabel);\n }\n\n // IE11 and Edge do not support drop files on file inputs, so we've removed text that indicates that\n if (\n /rv:11.0/i.test(navigator.userAgent) ||\n /Edge\\/\\d./i.test(navigator.userAgent)\n ) {\n fileInputParent.querySelector(`.${DRAG_TEXT_CLASS}`).outerHTML = \"\";\n }\n\n return { instructions, dropTarget };\n};\n\n/**\n * Removes image previews, we want to start with a clean list every time files are added to the file input\n * @param {HTMLElement} dropTarget - target area div that encases the input\n * @param {HTMLElement} instructions - text to inform users to drag or select files\n */\nconst removeOldPreviews = (dropTarget, instructions, inputAriaLabel) => {\n const filePreviews = dropTarget.querySelectorAll(`.${PREVIEW_CLASS}`);\n const fileInputElement = dropTarget.querySelector(INPUT);\n const currentPreviewHeading = dropTarget.querySelector(\n `.${PREVIEW_HEADING_CLASS}`\n );\n const currentErrorMessage = dropTarget.querySelector(\n `.${ACCEPTED_FILE_MESSAGE_CLASS}`\n );\n\n /**\n * finds the parent of the passed node and removes the child\n * @param {HTMLElement} node\n */\n const removeImages = (node) => {\n node.parentNode.removeChild(node);\n };\n\n // Remove the heading above the previews\n if (currentPreviewHeading) {\n currentPreviewHeading.outerHTML = \"\";\n }\n\n // Remove existing error messages\n if (currentErrorMessage) {\n currentErrorMessage.outerHTML = \"\";\n dropTarget.classList.remove(INVALID_FILE_CLASS);\n }\n\n // Get rid of existing previews if they exist, show instructions\n if (filePreviews !== null) {\n if (instructions) {\n instructions.classList.remove(HIDDEN_CLASS);\n }\n fileInputElement.setAttribute(\"aria-label\", inputAriaLabel);\n Array.prototype.forEach.call(filePreviews, removeImages);\n }\n};\n\n/**\n * When new files are applied to file input, this function generates previews\n * and removes old ones.\n * @param {event} e\n * @param {HTMLElement} fileInputEl - file input element\n * @param {HTMLElement} instructions - text to inform users to drag or select files\n * @param {HTMLElement} dropTarget - target area div that encases the input\n */\n\nconst handleChange = (e, fileInputEl, instructions, dropTarget) => {\n const fileNames = e.target.files;\n const filePreviewsHeading = document.createElement(\"div\");\n const inputAriaLabel = fileInputEl.dataset.defaultAriaLabel;\n const fileStore = [];\n\n // First, get rid of existing previews\n removeOldPreviews(dropTarget, instructions, inputAriaLabel);\n\n // Then, iterate through files list and:\n // 1. Add selected file list names to aria-label\n // 2. Create previews\n for (let i = 0; i < fileNames.length; i += 1) {\n const reader = new FileReader();\n const fileName = fileNames[i].name;\n\n // Push updated file names into the store array\n fileStore.push(fileName);\n\n // read out the store array via aria-label, wording options vary based on file count\n if (i === 0) {\n fileInputEl.setAttribute(\n \"aria-label\",\n `You have selected the file: ${fileName}`\n );\n } else if (i >= 1) {\n fileInputEl.setAttribute(\n \"aria-label\",\n `You have selected ${fileNames.length} files: ${fileStore.join(\", \")}`\n );\n }\n\n // Starts with a loading image while preview is created\n reader.onloadstart = function createLoadingImage() {\n const imageId = createUniqueID(makeSafeForID(fileName));\n\n instructions.insertAdjacentHTML(\n \"afterend\",\n Sanitizer.escapeHTML`
\n \"\"${fileName}\n
`\n );\n };\n\n // Not all files will be able to generate previews. In case this happens, we provide several types \"generic previews\" based on the file extension.\n reader.onloadend = function createFilePreview() {\n const imageId = createUniqueID(makeSafeForID(fileName));\n const previewImage = document.getElementById(imageId);\n if (fileName.indexOf(\".pdf\") > 0) {\n previewImage.setAttribute(\n \"onerror\",\n `this.onerror=null;this.src=\"${SPACER_GIF}\"; this.classList.add(\"${PDF_PREVIEW_CLASS}\")`\n );\n } else if (\n fileName.indexOf(\".doc\") > 0 ||\n fileName.indexOf(\".pages\") > 0\n ) {\n previewImage.setAttribute(\n \"onerror\",\n `this.onerror=null;this.src=\"${SPACER_GIF}\"; this.classList.add(\"${WORD_PREVIEW_CLASS}\")`\n );\n } else if (\n fileName.indexOf(\".xls\") > 0 ||\n fileName.indexOf(\".numbers\") > 0\n ) {\n previewImage.setAttribute(\n \"onerror\",\n `this.onerror=null;this.src=\"${SPACER_GIF}\"; this.classList.add(\"${EXCEL_PREVIEW_CLASS}\")`\n );\n } else if (fileName.indexOf(\".mov\") > 0 || fileName.indexOf(\".mp4\") > 0) {\n previewImage.setAttribute(\n \"onerror\",\n `this.onerror=null;this.src=\"${SPACER_GIF}\"; this.classList.add(\"${VIDEO_PREVIEW_CLASS}\")`\n );\n } else {\n previewImage.setAttribute(\n \"onerror\",\n `this.onerror=null;this.src=\"${SPACER_GIF}\"; this.classList.add(\"${GENERIC_PREVIEW_CLASS}\")`\n );\n }\n\n // Removes loader and displays preview\n previewImage.classList.remove(LOADING_CLASS);\n previewImage.src = reader.result;\n };\n\n if (fileNames[i]) {\n reader.readAsDataURL(fileNames[i]);\n }\n\n // Adds heading above file previews, pluralizes if there are multiple\n if (i === 0) {\n dropTarget.insertBefore(filePreviewsHeading, instructions);\n filePreviewsHeading.innerHTML = `Selected file Change file`;\n } else if (i >= 1) {\n dropTarget.insertBefore(filePreviewsHeading, instructions);\n filePreviewsHeading.innerHTML = Sanitizer.escapeHTML`${\n i + 1\n } files selected Change files`;\n }\n\n // Hides null state content and sets preview heading class\n if (filePreviewsHeading) {\n instructions.classList.add(HIDDEN_CLASS);\n filePreviewsHeading.classList.add(PREVIEW_HEADING_CLASS);\n }\n }\n};\n\n/**\n * When using an Accept attribute, invalid files will be hidden from\n * file browser, but they can still be dragged to the input. This\n * function prevents them from being dragged and removes error states\n * when correct files are added.\n * @param {event} e\n * @param {HTMLElement} fileInputEl - file input element\n * @param {HTMLElement} instructions - text to inform users to drag or select files\n * @param {HTMLElement} dropTarget - target area div that encases the input\n */\nconst preventInvalidFiles = (e, fileInputEl, instructions, dropTarget) => {\n const acceptedFilesAttr = fileInputEl.getAttribute(\"accept\");\n dropTarget.classList.remove(INVALID_FILE_CLASS);\n\n /**\n * We can probably move away from this once IE11 support stops, and replace\n * with a simple es `.includes`\n * check if element is in array\n * check if 1 or more alphabets are in string\n * if element is present return the position value and -1 otherwise\n * @param {Object} file\n * @param {String} value\n * @returns {Boolean}\n */\n const isIncluded = (file, value) => {\n let returnValue = false;\n const pos = file.indexOf(value);\n if (pos >= 0) {\n returnValue = true;\n }\n return returnValue;\n };\n\n // Runs if only specific files are accepted\n if (acceptedFilesAttr) {\n const acceptedFiles = acceptedFilesAttr.split(\",\");\n const errorMessage = document.createElement(\"div\");\n\n // If multiple files are dragged, this iterates through them and look for any files that are not accepted.\n let allFilesAllowed = true;\n const scannedFiles = e.target.files || e.dataTransfer.files;\n for (let i = 0; i < scannedFiles.length; i += 1) {\n const file = scannedFiles[i];\n if (allFilesAllowed) {\n for (let j = 0; j < acceptedFiles.length; j += 1) {\n const fileType = acceptedFiles[j];\n allFilesAllowed =\n file.name.indexOf(fileType) > 0 ||\n isIncluded(file.type, fileType.replace(/\\*/g, \"\"));\n if (allFilesAllowed) {\n TYPE_IS_VALID = true;\n break;\n }\n }\n } else break;\n }\n\n // If dragged files are not accepted, this removes them from the value of the input and creates and error state\n if (!allFilesAllowed) {\n removeOldPreviews(dropTarget, instructions);\n fileInputEl.value = \"\"; // eslint-disable-line no-param-reassign\n dropTarget.insertBefore(errorMessage, fileInputEl);\n errorMessage.textContent =\n fileInputEl.dataset.errormessage || `This is not a valid file type.`;\n errorMessage.classList.add(ACCEPTED_FILE_MESSAGE_CLASS);\n dropTarget.classList.add(INVALID_FILE_CLASS);\n TYPE_IS_VALID = false;\n e.preventDefault();\n e.stopPropagation();\n }\n }\n};\n\n/**\n * 1. passes through gate for preventing invalid files\n * 2. handles updates if file is valid\n * @param {event} event\n * @param {HTMLElement} element\n * @param {HTMLElement} instructionsEl\n * @param {HTMLElement} target\n */\nconst handleUpload = (event, element, instructionsEl, dropTargetEl) => {\n preventInvalidFiles(event, element, instructionsEl, dropTargetEl);\n if (TYPE_IS_VALID === true) {\n handleChange(event, element, instructionsEl, dropTargetEl);\n }\n};\n\nconst fileInput = behavior(\n {},\n {\n init(root) {\n select(DROPZONE, root).forEach((fileInputEl) => {\n const { instructions, dropTarget } = buildFileInput(fileInputEl);\n\n dropTarget.addEventListener(\n \"dragover\",\n function handleDragOver() {\n this.classList.add(DRAG_CLASS);\n },\n false\n );\n\n dropTarget.addEventListener(\n \"dragleave\",\n function handleDragLeave() {\n this.classList.remove(DRAG_CLASS);\n },\n false\n );\n\n dropTarget.addEventListener(\n \"drop\",\n function handleDrop() {\n this.classList.remove(DRAG_CLASS);\n },\n false\n );\n\n fileInputEl.addEventListener(\n \"change\",\n (e) => handleUpload(e, fileInputEl, instructions, dropTarget),\n false\n );\n });\n },\n getFileInputContext,\n disable,\n enable,\n }\n);\n\nmodule.exports = fileInput;\n","const behavior = require(\"../utils/behavior\");\nconst { CLICK } = require(\"../events\");\nconst { prefix: PREFIX } = require(\"../config\");\n\nconst SCOPE = `.${PREFIX}-footer--big`;\nconst NAV = `${SCOPE} nav`;\nconst BUTTON = `${NAV} .${PREFIX}-footer__primary-link`;\nconst HIDE_MAX_WIDTH = 480;\n\n/**\n * Expands selected footer menu panel, while collapsing others\n */\nfunction showPanel() {\n if (window.innerWidth < HIDE_MAX_WIDTH) {\n const isOpen = this.getAttribute('aria-expanded') === 'true';\n const thisFooter = this.closest(SCOPE);\n\n // Close all other menus\n thisFooter.querySelectorAll(BUTTON).forEach((button) => {\n button.setAttribute('aria-expanded', false);\n });\n\n this.setAttribute('aria-expanded', !isOpen);\n }\n}\n\n/**\n * Swaps the

element for a