UNPKG

rivet-core

Version:

Indiana University design system

1,359 lines (1,134 loc) 167 kB
/*! * rivet-core - @version 2.9.1 * * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Rivet = {})); })(this, (function (exports) { 'use strict'; /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * Element.matches() polyfill *****************************************************************************/ if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } /****************************************************************************** * Element.closest() polyfill * * @see https://go.iu.edu/4ftm *****************************************************************************/ if (!Element.prototype.closest) { Element.prototype.closest = function (selector) { var el = this; var ancestor = this; if (!document.documentElement.contains(el)) { return null } do { if (ancestor.matches(selector)) { return ancestor } ancestor = ancestor.parentElement; } while (ancestor !== null) return null }; } /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * CustomEvent polyfill * * @see https://go.iu.edu/4ftn *****************************************************************************/ (function () { if (typeof window.CustomEvent === 'function') { return false } function CustomEvent (event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var customEvent = document.createEvent('CustomEvent'); customEvent.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); return customEvent } CustomEvent.prototype = window.Event.prototype; window.CustomEvent = CustomEvent; })(); /****************************************************************************** * Copyright (C) 2022 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * Array.from() polyfill * * @see https://go.iu.edu/4ftl *****************************************************************************/ if (!Array.from) { Array.from = (function () { var symbolIterator; try { symbolIterator = Symbol.iterator ? Symbol.iterator : 'Symbol(Symbol.iterator)'; } catch (e) { symbolIterator = 'Symbol(Symbol.iterator)'; } var toStr = Object.prototype.toString; var isCallable = function (fn) { return ( typeof fn === 'function' || toStr.call(fn) === '[object Function]' ) }; var toInteger = function (value) { var number = Number(value); if (isNaN(number)) return 0 if (number === 0 || !isFinite(number)) return number return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)) }; var maxSafeInteger = Math.pow(2, 53) - 1; var toLength = function (value) { var len = toInteger(value); return Math.min(Math.max(len, 0), maxSafeInteger) }; var setGetItemHandler = function setGetItemHandler (isIterator, items) { var iterator = isIterator && items[symbolIterator](); return function getItem (k) { return isIterator ? iterator.next() : items[k] } }; var getArray = function getArray ( T, A, len, getItem, isIterator, mapFn ) { // 16. Let k be 0. var k = 0; // 17. Repeat, while k < len… or while iterator is done (also steps a - h) while (k < len || isIterator) { var item = getItem(k); var kValue = isIterator ? item.value : item; if (isIterator && item.done) { return A } else { if (mapFn) { A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); } else { A[k] = kValue; } } k += 1; } if (isIterator) { throw new TypeError( 'Array.from: provided arrayLike or iterator has length more then 2 ** 52 - 1' ) } else { A.length = len; } return A }; // The length property of the from method is 1. return function from (arrayLikeOrIterator /*, mapFn, thisArg */) { // 1. Let C be the this value. var C = this; // 2. Let items be ToObject(arrayLikeOrIterator). var items = Object(arrayLikeOrIterator); var isIterator = isCallable(items[symbolIterator]); // 3. ReturnIfAbrupt(items). if (arrayLikeOrIterator == null && !isIterator) { throw new TypeError( 'Array.from requires an array-like object or iterator - not null or undefined' ) } // 4. If mapfn is undefined, then var mapping be false. var mapFn = arguments.length > 1 ? arguments[1] : void undefined; var T; if (typeof mapFn !== 'undefined') { // 5. else // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. if (!isCallable(mapFn)) { throw new TypeError( 'Array.from: when provided, the second argument must be a function' ) } // 5. b. If thisArg was supplied, var T be thisArg; else var T be undefined. if (arguments.length > 2) { T = arguments[2]; } } // 10. Let lenValue be Get(items, "length"). // 11. Let len be ToLength(lenValue). var len = toLength(items.length); // 13. If IsConstructor(C) is true, then // 13. a. Let A be the result of calling the [[Construct]] internal method // of C with an argument list containing the single item len. // 14. a. Else, Let A be ArrayCreate(len). var A = isCallable(C) ? Object(new C(len)) : new Array(len); return getArray( T, A, len, setGetItemHandler(isIterator, items), isIterator, mapFn ) } })(); } /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * ChildNode.remove() polyfill * * @see https://go.iu.edu/4fto *****************************************************************************/ (function (arr) { arr.forEach(function (item) { if (item.hasOwnProperty('remove')) { return } Object.defineProperty(item, 'remove', { configurable: true, enumerable: true, writable: true, value: function remove () { if (this.parentNode === null) { return } this.parentNode.removeChild(this); } }); }); })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); "inert"in HTMLElement.prototype||(Object.defineProperty(HTMLElement.prototype,"inert",{enumerable:!0,get:function(){return this.hasAttribute("inert")},set:function(h){h?this.setAttribute("inert",""):this.removeAttribute("inert");}}),window.addEventListener("load",function(){function h(a){var b=null;try{b=new KeyboardEvent("keydown",{keyCode:9,which:9,key:"Tab",code:"Tab",keyIdentifier:"U+0009",shiftKey:!!a,bubbles:!0});}catch(g){try{b=document.createEvent("KeyboardEvent"),b.initKeyboardEvent("keydown", !0,!0,window,"Tab",0,a?"Shift":"",!1,"en");}catch(d){}}if(b){try{Object.defineProperty(b,"keyCode",{value:9});}catch(g){}document.dispatchEvent(b);}}function k(a){for(;a&&a!==document.documentElement;){if(a.hasAttribute("inert"))return a;a=a.parentElement;}return null}function e(a){var b=a.path;return b&&b[0]||a.target}function l(a){a.path[a.path.length-1]!==window&&(m(e(a)),a.preventDefault(),a.stopPropagation());}function m(a){var b=k(a);if(b){if(document.hasFocus()&&0!==f){var g=(c||document).activeElement; h(0>f?!0:!1);if(g!=(c||document).activeElement)return;var d=document.createTreeWalker(document.body,NodeFilter.SHOW_ELEMENT,{acceptNode:function(a){return !a||!a.focus||0>a.tabIndex?NodeFilter.FILTER_SKIP:b.contains(a)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}});d.currentNode=b;d=(-1===Math.sign(f)?d.previousNode:d.nextNode).bind(d);for(var e;e=d();)if(e.focus(),(c||document).activeElement!==g)return}a.blur();}}(function(a){var b=document.createElement("style");b.type="text/css";b.styleSheet? b.styleSheet.cssText=a:b.appendChild(document.createTextNode(a));document.body.appendChild(b);})("/*[inert]*/*[inert]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}");var n=function(a){return null};window.ShadowRoot&&(n=function(a){for(;a&&a!==document.documentElement;){if(a instanceof window.ShadowRoot)return a;a=a.parentNode;}return null});var f=0;document.addEventListener("keydown",function(a){f=9===a.keyCode?a.shiftKey?-1:1:0;});document.addEventListener("mousedown", function(a){f=0;});var c=null;document.body.addEventListener("focus",function(a){var b=e(a);a=b==a.target?null:n(b);if(a!=c){if(c){if(!(c instanceof window.ShadowRoot))throw Error("not shadow root: "+c);c.removeEventListener("focusin",l,!0);}a&&a.addEventListener("focusin",l,!0);c=a;}m(b);},!0);document.addEventListener("click",function(a){var b=e(a);k(b)&&(a.preventDefault(),a.stopPropagation());},!0);})); /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ const globalSettings = { prefix: 'rvt' }; var Lie = typeof Promise === 'function' ? Promise : function (fn) { let queue = [], resolved = 0, value; fn($ => { value = $; resolved = 1; queue.splice(0).forEach(then); }); return {then}; function then(fn) { return (resolved ? setTimeout(fn, 0, value) : queue.push(fn)), this; } }; const TRUE = true, FALSE = false; const QSA$1 = 'querySelectorAll'; function add(node) { this.observe(node, {subtree: TRUE, childList: TRUE}); } /** * Start observing a generic document or root element. * @param {Function} callback triggered per each dis/connected node * @param {Element?} root by default, the global document to observe * @param {Function?} MO by default, the global MutationObserver * @returns {MutationObserver} */ const notify = (callback, root, MO) => { const loop = (nodes, added, removed, connected, pass) => { for (let i = 0, {length} = nodes; i < length; i++) { const node = nodes[i]; if (pass || (QSA$1 in node)) { if (connected) { if (!added.has(node)) { added.add(node); removed.delete(node); callback(node, connected); } } else if (!removed.has(node)) { removed.add(node); added.delete(node); callback(node, connected); } if (!pass) loop(node[QSA$1]('*'), added, removed, connected, TRUE); } } }; const observer = new (MO || MutationObserver)(records => { for (let added = new Set, removed = new Set, i = 0, {length} = records; i < length; i++ ) { const {addedNodes, removedNodes} = records[i]; loop(removedNodes, added, removed, FALSE, FALSE); loop(addedNodes, added, removed, TRUE, FALSE); } }); observer.add = add; observer.add(root || document); return observer; }; const QSA = 'querySelectorAll'; const {document: document$1, Element: Element$1, MutationObserver: MutationObserver$1, Set: Set$1, WeakMap: WeakMap$1} = self; const elements = element => QSA in element; const {filter} = []; var QSAO = options => { const live = new WeakMap$1; const drop = elements => { for (let i = 0, {length} = elements; i < length; i++) live.delete(elements[i]); }; const flush = () => { const records = observer.takeRecords(); for (let i = 0, {length} = records; i < length; i++) { parse(filter.call(records[i].removedNodes, elements), false); parse(filter.call(records[i].addedNodes, elements), true); } }; const matches = element => ( element.matches || element.webkitMatchesSelector || element.msMatchesSelector ); const notifier = (element, connected) => { let selectors; if (connected) { for (let q, m = matches(element), i = 0, {length} = query; i < length; i++) { if (m.call(element, q = query[i])) { if (!live.has(element)) live.set(element, new Set$1); selectors = live.get(element); if (!selectors.has(q)) { selectors.add(q); options.handle(element, connected, q); } } } } else if (live.has(element)) { selectors = live.get(element); live.delete(element); selectors.forEach(q => { options.handle(element, connected, q); }); } }; const parse = (elements, connected = true) => { for (let i = 0, {length} = elements; i < length; i++) notifier(elements[i], connected); }; const {query} = options; const root = options.root || document$1; const observer = notify(notifier, root, MutationObserver$1); const {attachShadow} = Element$1.prototype; if (attachShadow) Element$1.prototype.attachShadow = function (init) { const shadowRoot = attachShadow.call(this, init); observer.add(shadowRoot); return shadowRoot; }; if (query.length) parse(root[QSA](query)); return {drop, flush, observer, parse}; }; const {create, keys} = Object; const attributes = new WeakMap; const lazy = new Set; const query = []; const config = {}; const defined = {}; const attributeChangedCallback = (records, o) => { for (let h = attributes.get(o), i = 0, {length} = records; i < length; i++) { const {target, attributeName, oldValue} = records[i]; const newValue = target.getAttribute(attributeName); h.attributeChanged(attributeName, oldValue, newValue); } }; const set = (value, m, l, o) => { const handler = create(o, {element: {enumerable: true, value}}); for (let i = 0, {length} = l; i < length; i++) value.addEventListener(l[i].t, handler, l[i].o); m.set(value, handler); if (handler.init) handler.init(); const {observedAttributes} = o; if (observedAttributes) { const mo = new MutationObserver(attributeChangedCallback); mo.observe(value, { attributes: true, attributeOldValue: true, attributeFilter: observedAttributes.map(attributeName => { if (value.hasAttribute(attributeName)) handler.attributeChanged( attributeName, null, value.getAttribute(attributeName) ); return attributeName; }) }); attributes.set(mo, handler); } return handler; }; const {drop, flush, parse} = QSAO({ query, handle(element, connected, selector) { const {m, l, o} = config[selector]; const handler = m.get(element) || set(element, m, l, o); const method = connected ? 'connected' : 'disconnected'; if (method in handler) handler[method](); } }); const define = (selector, definition) => { if (-1 < query.indexOf(selector)) throw new Error('duplicated: ' + selector); flush(); const listeners = []; const retype = create(null); for (let k = keys(definition), i = 0, {length} = k; i < length; i++) { const key = k[i]; if (/^on/.test(key) && !/Options$/.test(key)) { const options = definition[key + 'Options'] || false; const lower = key.toLowerCase(); let type = lower.slice(2); listeners.push({t: type, o: options}); retype[type] = key; if (lower !== key) { type = key.slice(2, 3).toLowerCase() + key.slice(3); retype[type] = key; listeners.push({t: type, o: options}); } } } if (listeners.length) { definition.handleEvent = function (event) { this[retype[event.type]](event); }; } query.push(selector); config[selector] = {m: new WeakMap, l: listeners, o: definition}; parse(document.querySelectorAll(selector)); whenDefined(selector); if (!lazy.has(selector)) defined[selector]._(); }; const whenDefined = selector => { if (!(selector in defined)) { let _, $ = new Lie($ => { _ = $; }); defined[selector] = {_, $}; } return defined[selector].$; }; /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * Abstract base class from which all Rivet component classes are derived. *****************************************************************************/ class Component { /**************************************************************************** * Initializes all current and future instances of the component that are * added to the DOM. * * @static ***************************************************************************/ static initAll () { this.init(this.selector); } /**************************************************************************** * Initializes a specific component instance with the given selector. * * @static * @param {string} selector - CSS selector of component to initialize * @returns {HTMLElement} The initialized component ***************************************************************************/ static init (selector) { define(selector, this.methods); return document.querySelector(selector) } /**************************************************************************** * Gets the component's CSS selector. * * @abstract * @static * @returns {string} The CSS selector ***************************************************************************/ static get selector () { /* Virtual, must be implemented by subclass. */ } /**************************************************************************** * Gets the component's methods. * * @abstract * @static * @returns {Object} The component's methods ***************************************************************************/ static get methods () { /* Virtual, must be implemented by subclass. */ } /**************************************************************************** * Binds the given method to the component DOM element. * * @static * @param {Component} self - Component instance * @param {string} name - Method name * @param {Function} method - Method to bind ***************************************************************************/ static bindMethodToDOMElement (self, name, method) { Object.defineProperty(self.element, name, { value: method.bind(self), writable: false }); } /**************************************************************************** * Dispatches a custom browser event. * * @static * @param {string} eventName - Event name * @param {HTMLElement} element - Event target * @param {Object?} detail - Optional event details * @returns {boolean} Event success or failure ***************************************************************************/ static dispatchCustomEvent (eventName, element, detail = {}) { const prefix = globalSettings.prefix; const event = new CustomEvent(`${prefix}${eventName}`, { bubbles: true, cancelable: true, detail }); return element.dispatchEvent(event) } /**************************************************************************** * Dispatches a "component added" browser event. * * @static * @param {HTMLElement} element - New component DOM element * @returns {boolean} Event success or failure ***************************************************************************/ static dispatchComponentAddedEvent (element) { return this.dispatchCustomEvent('ComponentAdded', document, { component: element }) } /**************************************************************************** * Dispatches a "component removed" browser event. * * @static * @param {HTMLElement} element - Removed component DOM element * @returns {boolean} Event success or failure ***************************************************************************/ static dispatchComponentRemovedEvent (element) { return this.dispatchCustomEvent('ComponentRemoved', document, { component: element }) } /**************************************************************************** * Watches the component's DOM and updates references to child elements * if the DOM changes. Accepts an optional callback to perform additional * updates to the component on DOM change. * * @static * @param {Object} self - Component instance * @param {Function} callback - Optional callback ***************************************************************************/ static watchForDOMChanges (self, callback = null) { self.observer = new MutationObserver((mutationList, observer) => { self._initElements(); if (callback) { callback(); } }); self.observer.observe(self.element, { childList: true, subtree: true }); } /**************************************************************************** * Stop watching the component's DOM for changes. * * @static * @param {Object} self - Component instance ***************************************************************************/ static stopWatchingForDOMChanges (self) { self.observer.disconnect(); } /**************************************************************************** * Generates a random unique ID for a component's data attributes. Rivet * components and their child elements are automatically assigned IDs if the * developer does not manually specify one in the markup. * * @static * @returns {string} Unique ID ***************************************************************************/ static generateUniqueId () { return globalSettings.prefix + '-' + Math.random().toString(20).substr(2, 12) } /**************************************************************************** * Sets the given element attribute if no value was already specified in the * component's markup. * * @static * @param {HTMLElement} element - Element to set attribute on * @param {string} attribute - Attribute name * @param {string} value - Attribute value ***************************************************************************/ static setAttributeIfNotSpecified (element, attribute, value) { const existingValue = element.getAttribute(attribute); if (!existingValue) { element.setAttribute(attribute, value); } } } /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ const keyCodes = { up: 38, down: 40, left: 37, right: 39, tab: 9, enter: 13, escape: 27, home: 36, end: 35, pageUp: 33, pageDown: 34 }; /****************************************************************************** * Copyright (C) 2024 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ const SUPPRESS_EVENT = true; /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * The accordion component can be used to group content into sections that can * be opened and closed. * * @see https://rivet.iu.edu/components/accordion/ *****************************************************************************/ class Accordion extends Component { /**************************************************************************** * Gets the accordion's CSS selector. * * @static * @returns {string} The CSS selector ***************************************************************************/ static get selector () { return '[data-rvt-accordion]' } /**************************************************************************** * Gets an object containing the methods that should be attached to the * component's root DOM element. Used by wicked-elements to initialize a DOM * element with Web Component-like behavior. * * @static * @returns {Object} Object with component methods ***************************************************************************/ static get methods () { return { /************************************************************************ * Initializes the accordion. ***********************************************************************/ init () { this._initSelectors(); this._initElements(); this._initAttributes(); this._setInitialPanelStates(); Component.bindMethodToDOMElement(this, 'open', this.open); Component.bindMethodToDOMElement(this, 'close', this.close); }, /************************************************************************ * Initializes accordion child element selectors. * * @private ***********************************************************************/ _initSelectors () { this.triggerAttribute = 'data-rvt-accordion-trigger'; this.panelAttribute = 'data-rvt-accordion-panel'; this.triggerSelector = `[${this.triggerAttribute}]`; this.panelSelector = `[${this.panelAttribute}]`; }, /************************************************************************ * Initializes accordion child elements. * * @private ***********************************************************************/ _initElements () { this.triggers = Array.from( this.element.querySelectorAll(this.triggerSelector) ); this.panels = Array.from( this.element.querySelectorAll(this.panelSelector) ); }, /************************************************************************ * Initializes accordion attributes. * * @private ***********************************************************************/ _initAttributes () { this._assignComponentElementIds(); this._setTriggerButtonTypeAttributes(); }, /************************************************************************ * Assigns random IDs to the accordion component's child elements if * IDs were not already specified in the markup. * * @private ***********************************************************************/ _assignComponentElementIds () { this._assignTriggerIds(); this._assignPanelIds(); }, /************************************************************************ * Assigns a random ID to each trigger. * * @private ***********************************************************************/ _assignTriggerIds () { this.triggers.forEach(trigger => { const id = Component.generateUniqueId(); Component.setAttributeIfNotSpecified(trigger, this.triggerAttribute, id); Component.setAttributeIfNotSpecified(trigger, 'id', `${id}-label`); }); }, /************************************************************************ * Assigns a random ID to each panel. * * @private ***********************************************************************/ _assignPanelIds () { const numPanels = this.panels.length; for (let i = 0; i < numPanels; i++) { const trigger = this.triggers[i]; const panel = this.panels[i]; const panelId = trigger.getAttribute(this.triggerAttribute); Component.setAttributeIfNotSpecified(panel, this.panelAttribute, panelId); Component.setAttributeIfNotSpecified(panel, 'id', panelId); Component.setAttributeIfNotSpecified(panel, 'aria-labelledby', `${panelId}-label`); } }, /************************************************************************ * Adds `type="button"` to each trigger's button element. * * @private ***********************************************************************/ _setTriggerButtonTypeAttributes () { this.triggers.forEach(trigger => { Component.setAttributeIfNotSpecified(trigger, 'type', 'button'); }); }, /************************************************************************ * Sets the initial state of the accordion's panels. * * @private ***********************************************************************/ _setInitialPanelStates () { this._shouldOpenAllPanels() ? this._openAllPanels() : this._setPanelDefaultStates(); }, /************************************************************************ * Returns true if all panels should be opened when the component is * added to the DOM. * * @private * @returns {boolean} Panels should be opened ***********************************************************************/ _shouldOpenAllPanels () { return this.element.hasAttribute('data-rvt-accordion-open-all') }, /************************************************************************ * Opens all panels. * * @private ***********************************************************************/ _openAllPanels () { this.panels.forEach(panel => { this.open(panel.getAttribute(this.panelAttribute), SUPPRESS_EVENT); }); }, /************************************************************************ * Sets the default open/closed state for each panel based on the ARIA * attributes set by the developer. * * @private ***********************************************************************/ _setPanelDefaultStates () { this.panels.forEach(panel => { this._panelShouldBeOpen(panel) ? this.open(panel.getAttribute(this.panelAttribute), SUPPRESS_EVENT) : this.close(panel.getAttribute(this.panelAttribute), SUPPRESS_EVENT); }); }, /************************************************************************ * Returns true if the given panel element should be opened on page load. * * @private * @param {HTMLElement} panel - Panel DOM element * @returns {boolean} Panel should be opened ***********************************************************************/ _panelShouldBeOpen (panel) { return panel.hasAttribute('data-rvt-accordion-panel-init') }, /************************************************************************ * Called when the accordion is added to the DOM. ***********************************************************************/ connected () { Component.dispatchComponentAddedEvent(this.element); Component.watchForDOMChanges(this); }, /************************************************************************ * Called when the accordion is removed from the DOM. ***********************************************************************/ disconnected () { Component.dispatchComponentRemovedEvent(this.element); Component.stopWatchingForDOMChanges(this); }, /************************************************************************ * Handles click events broadcast to the accordion. * * @param {Event} event - Click event ***********************************************************************/ onClick (event) { if (!this._eventOriginatedInsideTrigger(event)) { return } this._setTriggerToToggle(event); this._triggerToToggleIsOpen() ? this.close(this.triggerToToggleId) : this.open(this.triggerToToggleId); }, /************************************************************************ * Returns true if the given event originated inside one of the * accordion's panel triggers. * * @private * @param {Event} event - Event * @returns {boolean} Event originated inside panel trigger ***********************************************************************/ _eventOriginatedInsideTrigger (event) { return event.target.closest(this.triggerSelector) }, /************************************************************************ * Sets references to the panel trigger to be toggled by the given click * event. These references are used by other click handler submethods. * * @private * @param {Event} event - Click event ***********************************************************************/ _setTriggerToToggle (event) { this.triggerToToggle = event.target.closest(this.triggerSelector); this.triggerToToggleId = this.triggerToToggle.getAttribute(this.triggerAttribute); }, /************************************************************************ * Returns true if the panel trigger to toggle is already open. * * @private * @returns {boolean} Click originated inside panel trigger ***********************************************************************/ _triggerToToggleIsOpen () { return this.triggerToToggle.getAttribute('aria-expanded') === 'true' }, /************************************************************************ * Handles keydown events broadcast to the accordion. * * @param {Event} event - Keydown event ***********************************************************************/ onKeydown (event) { if (!this._eventOriginatedInsideTrigger(event)) { return } this._setNeighboringTriggerIndexes(event); switch (event.keyCode) { case keyCodes.up: event.preventDefault(); this._focusPreviousTrigger(); break case keyCodes.down: event.preventDefault(); this._focusNextTrigger(); break case keyCodes.home: this._focusFirstTrigger(); break case keyCodes.end: this._focusLastTrigger(); break } }, /************************************************************************ * Sets the indexes of the panel trigger before and after the one from * which the given keydown event originated. Used to determine which * panel trigger should receive focus when the up and down arrow keys * are pressed. * * @private * @param {Event} event - Keydown event ***********************************************************************/ _setNeighboringTriggerIndexes (event) { const currentTrigger = event.target.closest(this.triggerSelector); this.previousTriggerIndex = this.triggers.indexOf(currentTrigger) - 1; this.nextTriggerIndex = this.triggers.indexOf(currentTrigger) + 1; }, /************************************************************************ * Moves focus to the panel trigger before the one that currently has * focus. If focus is currently on the first trigger, move focus to the * last trigger. * * @private ***********************************************************************/ _focusPreviousTrigger () { this.triggers[this.previousTriggerIndex] ? this.triggers[this.previousTriggerIndex].focus() : this.triggers[this.triggers.length - 1].focus(); }, /************************************************************************ * Moves focus to the panel trigger after the one that currently has * focus. If focus is currently on the last trigger, move focus to the * first trigger. * * @private ***********************************************************************/ _focusNextTrigger () { this.triggers[this.nextTriggerIndex] ? this.triggers[this.nextTriggerIndex].focus() : this.triggers[0].focus(); }, /************************************************************************ * Moves focus to the first panel trigger. * * @private ***********************************************************************/ _focusFirstTrigger () { this.triggers[0].focus(); }, /************************************************************************ * Moves focus to the last panel trigger. * * @private ***********************************************************************/ _focusLastTrigger () { this.triggers[this.triggers.length - 1].focus(); }, /************************************************************************ * Opens the panel with the given data-rvt-accordion-panel ID value. * * @param {string} childMenuId - Panel ID * @param {boolean} suppressEvent - Suppress open event ***********************************************************************/ open (panelId, suppressEvent = false) { this._setPanelToOpen(panelId); if (!this._panelToOpenExists()) { console.warn(`No such accordion panel '${panelId}' in open()`); return } if (!suppressEvent) if (!this._eventDispatched('AccordionOpened', this.panelToOpen)) { return } this._openPanel(); }, /************************************************************************ * Sets references to the panel to be opened. These references are used * by other submethods. * * @private * @param {string} panelId - Panel ID ***********************************************************************/ _setPanelToOpen (panelId) { this.triggerToOpen = this.element.querySelector( `[${this.triggerAttribute} = "${panelId}"]` ); this.panelToOpen = this.element.querySelector( `[${this.panelAttribute} = "${panelId}"]` ); }, /************************************************************************ * Returns true if the panel to open actually exists in the DOM. * * @private * @returns {boolean} Panel to open exists ***********************************************************************/ _panelToOpenExists () { return this.panelToOpen }, /************************************************************************ * Expands the accordion panel to be opened. * * @private ***********************************************************************/ _openPanel () { this.triggerToOpen.setAttribute('aria-expanded', 'true'); this.panelToOpen.removeAttribute('hidden'); }, /************************************************************************ * Closes the panel with the given data-rvt-accordion-panel ID value. * * @param {string} childMenuId - Panel ID * @param {boolean} suppressEvent - Suppress close event ***********************************************************************/ close (panelId, suppressEvent = false) { this._setPanelToClose(panelId); if (!this._panelToCloseExists()) { console.warn(`No such accordion panel '${panelId}' in close()`); return } if (!suppressEvent) if (!this._eventDispatched('AccordionClosed', this.panelToClose)) { return } this._closePanel(); }, /************************************************************************ * Sets references to the panel to be closed. These references are used * by other submethods. * * @private * @param {string} panelId - Panel ID ***********************************************************************/ _setPanelToClose (panelId) { this.triggerToClose = this.element.querySelector( `[${this.triggerAttribute} = "${panelId}"]` ); this.panelToClose = this.element.querySelector( `[${this.panelAttribute} = "${panelId}"]` ); }, /************************************************************************ * Returns true if the panel to close actually exists in the DOM. * * @private * @returns {boolean} Panel to close exists ***********************************************************************/ _panelToCloseExists () { return this.panelToClose }, /************************************************************************ * Collapses the accordion panel to be closed. * * @private ***********************************************************************/ _closePanel () { this.triggerToClose.setAttribute('aria-expanded', 'false'); this.panelToClose.setAttribute('hidden', ''); }, /************************************************************************ * Returns true if the custom event with the given name was successfully * dispatched. * * @private * @param {string} name - Event name * @param {HTMLElement} panel - Panel DOM element toggled by event * @returns {boolean} Event successfully dispatched ***********************************************************************/ _eventDispatched (name, panel) { const dispatched = Component.dispatchCustomEvent( name, this.element, { panel } ); return dispatched } } } } /****************************************************************************** * Copyright (C) 2018 The Trustees of Indiana University * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ /****************************************************************************** * The alert component displays brief important messages to the user like * errors or action confirmations. * * @see https://rivet.iu.edu/components/alert/ *****************************************************************************/ class Alert extends Component { /**************************************************************************** * Gets the alert's CSS selector. * * @static * @returns {string} The CSS selector ***************************************************************************/ static get selector () { return '[data-rvt-alert]' } /**************************************************************************** * Gets an object containing the methods that should be attached to the * component's root DOM element. Used by wicked-elements to initialize a DOM * element with Web Component-like behavior. * * @static * @returns {Object} Object with component methods ***************************************************************************/ static get methods () { return { /************************************************************************ * Initializes the alert. ***********************************************************************/ init () { this._initSelectors(); this._initElements(); Component.bindMethodToDOMElement(this, 'dismiss', this.dismiss); }, /************************************************************************ * Initializes alert child element selectors. * * @private ***********************************************************************/ _initSelectors () { this.closeButtonAttribute = 'data-rvt-alert-close'; this.closeButtonSelector = `[${this.closeButtonAttribute}]`; }, /************************************************************************ * Initializes alert child elements. * * @private ***********************************************************************/ _initElements () { this.closeButton = this.element.querySelector(this.closeButtonSelector); }, /************************************************************************ * Called when the alert is added to the DOM. ***********************************************************************/ connected () { Component.dispatchComponentAddedEvent(this.element); }, /************************************************************************ * Called when the alert is removed from the DOM. ***********************************************************************/ disconnected () { Component.dispatchComponentRemovedEvent(this.element); }, /************************************************************************ * Handles click events broadcast to the alert. * * @param {Event} event - Click event ***********************************************************************/ onClick (event) { if (this._clickOriginatedInsideCloseButton(event)) { this.dismiss(); } }, /************************************************************************ * Returns true if the given click event originated inside the * alert's close button. * * @private * @param {Event} event - Click event * @returns {boolean} Click originated inside content area **********************