rivet-core
Version:
Indiana University design system
1,428 lines (1,188 loc) • 159 kB
JavaScript
/*!
* rivet-core - @version 2.9.1
*
* Copyright (C) 2018 The Trustees of Indiana University
* SPDX-License-Identifier: BSD-3-Clause
*/
/******************************************************************************
* 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
***********************************************************************/
_clickOriginatedInsideCloseButton (event) {
return this.closeButton && this.closeButton.contains(event.target)
},
/************************************************************************
* Dismisses the alert.
***********************************************************************/
dismiss () {
if (!this._dismissEventDispatched()) { return }
this.element.remove();
},
/************************************************************************
* Returns true if the custom "dismiss" event was successfully
* dispatched.
*
* @private
* @returns {boolean} Event successfully dispatched
***********************************************************************/
_dismissEventDispatched () {
const dispatched = Component.dispatchCustomEvent(
'AlertDismissed',
this.element
);
return dispatched
}
}
}
}
/******************************************************************************
* Copyright (C) 2018 The Trustees of Indiana University
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
/******************************************************************************
* The dialog component can be used to present content in a smaller window that
* is displayed on top of the main application or site content.
*
* @see https://rivet.iu.edu/components/dialog/
*****************************************************************************/
class Dialog extends Component {
/****************************************************************************
* Gets the dialog's CSS selector.
*
* @static
* @returns {string} The CSS selector
***************************************************************************/
static get selector () {
return '[data-rvt-dialog]'
}
/****************************************************************************
* 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 dialog.