@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,480 lines • 1.02 MB
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 dataAttributeToDatasetKey(attribute) {
return attribute.replace(/^data-/, "").replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
}
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$1(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,
dataAttributeToDatasetKey,
getDatasetJson,
getDatasetOptionalJson,
getElement: getElement$1,
getElements,
regexJsonString,
resolveClasses,
setPositionClasses,
wasClickOutside
}, Symbol.toStringTag, { value: "Module" }));
function init$i() {
addScrollbarProperty();
}
const page = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
init: init$i
}, 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(context, ...messages) {
if (config$1.errorsAlways || allow(context)) {
output("error", context, messages);
}
}
const classLogger = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
log,
logError,
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(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(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(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" }));
function hasRequiredProps(required) {
return (obj) => {
return required.every((prop) => {
return Object.prototype.hasOwnProperty.call(obj, prop);
});
};
}
const _ComponentInitializer = class _ComponentInitializer {
/**
* Create a new instance of ComponentInitializer
* @param {Object} options Options for configuring the component initializer.
* @param {String} options.type Type of component (used for logs).
* @param {String} options.baseAttribute Prefix and base attribute name (used for base attribute and further element attribute names).
*/
constructor(options) {
if (!_ComponentInitializer.hasRequiredOptions(options)) {
throw new Error(
`Missing a required options: ${_ComponentInitializer.requiredOptions.join(", ")}`
);
}
this.options = Object.assign({}, _ComponentInitializer.defaults, options);
this.logTitle = `ULU: ${this.options.type}:
`;
}
/**
* Initializes the component based on the provided configuration.
* @param {Object} config The initialization configuration.
* @param {Function} config.setup The setup function to call for each element.
* @param {String} config.key [null] The optional key to use with attribute selector.
* @param {Boolean} config.withData [null] Whether to retrieve element data.
* @param {Array} config.events [null] Ulu events that should call setup when dispatched (ie. pageModified, pageResized)
* @param {Boolean} config.onPageResized [null] Whether to bind event listener for page resize end
* @param {HTMLElement} config.context [document] The context to query within.
*/
init(config2) {
var _a;
this.setupElements(config2);
if ((_a = config2.events) == null ? void 0 : _a.length) {
config2.events.forEach((name) => {
document.addEventListener(getName$1(name), () => this.setupElements(config2));
});
}
}
/**
* Processes the elements based on the provided configuration.
* @param {object} config The initialization configuration.
* @param {function} config.setup The setup function to call for each element.
* @param {string} config.key The optional key to use with attribute selector.
* @param {boolean} config.withData [false] Whether to retrieve element data.
* @param {boolean} config.onPageModified [true] Whether to bind event listener for page modifications.
* @param {HTMLElement} config.context [document] The context to query within.
*/
setupElements(config2) {
const { setup: setup2, key: key2, withData, context } = config2;
const elementsWithData = this.queryAllInitial(key2, withData, context);
elementsWithData.forEach((elementWithData) => setup2(elementWithData, this));
}
/**
* Get an attribute name
* @param {String} key Optional key, if no key will return baseAttribute if key will return key added to base
* @returns {String} String like data-ulu-dialog or data-ulu-dialog-element
*/
getAttribute(key2) {
const { baseAttribute: baseAttribute2 } = this.options;
return key2 ? `${baseAttribute2}-${key2}` : `${baseAttribute2}`;
}
/**
* Create an attribute selector
* @param {String} key Optional key (see getAttribute)
*/
attributeSelector(key2) {
return `[${this.getAttribute(key2)}]`;
}
/**
* Create an attribute selector for initial
* @return {String}
*/
attributeSelectorInitial(key2) {
return `${this.attributeSelector(key2)}:not([${this.getAttribute("init")}])`;
}
/**
* Queries all main elements of a component that have not been initialized and extracts their data attributes.
* @param {HTMLElement} context The context to query within.
* @param {Boolean} withData Include dataset from element (as optional JSON)
* @param {Node} context Element to query elements from
* @returns {Array<{element: HTMLElement, data: object, initialize: Function}>} An array of objects containing the elements, their data, and convenience function initialize() which when called will set the init attribute on the element
*/
queryAllInitial(key2, withData, context = document) {
const elements = [...context.querySelectorAll(this.attributeSelectorInitial(key2))];
return elements.map((element) => ({
element,
data: withData ? this.getData(element, key2) : null,
initialize: () => this.initializeElement(element)
}));
}
/**
* Sets the init attribute on an element, marking it as initialized.
* @param {HTMLElement} element The element to initialize.
*/
initializeElement(element) {
element.setAttribute(this.getAttribute("init"), "");
}
/**
* Get an elements dataset value as JSON or other value
* @return {*} The value of the dataset, if JSON will return object else will return string value or undefined
*/
getData(element, key2) {
const datasetKey = dataAttributeToDatasetKey(this.getAttribute(key2));
return getDatasetOptionalJson(element, datasetKey);
}
/**
* Will output namespaced console logs for the given initializer
*/
log(...msgs) {
console.log(this.logTitle, ...msgs);
}
/**
* Will output namespaced console warnings for the given initializer
*/
warn(...msgs) {
console.warn(this.logTitle, ...msgs);
}
/**
* Will output namespaced console error for the given initializer
*/
logError(...msgs) {
console.error(this.logTitle, ...msgs);
}
};
__publicField(_ComponentInitializer, "defaults", {
type: null,
baseAttribute: null
});
__publicField(_ComponentInitializer, "requiredOptions", [
"type",
"baseAttribute"
]);
__publicField(_ComponentInitializer, "hasRequiredOptions", hasRequiredProps(
_ComponentInitializer.requiredOptions
));
let ComponentInitializer = _ComponentInitializer;
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(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 baseAttribute = "data-ulu-dialog";
const initializer$d = new ComponentInitializer({ type: "dialog", baseAttribute });
const closeAttribute = initializer$d.getAttribute("close");
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,
/**
* When open and not non-modal, the body is prevented from scrolling (defaults to true).
*/
preventScroll: true
};
let currentDefaults$3 = { ...defaults$a };
function setDefaults$3(options) {
currentDefaults$3 = Object.assign({}, currentDefaults$3, options);
}
function init$h() {
initializer$d.init({
events: ["pageModified"],
withData: true,
setup({ element, initialize, data }) {
setupDialog(element, data);
initialize();
}
});
initializer$d.init({
key: "trigger",
events: ["pageModified"],
withData: true,
setup({ element, initialize, data: dialogId }) {
setupTrigger$1(element, dialogId);
initialize();
}
});
}
function setupTrigger$1(trigger, dialogId) {
trigger.addEventListener("click", handleTrigger);
function handleTrigger() {
var _a;
const dialog2 = document.getElementById(dialogId);
if (!dialog2) {
console.error("Could not locate dialog (id)", dialogId);
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, userOptions) {
const options = Object.assign({}, currentDefaults$3, userOptions);
const body = document.body;
dialog2.addEventListener("click", handleClicks);
if (options.documentEnd) {
body.appendChild(dialog2);
}
if (options.pauseVideos) {
prepVideos(dialog2);
}
if (!options.nonModal && options.preventScroll) {
let overflowValue = body.style.overflow;
dialog2.addEventListener("toggle", (event) => {
const isOpen = event.newState === "open";
if (isOpen) overflowValue = body.style.overflow;
body.style.overflow = isOpen ? "hidden" : overflowValue;
});
}
function handleClicks(event) {
const { target } = event;
const closeFromButton = target.closest(initializer$d.attributeSelector("close"));
let closeFromOutside = options.clickOutsideCloses && target === dialog2 && wasClickOutside(dialog2, event);
if (closeFromOutside || closeFromButton) {
if (options.pauseVideos) {
pauseVideos(dialog2);
}
dialog2.close();
}
}
}
function getDialogOptions(dialog2) {
return Object.assign({}, currentDefaults$3, initializer$d.getData(dialog2));
}
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,
baseAttribute,
closeAttribute,
defaults: defaults$a,
getDialogOptions,
init: init$h,
initializer: initializer$d,
setDefaults: setDefaults$3,
setupDialog,
setupTrigger: setupTrigger$1
}, Symbol.toStringTag, { value: "Module" }));
const initializer$c = new ComponentInitializer({
type: "modal-builder",
baseAttribute: "data-ulu-modal-builder"
});
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" ${closeAttribute} autofocus>
${config2.templateCloseIcon(config2)}
</button>
</header>
` : ""}
<div class="modal__body" ${initializer$c.getAttribute("body")}></div>
${config2.hasResizer ? `<div class="modal__resizer" ${initializer$c.getAttribute("resizer")}>
${config2.templateResizerIcon(config2)}
</div>` : ""}
</div>
`;
}
};
let currentDefaults$2 = { ...defaults$9 };
function setDefaults$2(options) {
currentDefaults$2 = Object.assign({}, currentDefaults$2, options);
}
function init$g() {
initializer$c.init({
withData: true,
events: ["pageModified"],
setup({ element, data }) {
buildModal(element, data);
}
});
}
function buildModal(content, options) {
const config2 = Object.assign({}, currentDefaults$2, options);
if (config2.position !== "center" && config2.allowResize) {
config2.hasResizer = true;
}
if (config2.debug) {
initializer$c.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(initializer$c.attributeSelector(key2));
const body = selectChild("body");
const resizer2 = selectChild("resizer");
const dialogOptions = separateDialogOptions(config2);
content.removeAttribute("id");
content.removeAttribute("hidden");
content.removeAttribute(initializer$c.getAttribute());
content.parentNode.replaceChild(modal, content);
body.appendChild(content);
modal.setAttribute(baseAttribute, 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$g,
initializer: initializer$c,
setDefaults: setDefaults$2
}, 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 initializer$b = new ComponentInitializer({
type: "flipcard",
baseAttribute: "data-ulu-flipcard"
});
function init$f() {
initializer$b.init({
withData: true,
events: ["pageModified"],
setup({ element, data, initialize }) {
const options = Object.assign({}, data);
const front = element.querySelector(initializer$b.attributeSelector("front"));
const back = element.querySelector(initializer$b.attributeSelector("back"));
new Flipcard(element, front, back, options);
initialize();
}
});
}
const _Flipcard = class _Flipcard {
constructor(container2, front, back, config2) {
if (!back) {
logError(this, "Missing an element (container, front, back)");
}
this.options = Object.assign({}, _Flipcard.defaults, config2);
const { namespace } = this.options;
_Flipcard.instances.push(this);
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.setVisibility(false);
log(this, this);
}
toggle() {
this.setVisibility(!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>
`;
}
setVisibility(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", []);
/**
* Default options for Flipcard
*/
__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 flipcard = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
Flipcard,
init: init$f,
initializer: initializer$b
}, Symbol.toStringTag, { value: "Module" }));
const initializer$a = new ComponentInitializer({
type: "grid",
baseAttribute: "data-grid"
});
function init$e(classes) {
initializer$a.init({
events: ["pageModified", "pageResized"],
setup({ element, initialize }) {
setPositionClasses(element, classes);
initialize();
}
});
}
const grid = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
init: init$e,
initializer: initializer$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[slideBeforeInd