@ulu/frontend
Version:
A versatile SCSS and JavaScript component library offering configurable, accessible components and flexible integration into any project, with SCSS modules suitable for modern JS frameworks.
1,491 lines • 786 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
var __publicField = (obj, key2, value) => __defNormalProp(obj, typeof key2 !== "symbol" ? key2 + "" : key2, value);
const scriptRel = "modulepreload";
const assetsURL = function(dep, importerUrl) {
return new URL(dep, importerUrl).href;
};
const seen = {};
const __vitePreload = function preload(baseModule, deps, importerUrl) {
let promise = Promise.resolve();
if (deps && deps.length > 0) {
const links = document.getElementsByTagName("link");
const cspNonceMeta = document.querySelector(
"meta[property=csp-nonce]"
);
const cspNonce = (cspNonceMeta == null ? void 0 : cspNonceMeta.nonce) || (cspNonceMeta == null ? void 0 : cspNonceMeta.getAttribute("nonce"));
promise = Promise.allSettled(
deps.map((dep) => {
dep = assetsURL(dep, importerUrl);
if (dep in seen) return;
seen[dep] = true;
const isCss = dep.endsWith(".css");
const cssSelector = isCss ? '[rel="stylesheet"]' : "";
const isBaseRelative = !!importerUrl;
if (isBaseRelative) {
for (let i = links.length - 1; i >= 0; i--) {
const link2 = links[i];
if (link2.href === dep && (!isCss || link2.rel === "stylesheet")) {
return;
}
}
} else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return;
}
const link = document.createElement("link");
link.rel = isCss ? "stylesheet" : scriptRel;
if (!isCss) {
link.as = "script";
}
link.crossOrigin = "";
link.href = dep;
if (cspNonce) {
link.setAttribute("nonce", cspNonce);
}
document.head.appendChild(link);
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener("load", res);
link.addEventListener(
"error",
() => rej(new Error(`Unable to preload CSS for ${dep}`))
);
});
}
})
);
}
function handlePreloadError(err) {
const e = new Event("vite:preloadError", {
cancelable: true
});
e.payload = err;
window.dispatchEvent(e);
if (!e.defaultPrevented) {
throw err;
}
}
return promise.then((res) => {
for (const item of res || []) {
if (item.status !== "rejected") continue;
handlePreloadError(item.reason);
}
return baseModule().catch(handlePreloadError);
});
};
const defaults$b = {
iconClassClose: "css-icon css-icon--close",
iconClassDragX: "css-icon css-icon--drag-x",
iconClassPrevious: "css-icon css-icon--angle-left",
iconClassNext: "css-icon css-icon--angle-right"
};
let currentSettings = { ...defaults$b };
function getDefaultSettings() {
return { ...defaults$b };
}
function updateSettings(changes) {
Object.assign(currentSettings, changes);
}
function getSettings() {
return { ...currentSettings };
}
function getSetting(key2) {
if (!currentSettings.hasOwnProperty(key2)) {
console.warn(`Attempted to access non-existent setting: ${key2}`);
return void 0;
}
return currentSettings[key2];
}
function updateSetting(key2, value) {
currentSettings[key2] = value;
}
function wrapSettingString(key2) {
return {
toString() {
return getSetting(key2);
}
};
}
const settings = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
getDefaultSettings,
getSetting,
getSettings,
updateSetting,
updateSettings,
wrapSettingString
}, Symbol.toStringTag, { value: "Module" }));
function debounce(callback, wait, immediate, valueThis) {
var timeout;
return function executedFunction() {
var context = this;
var args = arguments;
var later = function() {
timeout = null;
callback.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function isBrowser() {
return typeof window !== "undefined" && typeof window.document !== "undefined";
}
function createElementFromHtml(markup) {
const doc = new DOMParser().parseFromString(markup, "text/html");
return doc.body.firstElementChild;
}
if (isBrowser()) {
initResize();
initPrint();
}
const events = {
/**
* Event is dispatched when DOM in the page has changed, triggers updates from
* all modules listening for the change (init instances, etc)
* - Is triggered by modules that were responsible for modifying the page
*/
pageModified(context) {
context.dispatchEvent(new CustomEvent(getName$1("pageModified"), { bubbles: true }));
},
/**
* Event called when page is resized
*/
pageResized(context) {
context.dispatchEvent(new CustomEvent(getName$1("pageResized"), { bubbles: true }));
},
/**
* Event dispatched before page print begins (teardown/restructure/hide things)
*/
beforePrint(context) {
context.dispatchEvent(new CustomEvent(getName$1("beforePrint"), { bubbles: true }));
},
/**
* Event dispatched after page print (cleanup)
*/
afterPrint(context) {
context.dispatchEvent(new CustomEvent(getName$1("afterPrint"), { bubbles: true }));
}
};
function dispatch(type, context) {
if (events[type]) {
events[type](context);
} else {
console.warn(`Unable to dispatch site event: ${type} in context:`, context);
}
}
function getName$1(type) {
return "ulu:" + type;
}
function initResize() {
window.addEventListener("resize", debounce(() => dispatch("pageResized", document), 250));
}
function initPrint() {
window.addEventListener("beforeprint", () => {
dispatch("beforePrint", document);
});
window.addEventListener("afterprint", () => {
dispatch("afterPrint", document);
});
}
const index$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
dispatch,
getName: getName$1
}, Symbol.toStringTag, { value: "Module" }));
const regexJsonString = /^[{\[][\s\S]*[}\]]$/;
function getDatasetJson(element, key2) {
const passed = element.dataset[key2];
try {
return JSON.parse(passed);
} catch (error) {
console.error(`Error getting JSON from dataset (${key2}) -- "${passed}"
`, element, error);
return {};
}
}
function getDatasetOptionalJson(element, key2) {
const passed = element.dataset[key2];
if (passed && regexJsonString.test(passed.trim())) {
return getDatasetJson(element, key2);
} else {
return passed;
}
}
function wasClickOutside(element, event) {
const rect = element.getBoundingClientRect();
return event.clientY < rect.top || // above
event.clientY > rect.top + rect.height || // below
event.clientX < rect.left || // left side
event.clientX > rect.left + rect.width;
}
function setPositionClasses(parent, classes = {
columnFirst: "position-column-first",
columnLast: "position-column-last",
rowFirst: "position-row-first",
rowLast: "position-row-last"
}) {
const children = [...parent.children];
const rows = [];
let lastY;
children.forEach((child) => {
const y = child.getBoundingClientRect().y;
if (lastY !== y) rows.push([]);
rows[rows.length - 1].push(child);
lastY = y;
child.classList.remove(...Object.values(classes));
});
rows.forEach((row, index2) => {
if (index2 === 0)
row.forEach((child) => child.classList.add(classes.rowFirst));
if (index2 == rows.length - 1)
row.forEach((child) => child.classList.add(classes.rowLast));
row.forEach((child, childIndex) => {
if (childIndex === 0)
child.classList.add(classes.columnFirst);
if (childIndex == row.length - 1)
child.classList.add(classes.columnLast);
});
});
}
function getElement(target, context = document) {
if (typeof target === "string") {
return context.querySelector(target);
} else if (target instanceof Element) {
return target;
} else {
console.warn("getElement: Invalid target type (expected String/Node)", target);
return null;
}
}
function getElements(target, context = document) {
if (typeof target === "string") {
return [...context.querySelectorAll(target)];
} else if (target instanceof Element) {
return [target];
} else if (Array.isArray(target) || target instanceof NodeList) {
return [...target];
} else {
console.warn("getElement: Invalid target type (expected String/Node/Array/Node List)", target);
return null;
}
}
function resolveClasses(classes) {
if (typeof classes === "string") {
return classes.split(" ").filter((c) => c !== "");
} else if (Array.isArray(classes)) {
return classes;
} else if (!classes) {
return [];
} else {
console.warn("resolveClassArray: Invalid class input type.", classes);
return [];
}
}
function addScrollbarProperty(element = document.body, container2 = window, propName = "--ulu-scrollbar-width") {
const scrollbarWidth = container2.innerWidth - element.clientWidth;
element.style.setProperty(propName, `${scrollbarWidth}px`);
}
const dom = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
addScrollbarProperty,
getDatasetJson,
getDatasetOptionalJson,
getElement,
getElements,
regexJsonString,
resolveClasses,
setPositionClasses,
wasClickOutside
}, Symbol.toStringTag, { value: "Module" }));
function init$h() {
addScrollbarProperty();
}
const page = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
init: init$h
}, Symbol.toStringTag, { value: "Module" }));
function removeArrayElement(array, element) {
var index2 = array.indexOf(element);
if (index2 > -1) {
array.splice(index2, 1);
}
}
const config$1 = {
debug: false,
warningsAlways: true,
errorsAlways: true,
outputContext: false
};
const hasConsole = "console" in window;
function allow(context) {
return hasConsole && config$1.debug && ((context == null ? void 0 : context.debug) || context == null);
}
function getName(context) {
var _a;
return typeof context === "object" && ((_a = context == null ? void 0 : context.constructor) == null ? void 0 : _a.name);
}
function output(method, context, messages) {
const label = getName(context) || "Logger";
console[method](label, ...messages);
if (config$1.outputContext) {
console.log("Context:\n", context);
}
}
function set(changes) {
Object.assign(config$1, changes);
}
function log(context, ...messages) {
if (allow(context)) {
output("log", context, messages);
}
}
function logWarning(context, ...messages) {
if (config$1.warningsAlways || allow(context)) {
output("warn", context, messages);
}
}
function logError$1(context, ...messages) {
if (config$1.errorsAlways || allow(context)) {
output("error", context, messages);
}
}
const classLogger = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
log,
logError: logError$1,
logWarning,
set
}, Symbol.toStringTag, { value: "Module" }));
window.addEventListener(getName$1("pageResized"), () => {
BreakpointManager.instances.forEach((i) => i.update());
});
const _BreakpointManager = class _BreakpointManager {
/**
* @param {Object} config Configruation object
* @param {Array} config.order Array of strings that correspond to the breakpoints setup in the styles, Breakpoints from smallest to largest, defaults to [small, medium, large]
* @param {Array} config.customProperty Property to grab breakpoint from (default is --breakpoint)
* @param {Array} config.valueFromPsuedo Use the legacy method of grabbing breakpoint from psuedo element, default uses custom property
* @param {Node} config.element The element to retrieve active breakpoint from stylesheet. (default is html) For using the old psuedo method, adjust this to document.body
* @param {String} config.psuedoSelector Change psuedo selector used to get the breakpoint from the psuedo's content property
*/
constructor(config2) {
Object.assign(this, _BreakpointManager.defaults, config2);
this.active = null;
this.previous = null;
this.activeIndex = null;
this.resizeDirection = null;
this.previousIndex = null;
this.breakpoints = {};
this.onChangeCallbacks = [];
this.order.forEach((n) => this.breakpoints[n] = new Breakpoint(n, this));
log(this, this);
this.update();
_BreakpointManager.instances.push(this);
}
/**
* Add a callback for everytime a breakpoint changes
* - Not recommended, possibly use to watch for changes, etc
* - For more control use intance.at(name) with breakpoint methods
* @param {Function} callback Function to call, passed one argument current instance which can be used to get information about breakpoints
*/
onChange(callback) {
this.onChangeCallbacks.push(callback);
}
/**
* Remove change callback
* @param {Function} callback Function to remove
*/
removeOnChange(callback) {
removeArrayElement(this.onChangeCallbacks, callback);
}
/**
* Get breakpoint from a psuedo element
*/
getBreakpointInPsuedo() {
return window.getComputedStyle(this.element, this.psuedoSelector).content.replace(/^"|"$/g, "");
}
/**
* Get breakpoint from a custom property
*/
getBreakpointInProperty() {
return getComputedStyle(this.element).getPropertyValue(this.customProperty).trim();
}
/**
* Get breakpoint from element (design note: user could override prototype)
*/
getBreakpoint() {
if (this.valueFromPsuedo) {
return this.getBreakpointInPsuedo();
} else {
return this.getBreakpointInProperty();
}
}
/**
* Updates the active breakpoint by checking the element and executes handlers on change
*/
update() {
const name = this.getBreakpoint();
if (!name) {
logError$1(this, "Unable to get current breakpoint, maybe order is incorrect? Breakpoint check skipped!");
return;
}
if (name === this.active) return;
this.previous = this.active;
this.previousIndex = this.activeIndex;
const index2 = this.order.indexOf(name);
this.active = name;
this.activeIndex = index2;
const activeBreakpoint = this.at(this.active);
const mapBreakpoints = (n) => this.at(n);
const max2 = this.order.slice(index2).map(mapBreakpoints);
const notMax = this.order.slice(0, index2).map(mapBreakpoints);
const min2 = this.order.slice(0, index2 + 1).map(mapBreakpoints);
const notMin = this.order.slice(index2 + 1).map(mapBreakpoints);
const notOnly = this.order.slice().map(mapBreakpoints);
notOnly.splice(index2, 1);
log(this, "max:", max2.map((b) => b.name).join());
log(this, "min:", min2.map((b) => b.name).join());
max2.forEach((b) => b._setDirection("max", true));
min2.forEach((b) => b._setDirection("min", true));
activeBreakpoint._setDirection("only", true);
notMax.forEach((b) => b._setDirection("max", false));
notMin.forEach((b) => b._setDirection("min", false));
notOnly.forEach((b) => b._setDirection("only", false));
if (this.previousIndex !== null) {
this.resizeDirection = this.previousIndex < index2 ? "up" : "down";
}
this.onChangeCallbacks.forEach((cb) => cb(this));
}
/**
* Get a breakpoint by key
* @param {String} name The name of the breakpoint to get
* @return {Breakpoint} Breakpoint to act on (see Breakpoint class)
*/
at(name) {
const bp = this.breakpoints[name];
if (!name) {
logError$1(this, "Unable to find breakpoint for:", bp);
}
return bp;
}
};
__publicField(_BreakpointManager, "instances", []);
__publicField(_BreakpointManager, "defaults", {
element: document == null ? void 0 : document.documentElement,
valueFromPsuedo: false,
customProperty: "--breakpoint",
psuedoSelector: ":before",
order: ["none", "small", "medium", "large"],
debug: false
});
let BreakpointManager = _BreakpointManager;
class BreakpointDirection {
constructor(direction, breakpoint) {
this.direction = direction;
this.active = false;
this.on = [];
this.off = [];
this.breakpoint = breakpoint;
}
/**
* Change the state of the direction
*/
change(to) {
if (this.active !== to) {
if (to) this._call(true);
else if (this.active) this._call(false);
this.active = to;
}
}
/**
* Calls all functions in handlers or
*/
_call(forActive) {
const handlers = forActive ? this.on : this.off;
handlers.forEach((handler) => handler());
log(this.breakpoint._manager, `Handlers called (${forActive ? "on" : "off"}): ${this.direction}`);
}
/**
* Returns handlers in normalized object format on/off
*/
getHandlers(handler) {
return typeof handler !== "object" ? { on: handler } : handler;
}
/**
* Adds a handler for the direction, optionally use object to add off state handler
* @param {Function|Object} handler Function to be executed when direction is active, read object description for on/off
* @param {Function|Object} handler.on Function to be executed when direction is active
* @param {Function|Object} handler.off Function to be executed when direction was active and is now changed to inactive
*/
add(handler) {
const handlers = this.getHandlers(handler);
if (handlers.on) {
this.on.push(handlers.on);
}
if (handlers.off) {
this.off.push(handlers.off);
}
if (this.active && handlers.on) {
handlers.on();
log(this.breakpoint._manager, `Handler called immediately: ${this.direction}`, handlers.on);
}
}
/**
* Removes a handler
*/
remove(handler) {
const handlers = this.getHandlers(handler);
if (handlers.on) {
removeArrayElement(this.on, handlers.on);
}
if (handlers.off) {
removeArrayElement(this.off, handlers.off);
}
}
}
class Breakpoint {
constructor(name, manager) {
this.directions = {
max: new BreakpointDirection("max", this),
min: new BreakpointDirection("min", this),
only: new BreakpointDirection("only", this)
};
this._manager = manager;
this.name = name;
}
/**
* Private method used inrternally for managing direction activation
* - Each direction manages it's own state and handlers
* @param {String} direction The directional key
* @param {Boolean} active State of that direction to set
*/
_setDirection(direction, active) {
this.directions[direction].change(active);
}
/**
* Attach handler to be executed from the breakpoint and to all breakpoints below.
* - If the browser resizes from a breakpoint below this breakpoint,
* and above the breakpoint name specified, this handler will fire
* @param {Function} handler Handler to be executed
*/
max(handler) {
this.directions.max.add(handler);
}
/**
* Attach handler to be executed from the breakpoint and to all breakpoints below.
* - If the browser resizes from a breakpoint above this breakpoint,
* and below the breakpoint name specified, this handler will fire
* @param {Function} handler Handler to be executed
*/
min(handler) {
this.directions.min.add(handler);
}
/**
* Attach a handler to fire when the breakpoint is within the key
* @param {Function} handler Handler to be executed
*/
only(handler) {
this.directions.only.add(handler);
}
/**
* Remove handler
* @param {Function} handler Handler to be removed, extended on/off object style can be used
* @param {String} direction Remove handler only from specified direction, else search all directions
*/
remove(handler, direction) {
const directions = direction ? [direction] : ["max", "min", "only"];
directions.forEach((d) => d.remove(handler));
}
log(...msg) {
msg.unshift(`Breakpoint (${this.name}):`);
this._manager.log.apply(this._manager, msg);
}
}
const breakpoints = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
BreakpointManager
}, Symbol.toStringTag, { value: "Module" }));
let idCount = 0;
function newId() {
return `ulu-uid-${++idCount}`;
}
function ensureId(element) {
if (!element.id) {
element.id = newId();
}
}
const id = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
ensureId,
newId
}, Symbol.toStringTag, { value: "Module" }));
const _Collapsible = class _Collapsible {
/**
* @param {Object} elements Elements object
* @param {Node} elements.trigger Trigger button/element that opens/closes collapsible
* @param {Node} elements.content The content element that the trigger reveals
* @param {Object} config Configuration options (see defaults)
* @returns {Object} Collapsible instance
*/
constructor(elements, config2) {
const { trigger, content } = elements;
if (!trigger || !content) {
logError$1(this, "missing required elements (trigger or content)");
return;
}
const options = Object.assign({}, _Collapsible.defaults, config2);
this.elements = elements;
this.options = options;
this.isOpen = false;
this.handlers = {};
ensureId(trigger);
ensureId(content);
this.debugLog(this, this);
if (!options.selfManaged) {
this.attachHandlers();
}
this.setup();
}
attachHandlers() {
const { trigger, content } = this.elements;
const { focusoutCloses } = this.options;
this.clickHandler = (event) => {
this.onClick(event);
};
this.focusoutHandler = (event) => {
if (focusoutCloses) {
this.close(event);
}
};
trigger.addEventListener("click", this.clickHandler);
content.addEventListener("focusout", this.focusoutHandler);
}
removeHandlers() {
const { trigger, content } = this.elements;
trigger.removeEventListener("click", this.clickHandler);
content.removeEventListener("focusout", this.focusoutHandler);
}
onClick(event) {
this.toggle(event);
}
destroy() {
this.removeHandlers();
this.destroyTemporaryHandlers();
}
debugLog(...msgs) {
if (this.options.debug) {
log(this, ...msgs);
}
}
setup() {
const { trigger, content } = this.elements;
const { startOpen } = this.options;
trigger.setAttribute("role", "button");
trigger.setAttribute("aria-controls", content.id);
content.setAttribute("aria-labelledby", trigger.id);
this.setState(startOpen);
}
createEvent(name, detail) {
return new CustomEvent(getName$1("collapsible:" + name), { detail });
}
setState(isOpen, event) {
const ctx = {
collapsible: this,
isOpen,
event
};
this.debugLog(this, "Set state", ctx);
const { trigger, content } = this.elements;
const { openClass } = this.options;
const setClass = (el) => el.classList[isOpen ? "add" : "remove"](openClass);
trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
setClass(trigger);
setClass(content);
this.isOpen = isOpen;
this.options.onChange(ctx);
trigger.dispatchEvent(this.createEvent("change", ctx));
if (isOpen) {
this.setupTemporaryHandlers();
} else {
this.destroyTemporaryHandlers();
}
}
/**
* Setup handlers needed for closing once open
*/
setupTemporaryHandlers() {
const { content, trigger } = this.elements;
const { clickOutsideCloses, escapeCloses } = this.options;
const onDocumentClick = (event) => {
const { target } = event;
const inTrigger = trigger.contains(target);
const inContent = content.contains(target);
if (clickOutsideCloses && !inTrigger && !inContent) {
this.close(event);
}
};
const onDocumentKeydown = (event) => {
if (escapeCloses && event.key === "Escape") {
this.close(event);
}
};
document.addEventListener("click", onDocumentClick);
document.addEventListener("keydown", onDocumentKeydown);
this.handlers.onDocumentClick = onDocumentClick;
this.handlers.onDocumentKeydown = onDocumentKeydown;
}
/**
* Destroy handlers attached for closing once open
*/
destroyTemporaryHandlers() {
const { onDocumentClick, onDocumentKeydown } = this.handlers;
if (onDocumentClick) {
document.removeEventListener("click", onDocumentClick);
}
if (onDocumentClick) {
document.removeEventListener("keydown", onDocumentKeydown);
}
}
open(event) {
this.setState(true, event);
}
close(event) {
this.setState(false, event);
}
toggle(event) {
this.setState(!this.isOpen, event);
}
// This is removed because I think it's not useful, users should keep references
// Static Methods for managing instances of this class
// static instances = [];
// /**
// * Get collapsible instance by trigger element
// * @param {Node|String} trigger Trigger node or trigger ID
// */
// static getInstance(trigger) {
// return Collapsible.instances.find(c => typeof trigger === "string" ?
// c.elements.trigger.id === trigger :
// c.elements.trigger === trigger
// );
// }
// static removeInstance(instance) {
// const index = Collapsible.instances.findIndex(c => c === instance);
// if (index > -1) {
// Collapsible.instances.splice(index, 1);
// }
// }
};
__publicField(_Collapsible, "defaults", {
clickOutsideCloses: false,
// oneOpenPerContext: false, // This should be another module that manages instances within a context (accordions)
// clickWithinCloses: false, // Not sure how this was used but seems like it should be separate
focusoutCloses: false,
escapeCloses: false,
/**
* The module won't attach the handlers (you need to do it yourself)
*/
selfManaged: false,
/**
* This collapsible starts in open state
*/
startOpen: false,
/**
* Open/active state class
*/
openClass: "is-active",
/**
* Output debug info
*/
debug: true,
onChange(_ctx) {
}
});
let Collapsible = _Collapsible;
const collapsible = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
Collapsible
}, Symbol.toStringTag, { value: "Module" }));
const _Resizer = class _Resizer {
/**
*
* @param {Node} container Container to be resize
* @param {Node} control Resize handle element
* @param {Object} options Defualt can be changed on class
* @param {Boolean} options.debug Enable non-essential debugging logs
* @param {Boolean} options.overrideMaxWidth When script is activated by handle remove the elements max-width and allow the width of the resize to exceed the max (default false)
* @param {Boolean} options.fromLeft The script should assume the handle is on the left side of the element
*/
constructor(container2, control, options) {
if (!control || !container2) {
logError$1(this, "Missing required elements 'control' or 'container'");
}
this.options = Object.assign({}, _Resizer.defaults, options);
this.container = container2;
this.control = control;
this.handlerMousedown = this.onMousedown.bind(this);
this.control.addEventListener("mousedown", this.handlerMousedown);
}
destroy() {
this.control.removeEventListener("mousedown", this.handlerMousedown);
}
onMousedown(e) {
const { overrideMaxWidth, fromLeft } = this.options;
const doc = document.documentElement;
const win = document.defaultView;
const x = e.clientX;
const width = parseInt(win.getComputedStyle(this.container).width, 10);
if (overrideMaxWidth) {
this.container.style.maxWidth = "none";
}
const mousemove = (event) => {
const polarity = fromLeft ? -1 : 1;
this.container.style.width = `${width + (event.clientX - x) * polarity}px`;
};
const cleanup = () => {
doc.removeEventListener("mousemove", mousemove, false);
};
doc.addEventListener("mousemove", mousemove, false);
doc.addEventListener("mouseup", cleanup, { capture: true, once: true });
}
};
__publicField(_Resizer, "defaults", {
debug: false,
overrideMaxWidth: false,
fromLeft: false
});
let Resizer = _Resizer;
const resizer = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
Resizer
}, Symbol.toStringTag, { value: "Module" }));
const selectors = [
".youtube-embedded-video",
'iframe[title*="YouTube video player"]',
'iframe[src*="youtube.com/embed"]'
];
function pauseVideos$1(context = document) {
const videos = getVideos(context);
videos.forEach((video) => {
try {
video.contentWindow.postMessage('{"event":"command","func":"stopVideo","args":""}', "*");
} catch (error) {
console.error(error);
}
});
}
function prepVideos$1(context = document) {
const videos = getVideos(context);
videos.forEach((video) => {
const { src } = video;
if (src) {
video.src = src.split("?")[0] + "?rel=0&enablejsapi=1";
}
});
}
function getVideos(context) {
return context.querySelectorAll(selectors.join(", "));
}
const pauseYoutubeVideo = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
pauseVideos: pauseVideos$1,
prepVideos: prepVideos$1
}, Symbol.toStringTag, { value: "Module" }));
const attrs$d = {
init: "data-ulu-dialog-init",
dialog: "data-ulu-dialog",
trigger: "data-ulu-dialog-trigger",
close: "data-ulu-dialog-close"
};
const attrSelector$d = (key2) => `[${attrs$d[key2]}]`;
const attrSelectorInitial$9 = (key2) => `${attrSelector$d(key2)}:not([${attrs$d.init}])`;
const queryAllInitial$3 = (key2) => document.querySelectorAll(attrSelectorInitial$9(key2));
const defaults$a = {
/**
* Use non-modal interface for dialog
*/
nonModal: false,
/**
* Move the dialog to the document end (hoist out of content)
* - helpful if dialogs are within editor body, etc
*/
documentEnd: false,
/**
* Requires styling that reduces any padding/border on dialog
*/
clickOutsideCloses: true,
/**
* Whether or not to pause videos when dialog closes (currently just youtube and native)
*/
pauseVideos: true
};
let currentDefaults$3 = { ...defaults$a };
function setDefaults$3(options) {
currentDefaults$3 = Object.assign({}, currentDefaults$3, options);
}
function init$g() {
document.addEventListener(getName$1("pageModified"), setup$d);
setup$d();
}
function setup$d() {
const dialogs = queryAllInitial$3("dialog");
dialogs.forEach(setupDialog);
const triggers = queryAllInitial$3("trigger");
triggers.forEach(setupTrigger$2);
}
function setupTrigger$2(trigger) {
trigger.addEventListener("click", handleTrigger);
trigger.setAttribute(attrs$d.init, "");
function handleTrigger() {
var _a;
const id2 = trigger.dataset.uluDialogTrigger;
const dialog2 = document.getElementById(id2);
if (!dialog2) {
console.error("Could not locate dialog (id)", id2);
return;
}
if (((_a = dialog2 == null ? void 0 : dialog2.tagName) == null ? void 0 : _a.toLowerCase()) !== "dialog") {
console.error("Attempted to trigger non <dialog> element. Did you mean to use modal builder?");
return;
}
const options = getDialogOptions(dialog2);
dialog2[options.nonModal ? "show" : "showModal"]();
}
}
function setupDialog(dialog2) {
const options = getDialogOptions(dialog2);
dialog2.addEventListener("click", handleClicks);
dialog2.setAttribute(attrs$d.init, "");
if (options.documentEnd) {
document.body.appendChild(dialog2);
}
if (options.pauseVideos) {
prepVideos(dialog2);
}
function handleClicks(event) {
const { target } = event;
const closeFromButton = target.closest("[data-ulu-dialog-close]");
let closeFromOutside = options.clickOutsideCloses && target === dialog2 && wasClickOutside(dialog2, event);
if (closeFromOutside || closeFromButton) {
if (options.pauseVideos) {
pauseVideos(dialog2);
}
dialog2.close();
}
}
}
function getDialogOptions(dialog2) {
const options = getDatasetJson(dialog2, "uluDialog");
return Object.assign({}, currentDefaults$3, options);
}
function prepVideos(dialog2) {
prepVideos$1(dialog2);
}
function pauseVideos(dialog2) {
pauseVideos$1(dialog2);
const nativeVideos = dialog2.querySelectorAll("video");
nativeVideos.forEach((video) => video.pause());
}
const dialog = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
attrs: attrs$d,
defaults: defaults$a,
getDialogOptions,
init: init$g,
setDefaults: setDefaults$3,
setup: setup$d,
setupDialog,
setupTrigger: setupTrigger$2
}, Symbol.toStringTag, { value: "Module" }));
const attrs$c = {
builder: "data-ulu-modal-builder",
body: "data-ulu-modal-builder-body",
resizer: "data-ulu-modal-builder-resizer"
};
const attrSelector$c = (key2) => `[${attrs$c[key2]}]`;
const defaults$9 = {
title: null,
titleIcon: null,
nonModal: false,
documentEnd: true,
allowResize: false,
position: "center",
bodyFills: false,
noBackdrop: false,
size: "default",
print: false,
noMinHeight: false,
class: "",
classCloseIcon: wrapSettingString("iconClassClose"),
classResizerIcon: wrapSettingString("iconClassDragX"),
debug: false,
templateCloseIcon(config2) {
return `<span class="modal__close-icon ${config2.classCloseIcon}" aria-hidden="true"></span>`;
},
templateResizerIcon(config2) {
return `<span class="modal__resizer-icon ${config2.classResizerIcon}" aria-hidden="true"></span>`;
},
/**
* Default modal template
* @param {String} id ID for new modal
* @param {Object} config Resolved options
* @returns {String} Markup for modal
*/
template(id2, config2) {
const classes = [
"modal",
`modal--${config2.position}`,
`modal--${config2.size}`,
`modal--${config2.allowResize ? "resize" : "no-resize"}`,
...!config2.title ? ["modal--no-header"] : [],
...config2.bodyFills ? ["modal--body-fills"] : [],
...config2.noBackdrop ? ["modal--no-backdrop"] : [],
...config2.noMinHeight ? ["modal--no-min-height"] : [],
...config2.class ? [config2.class] : []
];
return `
<dialog id="${id2}" class="${classes.join(" ")}">
${config2.title ? `
<header class="modal__header">
<h2 class="modal__title">
${config2.titleIcon ? `<span class="modal__title-icon ${config2.titleIcon}" aria-hidden="true"></span>` : ""}
<span class="modal__title-text">${config2.title}</span>
</h2>
<button class="modal__close" aria-label="Close modal" ${attrs$d.close} autofocus>
${config2.templateCloseIcon(config2)}
</button>
</header>
` : ""}
<div class="modal__body" ${attrs$c.body}></div>
${config2.hasResizer ? `<div class="modal__resizer" ${attrs$c.resizer}>
${config2.templateResizerIcon(config2)}
</div>` : ""}
</div>
`;
}
};
let currentDefaults$2 = { ...defaults$9 };
function setDefaults$2(options) {
currentDefaults$2 = Object.assign({}, currentDefaults$2, options);
}
function init$f() {
document.addEventListener(getName$1("pageModified"), setup$c);
setup$c();
}
function setup$c() {
const builders = document.querySelectorAll(attrSelector$c("builder"));
builders.forEach(setupBuilder);
}
function setupBuilder(element) {
const options = getDatasetJson(element, "uluModalBuilder");
element.removeAttribute(attrs$c.builder);
buildModal(element, options);
}
function buildModal(content, options) {
const config2 = Object.assign({}, currentDefaults$2, options);
if (config2.position !== "center" && config2.allowResize) {
config2.hasResizer = true;
}
if (config2.debug) {
console.log(config2, content);
}
if (!content.id) {
throw new Error("Missing ID on modal");
}
const markup = config2.template(content.id, config2);
const modal = createElementFromHtml(markup.trim());
const selectChild = (key2) => modal.querySelector(attrSelector$c(key2));
const body = selectChild("body");
const resizer2 = selectChild("resizer");
const dialogOptions = separateDialogOptions(config2);
content.removeAttribute("id");
content.removeAttribute("hidden");
content.removeAttribute(attrs$c.builder);
content.parentNode.replaceChild(modal, content);
body.appendChild(content);
modal.setAttribute(attrs$d.dialog, JSON.stringify(dialogOptions));
if (config2.hasResizer) {
new Resizer(modal, resizer2, {
fromLeft: config2.position === "right"
});
}
if (config2.print) {
let printClone;
document.addEventListener(getName$1("beforePrint"), () => {
printClone = content.cloneNode(true);
modal.after(printClone);
});
document.addEventListener(getName$1("afterPrint"), () => {
printClone.remove();
});
}
return { modal };
}
function separateDialogOptions(config2) {
return Object.keys(defaults$a).reduce((acc, key2) => {
if (key2 in config2) {
acc[key2] = config2[key2];
}
return acc;
}, {});
}
const modalBuilder = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
buildModal,
defaults: defaults$9,
init: init$f,
setDefaults: setDefaults$2,
setup: setup$c,
setupBuilder
}, Symbol.toStringTag, { value: "Module" }));
const linebreaks = /(\r\n|\n|\r)/gm;
const multiSpace = /\s+/g;
function trimWhitespace(string) {
return string.replace(linebreaks, "").replace(multiSpace, " ").trim();
}
const _Flipcard = class _Flipcard {
constructor(container2, front, back, config2, debug = false) {
if (!back) {
logError$1(this, "Missing an element (container, front, back)");
}
this.options = Object.assign({}, _Flipcard.defaults, config2);
const { namespace } = this.options;
_Flipcard.instances.push(this);
this.debug = debug;
this.elements = { container: container2, front, back };
this.isOpen = false;
this.uid = `${namespace}-id-${_Flipcard.instances.length}`;
this.stateAttr = `data-${namespace}-state`.toLowerCase();
this.setup();
this.setVisiblity(false);
log(this, this);
}
toggle() {
this.setVisiblity(!this.isOpen);
}
setup() {
const { uid } = this;
const { namespace, proxyClick: proxyClick2 } = this.options;
const { container: container2, front, back } = this.elements;
const control = this.elements.control = document.createElement("button");
control.classList.add(this.getClass("control-button"));
control.setAttribute("type", "button");
control.innerHTML = this.createControlContent();
control.style.gridArea = namespace;
control.style.zIndex = "-1";
control.addEventListener("focusin", () => {
control.style.zIndex = "20";
});
control.addEventListener("focusout", () => {
control.style.zIndex = "-1";
});
control.addEventListener("click", this.toggle.bind(this));
back.parentNode.insertBefore(control, back);
container2.classList.add(this.options.namespace);
container2.setAttribute("style", trimWhitespace(this.containerCss()));
if (proxyClick2) {
container2.addEventListener("click", this.onProxyClick.bind(this));
}
front.style.gridArea = namespace;
back.style.gridArea = namespace;
control.id = `${uid}-control`;
control.setAttribute("aria-controls", back.id);
control.setAttribute("aria-expanded", "false");
back.id = `${uid}-back`;
back.setAttribute("aria-labelledby", control.id);
back.setAttribute("aria-hidden", "true");
}
/**
* Click handler on everything on container
* - Determines if click was something that should be ignored (link, etc)
*/
onProxyClick({ target }) {
const { exclude, allowSelection, selectionMin } = this.options.proxyClick;
const selection = window.getSelection();
if (exclude && !target.matches(exclude)) {
if (!allowSelection || selection.toString().length < selectionMin) {
this.toggle();
}
}
}
getClass(child) {
const { namespace } = this.options;
return child ? `${namespace}__${child}` : namespace;
}
createControlContent() {
return `
<span class="hidden-visually">Show More Information</span>
`;
}
setVisiblity(visible2) {
const { back, container: container2, control } = this.elements;
const state = visible2 ? "open" : "closed";
back.style.zIndex = visible2 ? "10" : "1";
back.style.visibility = visible2 ? "visible" : "hidden";
container2.setAttribute(this.stateAttr, state);
back.setAttribute("aria-hidden", visible2 ? "false" : "true");
control.setAttribute("aria-expanded", visible2 ? "true" : "false");
this.isOpen = visible2;
}
containerCss() {
return `
display: -ms-grid;
display: grid;
position: relative;
-ms-grid-columns: 1fr;
grid-template-columns: 1fr;
justify-items: stretch;
grid-template-areas: "${this.options.namespace}";
cursor: pointer;
`;
}
panelCss(zIndex = 1) {
return `
grid-area: ${this.options.namespace};
z-index: ${zIndex}
`;
}
};
__publicField(_Flipcard, "instances", []);
__publicField(_Flipcard, "defaults", {
namespace: "Flipcard",
proxyClick: {
allowSelection: true,
// Don't proxy click if the user has more than the minmimum selected
selectionMin: 10,
// Minimum length that qualifies as a selection
exclude: "a, input, textarea, button"
// Selectors to avoid closing a flipcard onProxyclick
}
});
let Flipcard = _Flipcard;
const attrs$b = {
init: "data-ulu-flipcard-init",
flipcard: "data-ulu-flipcard",
front: "data-ulu-flipcard-front",
back: "data-ulu-flipcard-back"
};
const attrSelector$b = (key2) => `[${attrs$b[key2]}]`;
const attrSelectorInitial$8 = (key2) => `${attrSelector$b(key2)}:not([${attrs$b.init}])`;
const instances$4 = [];
function init$e() {
document.addEventListener(getName$1("pageModified"), setup$b);
setup$b();
}
function setup$b() {
const builders = document.querySelectorAll(attrSelectorInitial$8("flipcard"));
builders.forEach(setupFlipcard);
}
function setupFlipcard(container2) {
container2.setAttribute(attrs$b.init, "");
const options = getDatasetOptionalJson(container2, "uluFlipcard");
const config2 = Object.assign({}, options);
const front = container2.querySelector(attrSelectorInitial$8("front"));
const back = container2.querySelector(attrSelectorInitial$8("back"));
instances$4.push(new Flipcard(container2, front, back, config2));
}
const flipcard = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
Flipcard,
attrs: attrs$b,
init: init$e,
setup: setup$b
}, Symbol.toStringTag, { value: "Module" }));
function init$d(selector = "[data-grid]", classes) {
document.addEventListener(getName$1("pageModified"), () => setup$a(selector, classes));
document.addEventListener(getName$1("pageResized"), () => setup$a(selector, classes));
setup$a(selector, classes);
}
function setup$a(selector, classes) {
document.querySelectorAll(selector).forEach((element) => setPositionClasses(element, classes || void 0));
}
const grid = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
init: init$d,
setup: setup$a
}, Symbol.toStringTag, { value: "Module" }));
function createPager() {
return function pager(instance, dir) {
const isNext = dir === "next";
const { track } = instance.elements;
if (!track.children) return 400;
const trackStyle = window.getComputedStyle(track);
const scrollPaddingRaw = trackStyle.getPropertyValue("scroll-padding-left").replace("auto", "0px");
const scrollPadding = parseInt(scrollPaddingRaw, 10);
const { scrollLeft, offsetWidth } = track;
const right = scrollLeft + offsetWidth;
const slides = [...track.children].map((element) => {
const { offsetLeft, offsetWidth: offsetWidth2 } = element;
return {
element,
offsetLeft,
offsetRight: offsetLeft + offsetWidth2
};
});
let slideFound;
if (isNext) {
slideFound = slides.find((slide) => slide.offsetRight >= right);
} else {
let slideBeforeIndex = slides.findLastIndex((slide) => slide.offsetLeft <= scrollLeft);
if (slideBeforeIndex) {
let slideBefore = slides[slideBeforeIndex];
let slidesBefore = slides.slice(0, slideBeforeIndex + 1);
slideFound = slidesBefore.find((slide) => {
const rightEdge = slide.offsetLeft + scrollPadding + offsetWidth;
return rightEdge >= slideBefore.offsetRight;
});
}
}
if (slideFound) {
return isNext ? slideFound.offsetLeft : slideFound.offsetLeft + scrollPadding;
} else {
return 400;
}
};
}
const overflowScrollerPager = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
createPager
}, Symbol.toStringTag, { value: "Module" }));
function hasRequiredProps(required) {
return (obj) => {
return required.every((prop) => {
return Object.prototype.hasOwnProperty.call(obj, prop);
});
};
}
const requiredElements$1 = [
"track",
"controls"
];
const _OverflowScroller = class _OverflowScroller {
constructor(elements, config2) {
this.options = Object.assign({}, _OverflowScroller.defaults, config2);
if (!hasRequiredProps(requiredElements$1)) {
logError$1(this, "Missing a required Element");
}
this.elements = {
...elements,
...this.createControls(elements.controls)
};
this.nextEnabled = true;
this.previousEnabled = true;
this.scrollHandler = (e) => this.onScroll(e);
this.elements.track.addEventListener("scroll", this.scrollHandler, { passive: true });
this.checkOverflow();
this.onScroll();
}
checkOverflow() {
const { track } = this.elements;
this.hasOverflow = track.scrollWidth > track.clientWidth;
}
createControls(context) {
const controls = document.createElement("ul");
const previousItem = document.createElement("li");
const nextItem = document.createElement("li");
const previous = this.createControlButton("previous");
const next = this.createControlButton("next");
const itemClass = this.getClass("controls-item");
nextItem.classList.add(itemClass);
nextItem.classList.add(itemClass + "--next");
previousItem.classList.add(itemClass);
previousItem.classList.add(itemClass + "--previous");
controls.classList.add(this.getClass("controls"));
previousItem.appendChild(previous);
nextItem.appendChild(next);
controls.appendChild(previousItem);
controls.appendChild(nextItem);
previous.addEventListener("click", this.previous.bind(this));
next.addEventListener("click", this.next.bind(this));
context.appendChild(controls);
return {
controls,
previousItem,
nextItem,
previous,
next
};
}
createControlButton(action) {
const button = document.createElement("button");
button.classList.add(this.getClass("control-button"));
button.classList.add(this.getClass(`control-button--${action}`));
button.classList.add(...this.options.buttonClasses);
button.setAttribute("type", "button");
button.innerHTML = this.getControlContent(action);
return button;
}
getControlContent(action) {
const classes = this.options[action === "next" ? "iconClassNext" : "iconClassPrevious"];
return `
<span class="hidden-visually">${action}</span>
<span class="${classes}" aria-hidden="true"></span>
`;
}
onScroll(event) {
if (!this.hasOverflow) return;
this.onScrollHorizontal();
}
onScrollHorizontal() {
const { nextEnabled, previousEnabled } = this;
const { track } = this.elements;
const { offsetStart, offsetEnd } = this.options;
const { scrollWidth, clientWidth, scrollLeft } = track;
const atStart = scrollLeft <= offsetStart;
const atEnd = scrollWidth - scrollLeft - offsetEnd <= clientWidth;
if (atStart && previousEnabled) {
this.setControlState("previous", false);
} else if (!atStart && !previousEnabled) {
this.setControlState("previous", true);
}
if (atEnd && nextEnabled) {
this.setControlState("next", false);
} else if (!atEnd && !nextEnabled) {
this.setControlState("next", true);
}
}
setControlState(dir, enabled) {
const isNext = dir === "next";
const { next, nextItem, previous, previousItem } = this.elements;
const item = isNext ? nextItem : previousItem;
const button = isNext ? next : previous;
const classlistMethod = enabled ? "remove" : "add";
item.classList[classlistMethod](this.getClass("controls-item--disabled"));
button.classList[enabled ? "remove" : "add"](this.getClass("control--disabled"));
if (enabled) {
button.removeAttribute("disabled");
} else {
button.setAttribute("disabled", "");
}
this[isNext ? "nextEnabled" : "previousEnabled"] = enabled;
}
resolveAmount(dir) {
const isNext = dir === "next";
const { amount } = this.options;
const { scrollLeft, offsetWidth } = this.elements.track;
if (amount === "auto") {
return isNext ? scrollLeft + offsetWidth : scrollLeft - offsetWidth;
} else if (typeof amount === "function") {
return amount(this, dir);
} else if (typeof amount === "number") {
return isNext ? scrollLeft + amount : scrollLeft - amount;
}
logError$1("Unable to resolve amount for scroll");
return 500;
}
next() {
this.elements.track.scrollTo({
top: 0,
left: this.resolveAmount("next"),
behavior: "smooth"
});
}
previous() {
this.elements.track.scrollTo({
top: 0,
left: this.resolveAmount("previous"),
behavior: "smooth"
});
}
getClass(child) {
con