@aegisjsproject/router
Version:
A simple but powerful router module
1,861 lines (1,643 loc) • 86.7 kB
JavaScript
'use strict';
var state = require('@aegisjsproject/state');
function stringify(thing) {
switch(typeof thing) {
case 'string':
return thing;
case 'function':
throw new TypeError('Functions are not supported.');
case 'undefined':
return '';
case 'object':
if (thing === null) {
return '';
} else if (thing instanceof Date) {
return thing.toISOString();
} else if (Array.isArray(thing)) {
return thing.map(stringify).join(',');
} else if (thing instanceof ArrayBuffer && Uint8Array.prototype.toBase64 instanceof Function) {
return new Uint8Array(thing).toBase64();
} else if (ArrayBuffer.isView(thing) && thing.toBase64 instanceof Function) {
return thing.toBase64();
} else if (thing instanceof Blob) {
return URL.createObjectURL(thing);
} else {
return thing.toString();
}
default:
return thing.toString();
}
}
/**
* Escapes a component of a URL, also protecting against path traversal
*
* @param {string} str
* @returns {string} The URL-safe string
*/
function escape(str) {
return encodeURIComponent(stringify(str).trim()).replaceAll('..%2F', '%2E%2E%2F').replaceAll('.%2F', '%2E%2E%2F');
}
/**
* Creates a URL parser tagged template with a custom base
*
* @param {string} [base=document.baseURI] Base to parse relative URLs from
* @returns {Function} A URL parsing tagged template
*/
function createURLParser(base = globalThis?.document?.baseURI) {
return function url(strings, value, ...values) {
if (value instanceof Blob && strings.length === 2 && strings[0] === '' && strings[1] === '') {
return new URL(URL.createObjectURL(value));
} else if (URL.canParse(value)) {
return URL.parse(String.raw(strings, '', ...values.map(escape)), value);
} else if (strings[0].startsWith('/')) {
return URL.parse(String.raw(strings, escape(value), ...values.map(escape)), base);
} else if (strings[0].startsWith('./') || strings[0].startsWith('../')) {
return URL.parse(String.raw(strings, escape(value), ...values.map(escape)), base);
} else {
return URL.parse(String.raw(strings, escape(value), ...values.map(escape)));
}
};
}
/**
* A function for creating URL objects from tagged template literals.
*
* @param {TemplateStringsArray} strings - The template string parts.
* @param {...any} args - The template string substitutions.
* @returns {URL | null} - A URL object if the URL is valid, otherwise null.
*/
const url = createURLParser();
const $$ = (selector, base = document) => base.querySelectorAll(selector);
const $ = (selector, base = document) => base.querySelector(selector);
const requestFullscreen = { data: 'requestFullscreenSelector' };
const toggleFullsceen = { data: 'toggleFullscreenSelector' };
const FUNCS = {
debug: {
log: 'aegis:debug:log',
info: 'aegis:debug:info',
warn: 'aegis:debug:warn',
error: 'aegis:debug:error',
},
navigate: {
back: 'aegis:navigate:back',
forward: 'aegis:navigate:forward',
reload: 'aegis:navigate:reload',
close: 'aegis:navigate:close',
link: 'aegis:navigate:go',
popup: 'aegis:navigate:popup',
},
ui: {
print: 'aegis:ui:print',
remove: 'aegis:ui:remove',
hide: 'aegis:ui:hide',
unhide: 'aegis:ui:unhide',
showModal: 'aegis:ui:showModal',
closeModal: 'aegis:ui:closeModal',
showPopover: 'aegis:ui:showPopover',
hidePopover: 'aegis:ui:hidePopover',
togglePopover: 'aegis:ui:togglePopover',
enable: 'aegis:ui:enable',
disable: 'aegis:ui:disable',
scrollTo: 'aegis:ui:scrollTo',
prevent: 'aegis:ui:prevent',
revokeObjectURL: 'aegis:ui:revokeObjectURL',
cancelAnimationFrame: 'aegis:ui:cancelAnimationFrame',
clearInterval: 'aegis:clearInterval',
clearTimeout: 'aegis:clearTimeout',
requestFullscreen: 'aegis:ui:requestFullscreen',
toggleFullscreen: 'aegis:ui:toggleFullsceen',
exitFullsceen: 'aegis:ui:exitFullscreen',
open: 'aegis:ui:open',
close: 'aegis:ui:close',
abortController: 'aegis:ui:controller:abort',
},
};
/**
* @type {Map<string, function>}
*/
const registry = new Map([
[FUNCS.debug.log, console.log],
[FUNCS.debug.warn, console.warn],
[FUNCS.debug.error, console.error],
[FUNCS.debug.info, console.info],
[FUNCS.navigate.back, () => history.back()],
[FUNCS.navigate.forward, () => history.forward()],
[FUNCS.navigate.reload, () => history.go(0)],
[FUNCS.navigate.close, () => globalThis.close()],
[FUNCS.navigate.link, event => {
if (event.isTrusted) {
event.preventDefault();
location.href = event.currentTarget.dataset.url;
}
}],
[FUNCS.navigate.popup, event => {
if (event.isTrusted) {
event.preventDefault();
globalThis.open(event.currentTarget.dataset.url);
}
}],
[FUNCS.ui.hide, ({ currentTarget }) => {
$$(currentTarget.dataset.hideSelector).forEach(el => el.hidden = true);
}],
[FUNCS.ui.unhide, ({ currentTarget }) => {
$$(currentTarget.dataset.unhideSelector).forEach(el => el.hidden = false);
}],
[FUNCS.ui.disable, ({ currentTarget }) => {
$$(currentTarget.dataset.disableSelector).forEach(el => el.disabled = true);
}],
[FUNCS.ui.enable, ({ currentTarget }) => {
$$(currentTarget.dataset.enableSelector).forEach(el => el.disabled = false);
}],
[FUNCS.ui.remove, ({ currentTarget }) => {
$$(currentTarget.dataset.removeSelector).forEach(el => el.remove());
}],
[FUNCS.ui.scrollTo, ({ currentTarget }) => {
const target = $(currentTarget.dataset.scrollToSelector);
if (target instanceof Element) {
target.scrollIntoView({
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches
? 'instant'
: 'smooth',
});
}
}],
[FUNCS.ui.revokeObjectURL, ({ currentTarget }) => URL.revokeObjectURL(currentTarget.src)],
[FUNCS.ui.cancelAnimationFrame, ({ currentTarget }) => cancelAnimationFrame(parseInt(currentTarget.dataset.animationFrame))],
[FUNCS.ui.clearInterval, ({ currentTarget }) => clearInterval(parseInt(currentTarget.dataset.clearInterval))],
[FUNCS.ui.clearTimeout, ({ currentTarget }) => clearTimeout(parseInt(currentTarget.dataset.timeout))],
[FUNCS.ui.abortController, ({ currentTarget }) => abortController(currentTarget.dataset.aegisEventController, currentTarget.dataset.aegisControllerReason)],
[FUNCS.ui.open, ({ currentTarget }) => document.querySelector(currentTarget.dataset.openSelector).open = true],
[FUNCS.ui.close, ({ currentTarget }) => document.querySelector(currentTarget.dataset.closeSelector).open = false],
[FUNCS.ui.showModal, ({ currentTarget }) => {
const target = $(currentTarget.dataset.showModalSelector);
if (target instanceof HTMLDialogElement) {
target.showModal();
}
}],
[FUNCS.ui.closeModal, ({ currentTarget }) => {
const target = $(currentTarget.dataset.closeModalSelector);
if (target instanceof HTMLDialogElement) {
target.close();
}
}],
[FUNCS.ui.showPopover, ({ currentTarget }) => {
const target = $(currentTarget.dataset.showPopoverSelector);
if (target instanceof HTMLElement) {
target.showPopover();
}
}],
[FUNCS.ui.hidePopover, ({ currentTarget }) => {
const target = $(currentTarget.dataset.hidePopoverSelector);
if (target instanceof HTMLElement) {
target.hidePopover();
}
}],
[FUNCS.ui.togglePopover, ({ currentTarget }) => {
const target = $(currentTarget.dataset.togglePopoverSelector);
if (target instanceof HTMLElement) {
target.togglePopover();
}
}],
[FUNCS.ui.print, () => globalThis.print()],
[FUNCS.ui.prevent, event => event.preventDefault()],
[FUNCS.ui.requestFullscreen, ({ currentTarget}) => {
if (currentTarget.dataset.hasOwnProperty(requestFullscreen.data)) {
document.getElementById(currentTarget.dataset[requestFullscreen.data]).requestFullscreen();
} else {
currentTarget.requestFullscreen();
}
}],
[FUNCS.ui.toggleFullscreen, ({ currentTarget }) => {
const target = currentTarget.dataset.hasOwnProperty(toggleFullsceen.data)
? document.getElementById(currentTarget.dataset[toggleFullsceen.data])
: currentTarget;
if (target.isSameNode(document.fullscreenElement)) {
document.exitFullscreen();
} else {
target.requestFullscreen();
}
}],
[FUNCS.ui.exitFullsceen, () => document.exitFullscreen()],
]);
/**
* Check if a callback is registered
*
* @param {CallbackRegistryKey|string} name The name/key to check for in callback registry
* @returns {boolean} Whether or not a callback is registered
*/
const hasCallback = name => registry.has(name?.toString());
/**
* Get a callback from the registry by name/key
*
* @param {CallbackRegistryKey|string} name The name/key of the callback to get
* @returns {Function|undefined} The corresponding function registered under that name/key
*/
const getCallback = name => registry.get(name?.toString());
// @ts-check
/**
* Internal slot for the callback to call to nnotify of changes
* @type {unique symbol}
*/
const notify = Symbol('signal:watcher:notify');
/**
* @type {unique symbol}
*/
const currentComputed = Symbol('signal:currentComputed');
/**
* Callback called when isWatched becomes true, if it was previously false (`Signal.subtle.watched`)
*
* @type {unique symbol}
*/
const watched = Symbol('Signal:subtle:watched');
/**
* Callback called whenever isWatched becomes false, if it was previously true (`Signal.subtle.unwatched`)
*
* @type {unique symbol}
*/
const unwatched = Symbol('Signal:subtle:unwatched');
/**
* For equality checks in Computed, it must be a unique value
* @type {unique symbol}
*/
const initial = Symbol('signal:initial');
/**
* Internal slot for determining calling `Signal.subtle.watched` and `Signal.subtle.unwatched` callbacks
* @type {unique symbol}
*/
const isWatched = Symbol('Signal:isWatched');
/**
* Internal slot for State|Computed to store the `Signal.subtle.watched` callback
* @type {unique symbol}
*/
const onWatch = Symbol('Signal:onWatch');
/**
* Internal slot for State|Computed to store the `Signal.subtle.unwatched` callback
* @type {unique symbol}
*/
const onUnwatch = Symbol('Signal:onUnwatch');
/**
* @type {unique symbol}
*/
const sources = Symbol('Signal:sources');
/**
* @type {unique symbol}
*/
const sinks = Symbol('Signal:sinks');
/**
* @typedef {(t: any, t2: any) => boolean} EqualityCheck
*/
/**
* Custom comparison function between old and new value. Default: Object.is.
* The signal is passed in as the this value for context.
*
* @type {EqualityCheck}
*/
const equals = Object.is;
/**
* @typedef {{
* equals?: EqualityCheck,
* [watched]?: (this: AnySignal<any>) => void,
* [unwatched]?: (this: AnySignal<any>) => void,
* }} SignalOptions
*/
/**
* @type {SignalOptions}
*/
const opts = Object.freeze({ equals });
/**
* @template T
* @typedef {State<T> | Computed<T>} AnySignal<T>
*/
/**
* A read-write Signal
* @template T
*/
class State {
/**
* @type {T}
*/
#value;
/**
* @type {EqualityCheck}
*/
#equals;
/**
* @type {VoidFunction|null}
*/
[unwatched] = null;
/**
* @type {VoidFunction|null}
*/
[onWatch] = null;
/**
* @type {VoidFunction|null}
*/
[onUnwatch] = null;
/**
* @type {boolean}
*/
[isWatched] = false;
/**
* @type {Set<Computed<any>|Watcher>}
*/
[sinks] = new Set();
/**
* @type {Set<Set<any>|Computed<any>>}
*/
[sources] = new Set();
/**
* Create a state Signal starting with the value T
* @param {T} value - The initial value.
* @param {SignalOptions} options
*/
constructor(value, options = opts) {
if (typeof options !== 'object') {
throw new TypeError('Invalid options.');
} else {
this.#equals = options.equals ?? equals;
this.#value = value;
if (typeof options?.[watched] === 'function') {
this[onWatch] = options[watched].bind(this);
}
if (typeof options?.[unwatched] === 'function') {
this[onUnwatch] = options[unwatched].bind(this);
}
}
}
/**
* Get the value of the signal
*
* @returns {T}
*/
get() {
const currentComputed = Signal.subtle.currentComputed();
if (currentComputed instanceof Computed && ! currentComputed[sources].has(this)) {
currentComputed[sources].add(this);
this[sinks].add(currentComputed);
}
return this.#value;
}
/**
* Set the state Signal value to T
*
* @param {T} newValue
*/
set(newValue) {
if (! this.#equals(this.#value, newValue)) {
this.#value = newValue;
for (const sink of Signal.subtle.introspectSinks(this)) {
sink[notify](this);
}
}
}
}
/**
* A Signal which is a formula based on other Signals
*
* @template T
*/
class Computed {
/**
* @type {EqualityCheck}
*/
#equals;
/**
* @type {() => T}
*/
#computation;
/**
* @type {boolean}
*/
#dirty = true;
/**
* @type {T|initial}
*/
#value = initial;
/**
* @type {VoidFunction|null}
*/
[watched] = null;
/**
* @type {VoidFunction|null}
*/
[unwatched] = null;
/**
* @type {VoidFunction|null}
*/
[onWatch] = null;
/**
* @type {VoidFunction|null}
*/
[onUnwatch] = null;
/**
* @type {boolean}
*/
[isWatched] = false;
/**
* @type {Set<Computed<any>|Watcher>}
*/
[sinks] = new Set();
/**
* @type {Set<State<any>|Computed<any>>}
*/
[sources] = new Set();
/**
* Create a Signal which evaluates to the value returned by the callback.
* Callback is called with this signal as the this value.
*
* @param {() => T} computation - The function to calculate the value.
* @param {SignalOptions} [options]
*/
constructor(computation, options = opts) {
if (typeof computation !== 'function') {
throw new TypeError('Computation must be a function.');
} else if (typeof options !== 'object') {
throw new TypeError('Invalid options.');
} else {
this.#equals = options.equals ?? equals;
this.#computation = computation;
if (typeof options[watched] === 'function') {
this[onWatch] = options[watched].bind(this);
}
if (typeof options[unwatched] === 'function') {
this[onUnwatch] = options[unwatched].bind(this);
}
}
}
/**
* Get the value of the signal
*
* @this {Computed<T>}
* @returns {T|initial}
*/
get() {
const oldComputed = Signal.subtle.currentComputed();
try {
if (oldComputed !== this && oldComputed !== null) {
oldComputed[sources].add(this);
this[sinks].add(oldComputed);
}
Signal[currentComputed] = this;
if (this.#dirty) {
// Must clear prior dependencies BEFORE computation
for (const source of Signal.subtle.introspectSources(this)) {
source[sinks].delete(this);
}
this[sources].clear();
const val = this.#computation();
if (! this.#equals(val, this.#value)) {
this.#value = val;
for (const sink of Signal.subtle.introspectSinks(this)) {
sink[notify](this);
}
}
this.#dirty = false;
return val;
} else {
return this.#value;
}
} finally {
// Restore previous context
Signal[currentComputed] = oldComputed;
}
}
/**
* Notifiies a `Signal.Computed` when a source has changed
*
* @param {State<any>|Computed<any>} source
*/
[notify](source) {
this.#dirty = true;
this[sources].add(source);
source[sinks].add(this);
for (const sink of Signal.subtle.introspectSinks(this)) {
sink[notify](this);
}
}
}
/**
* Watches for changes to specific signals.
* @memberof Signal.subtle
*/
class Watcher {
/**
* @type {boolean}
*/
#isWatched = false;
/**
* @type {Set<AnySignal<any>>}
*/
#pending = new Set();
/**
* @type {(this: Watcher) => void}
*/
#notify;
/**
* @type {Set<AnySignal<any>>}
*/
[sources] = new Set();
/**
* When a (recursive) source of Watcher is written to, call this callback,
* if it hasn't already been called since the last `watch` call.
* No signals may be read or written during the notify.
*
* @param {(this: Watcher) => void} notify - Called synchronously when a watched signal becomes dirty.
*/
constructor(notify) {
if (typeof notify !== 'function') {
throw new TypeError(`Notify must be a function but got a ${typeof notify}.`);
} else {
this.#notify = notify;
}
}
/**
* Add these signals to the Watcher's set, and set the watcher to run its
* notify callback next time any signal in the set (or one of its dependencies) changes.
* Can be called with no arguments just to reset the "notified" state, so that
* the notify callback will be invoked again.
*
* @param {...AnySignal<any>} signals
*/
watch(...signals) {
this.#isWatched = true;
for (const signal of signals) {
if (! (signal instanceof State || signal instanceof Computed)) {
throw new TypeError('Signal must be an instance of `Signal.State` or `Signal.Computed`.');
} else if (! this[sources].has(signal)) {
if (typeof signal[onWatch] === 'function' && ! signal[isWatched]) {
signal[isWatched] = true;
signal[onWatch].call(signal);
}
this[sources].add(signal);
signal[sinks].add(this);
}
}
}
/**
* Remove these signals from the watched set (e.g., for an effect which is disposed)
* @template T
* @param {...AnySignal<T>} signals
*/
unwatch(...signals) {
for (const signal of signals) {
if (! (signal instanceof State || signal instanceof Computed)) {
throw new TypeError('Signal must be an instance of `Signal.State` or `Signal.Computed`.');
} else {
if (typeof signal[onUnwatch] === 'function' && signal[isWatched]) {
const watchers = Signal.subtle.introspectSinks(signal).filter(sink => sink instanceof Signal.subtle.Watcher);
if (watchers.length === 1) {
signal[isWatched] = false;
signal[onUnwatch].call(signal);
}
}
signal[sinks].delete(this);
this[sources].delete(signal);
}
}
}
/**
* Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
* with a source which is dirty or pending and hasn't yet been re-evaluated
*
* @returns {Array<AnySignal<any>>}
*/
getPending() {
const pending = Array.from(this.#pending);
this.#pending.clear();
return pending;
}
/**
* Notify a `Signal.subtle.Watcher` when a source has changed
*
* @template T
* @param {AnySignal<T>} signal
*/
[notify](signal) {
this.#pending.add(signal);
if (this.#isWatched) {
this.#notify.call(this);
}
}
}
/**
* This namespace includes "advanced" features that are better to
* leave for framework authors rather than application developers.
* Analogous to `crypto.subtle`
*
* @namespace subtle
*/
const subtle = {
Watcher,
/**
* Hook to observe being watched
* @type {unique symbol}
*/
watched,
/**
* Hook to observe no longer watched
* @type {unique s}
*/
unwatched,
/**
* Run a callback with all tracking disabled
*
* @template T
* @param {() => T} cb
* @returns {T}
*/
untrack(cb) {
if (typeof cb !== 'function') {
throw new TypeError('Callback must be a function.');
} else {
const prev = Signal.subtle.currentComputed();
Signal[currentComputed] = null;
try {
return cb();
} finally {
Signal[currentComputed] = prev;
}
}
},
/**
* Get the current computed signal which is tracking any signal reads, if any
*
* @returns {Computed<any>|null}
*/
currentComputed() {
return Signal[currentComputed];
},
/**
* Returns ordered list of all signals which this one referenced
* during the last time it was evaluated.
* For a Watcher, lists the set of signals which it is watching.
*
* @param {Computed<any>|Watcher} s
* @returns {(State<any>|Computed<any>)[]}
*/
introspectSources(s) {
if (! (s instanceof Computed || s instanceof Watcher)) {
throw new TypeError('Expected a `Signal.Watcher` or `Signal.Computed`.');
} else {
return Array.from(s[sources]);
}
},
/**
* Returns the Watchers that this signal is contained in, plus any
* Computed signals which read this signal last time they were evaluated,
* if that computed signal is (recursively) watched.
*
* @param {State<any>|Computed<any>} s
* @returns {(Computed<any>|Watcher)[]}
*/
introspectSinks(s) {
if (! (s instanceof State || s instanceof Computed)) {
throw new TypeError('Expected a `Signal.State` or `Signal.Computed`.');
} else {
return Array.from(s[sinks]);
}
},
/**
* True if this signal is "live", in that it is watched by a Watcher,
* or it is read by a Computed signal which is (recursively) live.
*
* @param {State<any>|Computed<any>} s
* @return {boolean}
*/
hasSinks(s) {
if (! (s instanceof State || s instanceof Computed)) {
throw new TypeError('Expected a `Signal.State` or `Signal.Computed`.');
} else {
return s[sinks].size !== 0;
}
},
/**
* True if this element is "reactive", in that it depends
* on some other signal. A Computed where hasSources is false
* will always return the same constant.
*
* @param {Computed<any>|Watcher} s
* @returns {boolean}
*/
hasSources(s) {
if (! (s instanceof Computed || s instanceof Watcher)) {
throw new TypeError('Expected a `Signal.Watcher` or `Signal.Computed`.');
} else {
return s[sources].size !== 0;
}
},
};
/**
* The core Signals namespace.
* @namespace Signal
*/
const Signal = {
State,
Computed,
subtle,
/**
* @type {Computed<any>|null}
*/
[currentComputed]: null,
};
const PREFIX = 'data-aegis-event-';
const EVENT_PREFIX = PREFIX + 'on-';
const EVENT_PREFIX_LENGTH = EVENT_PREFIX.length;
const DATA_PREFIX = 'aegisEventOn';
const DATA_PREFIX_LENGTH = DATA_PREFIX.length;
const signalRegistry = new Map();
const controllerRegistry = new Map();
const once = PREFIX + 'once';
const passive = PREFIX + 'passive';
const capture = PREFIX + 'capture';
const signal = PREFIX + 'signal';
const onAbort = EVENT_PREFIX + 'abort';
const onBlur = EVENT_PREFIX + 'blur';
const onFocus = EVENT_PREFIX + 'focus';
const onCancel = EVENT_PREFIX + 'cancel';
const onAuxclick = EVENT_PREFIX + 'auxclick';
const onBeforeinput = EVENT_PREFIX + 'beforeinput';
const onBeforetoggle = EVENT_PREFIX + 'beforetoggle';
const onCanplay = EVENT_PREFIX + 'canplay';
const onCanplaythrough = EVENT_PREFIX + 'canplaythrough';
const onChange = EVENT_PREFIX + 'change';
const onClick = EVENT_PREFIX + 'click';
const onClose = EVENT_PREFIX + 'close';
const onCommand = EVENT_PREFIX + 'command';
const onContextmenu = EVENT_PREFIX + 'contextmenu';
const onCopy = EVENT_PREFIX + 'copy';
const onCuechange = EVENT_PREFIX + 'cuechange';
const onCut = EVENT_PREFIX + 'cut';
const onDblclick = EVENT_PREFIX + 'dblclick';
const onDrag = EVENT_PREFIX + 'drag';
const onDragend = EVENT_PREFIX + 'dragend';
const onDragenter = EVENT_PREFIX + 'dragenter';
const onDragexit = EVENT_PREFIX + 'dragexit';
const onDragleave = EVENT_PREFIX + 'dragleave';
const onDragover = EVENT_PREFIX + 'dragover';
const onDragstart = EVENT_PREFIX + 'dragstart';
const onDrop = EVENT_PREFIX + 'drop';
const onDurationchange = EVENT_PREFIX + 'durationchange';
const onEmptied = EVENT_PREFIX + 'emptied';
const onEnded = EVENT_PREFIX + 'ended';
const onFormdata = EVENT_PREFIX + 'formdata';
const onInput = EVENT_PREFIX + 'input';
const onInvalid = EVENT_PREFIX + 'invalid';
const onKeydown = EVENT_PREFIX + 'keydown';
const onKeypress = EVENT_PREFIX + 'keypress';
const onKeyup = EVENT_PREFIX + 'keyup';
const onLoad = EVENT_PREFIX + 'load';
const onLoadeddata = EVENT_PREFIX + 'loadeddata';
const onLoadedmetadata = EVENT_PREFIX + 'loadedmetadata';
const onLoadstart = EVENT_PREFIX + 'loadstart';
const onMousedown = EVENT_PREFIX + 'mousedown';
const onMouseenter = EVENT_PREFIX + 'mouseenter';
const onMouseleave = EVENT_PREFIX + 'mouseleave';
const onMousemove = EVENT_PREFIX + 'mousemove';
const onMouseout = EVENT_PREFIX + 'mouseout';
const onMouseover = EVENT_PREFIX + 'mouseover';
const onMouseup = EVENT_PREFIX + 'mouseup';
const onWheel = EVENT_PREFIX + 'wheel';
const onPaste = EVENT_PREFIX + 'paste';
const onPause = EVENT_PREFIX + 'pause';
const onPlay = EVENT_PREFIX + 'play';
const onPlaying = EVENT_PREFIX + 'playing';
const onProgress = EVENT_PREFIX + 'progress';
const onRatechange = EVENT_PREFIX + 'ratechange';
const onReset = EVENT_PREFIX + 'reset';
const onResize = EVENT_PREFIX + 'resize';
const onScroll = EVENT_PREFIX + 'scroll';
const onScrollend = EVENT_PREFIX + 'scrollend';
const onSecuritypolicyviolation = EVENT_PREFIX + 'securitypolicyviolation';
const onSeeked = EVENT_PREFIX + 'seeked';
const onSeeking = EVENT_PREFIX + 'seeking';
const onSelect = EVENT_PREFIX + 'select';
const onSlotchange = EVENT_PREFIX + 'slotchange';
const onStalled = EVENT_PREFIX + 'stalled';
const onSubmit = EVENT_PREFIX + 'submit';
const onSuspend = EVENT_PREFIX + 'suspend';
const onTimeupdate = EVENT_PREFIX + 'timeupdate';
const onVolumechange = EVENT_PREFIX + 'volumechange';
const onWaiting = EVENT_PREFIX + 'waiting';
const onSelectstart = EVENT_PREFIX + 'selectstart';
const onSelectionchange = EVENT_PREFIX + 'selectionchange';
const onToggle = EVENT_PREFIX + 'toggle';
const onPointercancel = EVENT_PREFIX + 'pointercancel';
const onPointerdown = EVENT_PREFIX + 'pointerdown';
const onPointerup = EVENT_PREFIX + 'pointerup';
const onPointermove = EVENT_PREFIX + 'pointermove';
const onPointerout = EVENT_PREFIX + 'pointerout';
const onPointerover = EVENT_PREFIX + 'pointerover';
const onPointerenter = EVENT_PREFIX + 'pointerenter';
const onPointerleave = EVENT_PREFIX + 'pointerleave';
const onGotpointercapture = EVENT_PREFIX + 'gotpointercapture';
const onLostpointercapture = EVENT_PREFIX + 'lostpointercapture';
const onMozfullscreenchange = EVENT_PREFIX + 'mozfullscreenchange';
const onMozfullscreenerror = EVENT_PREFIX + 'mozfullscreenerror';
const onAnimationcancel = EVENT_PREFIX + 'animationcancel';
const onAnimationend = EVENT_PREFIX + 'animationend';
const onAnimationiteration = EVENT_PREFIX + 'animationiteration';
const onAnimationstart = EVENT_PREFIX + 'animationstart';
const onTransitioncancel = EVENT_PREFIX + 'transitioncancel';
const onTransitionend = EVENT_PREFIX + 'transitionend';
const onTransitionrun = EVENT_PREFIX + 'transitionrun';
const onTransitionstart = EVENT_PREFIX + 'transitionstart';
const onWebkitanimationend = EVENT_PREFIX + 'webkitanimationend';
const onWebkitanimationiteration = EVENT_PREFIX + 'webkitanimationiteration';
const onWebkitanimationstart = EVENT_PREFIX + 'webkitanimationstart';
const onWebkittransitionend = EVENT_PREFIX + 'webkittransitionend';
const onError = EVENT_PREFIX + 'error';
const eventAttrs = new Signal.State([
onAbort,
onBlur,
onFocus,
onCancel,
onAuxclick,
onBeforeinput,
onBeforetoggle,
onCanplay,
onCanplaythrough,
onChange,
onClick,
onClose,
onCommand,
onContextmenu,
onCopy,
onCuechange,
onCut,
onDblclick,
onDrag,
onDragend,
onDragenter,
onDragexit,
onDragleave,
onDragover,
onDragstart,
onDrop,
onDurationchange,
onEmptied,
onEnded,
onFormdata,
onInput,
onInvalid,
onKeydown,
onKeypress,
onKeyup,
onLoad,
onLoadeddata,
onLoadedmetadata,
onLoadstart,
onMousedown,
onMouseenter,
onMouseleave,
onMousemove,
onMouseout,
onMouseover,
onMouseup,
onWheel,
onPaste,
onPause,
onPlay,
onPlaying,
onProgress,
onRatechange,
onReset,
onResize,
onScroll,
onScrollend,
onSecuritypolicyviolation,
onSeeked,
onSeeking,
onSelect,
onSlotchange,
onStalled,
onSubmit,
onSuspend,
onTimeupdate,
onVolumechange,
onWaiting,
onSelectstart,
onSelectionchange,
onToggle,
onPointercancel,
onPointerdown,
onPointerup,
onPointermove,
onPointerout,
onPointerover,
onPointerenter,
onPointerleave,
onGotpointercapture,
onLostpointercapture,
onMozfullscreenchange,
onMozfullscreenerror,
onAnimationcancel,
onAnimationend,
onAnimationiteration,
onAnimationstart,
onTransitioncancel,
onTransitionend,
onTransitionrun,
onTransitionstart,
onWebkitanimationend,
onWebkitanimationiteration,
onWebkitanimationstart,
onWebkittransitionend,
onError,
]);
const selector = new Signal.Computed(() => eventAttrs.get().map(attr => `[${CSS.escape(attr)}]`).join(','));
const isEventDataAttr = ([name]) => name.startsWith(DATA_PREFIX);
function _addListeners(el, { signal, attrFilter = EVENTS } = {}) {
const dataset = el.dataset;
for (const [attr, val] of Object.entries(dataset).filter(isEventDataAttr)) {
try {
const event = 'on' + attr.substring(DATA_PREFIX_LENGTH);
if (attrFilter.hasOwnProperty(event) && hasCallback(val)) {
el.addEventListener(event.substring(2).toLowerCase(), getCallback(val), {
passive: dataset.hasOwnProperty('aegisEventPassive'),
capture: dataset.hasOwnProperty('aegisEventCapture'),
once: dataset.hasOwnProperty('aegisEventOnce'),
signal: dataset.hasOwnProperty('aegisEventSignal') ? getSignal(dataset.aegisEventSignal) : signal,
});
}
} catch(err) {
reportError(err);
}
}
}
new MutationObserver(records => {
records.forEach(record => {
switch(record.type) {
case 'childList':
[...record.addedNodes]
.filter(node => node.nodeType === Node.ELEMENT_NODE)
.forEach(node => attachListeners(node));
break;
case 'attributes':
if (typeof record.oldValue === 'string' && hasCallback(record.oldValue)) {
record.target.removeEventListener(
record.attributeName.substring(EVENT_PREFIX_LENGTH),
getCallback(record.oldValue), {
once: record.target.hasAttribute(once),
capture: record.target.hasAttribute(capture),
passive: record.target.hasAttribute(passive),
}
);
}
if (
record.target.hasAttribute(record.attributeName)
&& hasCallback(record.target.getAttribute(record.attributeName))
) {
record.target.addEventListener(
record.attributeName.substring(EVENT_PREFIX_LENGTH),
getCallback(record.target.getAttribute(record.attributeName)), {
once: record.target.hasAttribute(once),
capture: record.target.hasAttribute(capture),
passive: record.target.hasAttribute(passive),
signal: record.target.hasAttribute(signal) ? getSignal(record.target.getAttribute(signal)) : undefined,
}
);
}
break;
}
});
});
const EVENTS = {
onAbort,
onBlur,
onFocus,
onCancel,
onAuxclick,
onBeforeinput,
onBeforetoggle,
onCanplay,
onCanplaythrough,
onChange,
onClick,
onClose,
onCommand,
onContextmenu,
onCopy,
onCuechange,
onCut,
onDblclick,
onDrag,
onDragend,
onDragenter,
onDragexit,
onDragleave,
onDragover,
onDragstart,
onDrop,
onDurationchange,
onEmptied,
onEnded,
onFormdata,
onInput,
onInvalid,
onKeydown,
onKeypress,
onKeyup,
onLoad,
onLoadeddata,
onLoadedmetadata,
onLoadstart,
onMousedown,
onMouseenter,
onMouseleave,
onMousemove,
onMouseout,
onMouseover,
onMouseup,
onWheel,
onPaste,
onPause,
onPlay,
onPlaying,
onProgress,
onRatechange,
onReset,
onResize,
onScroll,
onScrollend,
onSecuritypolicyviolation,
onSeeked,
onSeeking,
onSelect,
onSlotchange,
onStalled,
onSubmit,
onSuspend,
onTimeupdate,
onVolumechange,
onWaiting,
onSelectstart,
onSelectionchange,
onToggle,
onPointercancel,
onPointerdown,
onPointerup,
onPointermove,
onPointerout,
onPointerover,
onPointerenter,
onPointerleave,
onGotpointercapture,
onLostpointercapture,
onMozfullscreenchange,
onMozfullscreenerror,
onAnimationcancel,
onAnimationend,
onAnimationiteration,
onAnimationstart,
onTransitioncancel,
onTransitionend,
onTransitionrun,
onTransitionstart,
onWebkitanimationend,
onWebkitanimationiteration,
onWebkitanimationstart,
onWebkittransitionend,
onError,
once,
passive,
capture,
};
/**
* Get a registetd controller from the registry
*
* @param {string} key Generated key with which the controller was registered
* @returns {AbortController|void} Any registered controller, if any
*/
const getController = key => controllerRegistry.get(key);
function abortController(key, reason) {
const controller = getController(key);
if (! (controller instanceof AbortController)) {
return false;
} else if (typeof reason === 'string') {
controller.abort(new Error(reason));
return true;
} else {
controller.abort(reason);
return true;
}
}
/**
* Gets and `AbortSignal` from the registry
*
* @param {string} key The registered key for the signal
* @returns {AbortSignal|void} The corresponding `AbortSignal`, if any
*/
const getSignal = key => signalRegistry.get(key);
/**
* Add listeners to an element and its children, matching a generated query based on registered attributes
*
* @param {Element|Document} target Root node to add listeners from
* @param {object} options
* @param {AbortSignal} [options.signal] Optional signal to remove event listeners
* @returns {Element|Document} Returns the passed target node
*/
function attachListeners(target, { signal } = {}) {
const nodes = target instanceof Element && target.matches(selector.get())
? [target, ...target.querySelectorAll(selector.get())]
: target.querySelectorAll(selector.get());
nodes.forEach(el => _addListeners(el, { signal }));
return target;
}
const isModule = ! (document.currentScript instanceof HTMLScriptElement);
const SUPPORTS_IMPORTMAP = HTMLScriptElement.supports('importmap');
const ROUTES_REGISTRY = new Map();
const NO_BODY_METHODS = ['GET', 'HEAD', 'DELETE', 'OPTIONS'];
const DESC_SELECTOR = 'meta[name="description"], meta[itemprop="description"], meta[property="og:description"], meta[name="twitter:description"]';
const navObserver = new MutationObserver(entries => entries.forEach(entry => interceptNav(entry.target)));
const preloadObserver = new MutationObserver(entries => entries.forEach(_handlePreloadMutations));
const ROOT_ID = 'root';
const EVENT_TARGET = document;
const NAV_CLOSE_SYMBOL = Symbol.for('aegis:navigate:event:close');
const prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
let rootEl = document.getElementById(ROOT_ID) ?? document.body;
let rootSelector = '#' + ROOT_ID;
const SUPPORTS_TRUSTED_TYPES = 'trustedTypes' in globalThis;
const _isTrustedHTML = input => SUPPORTS_TRUSTED_TYPES && trustedTypes.isHTML(input);
const startViewTransition = typeof document.startViewTransition === 'function'
? (update, types) => document.startViewTransition({ update, types })
: update => Promise.try(update);
function _handlePreloadMutations(target) {
if (target instanceof MutationRecord) {
_handlePreloadMutations(target.target);
} else if (target.tagName === 'A' && ! target.classList.contains('no-router') && ! target.hasAttribute(onClick)) {
preloadOnHover(target, target.dataset);
} else {
target.querySelectorAll(`a:not(.no-router, [${onClick}])`).forEach(a => preloadOnHover(a, a.dataset));
}
}
const NAV_EVENT = 'aegis:navigate';
const EVENT_TYPES = {
navigate: 'aegis:router:navigate',
back: 'aegis:router:back',
forward: 'aegis:router:forward',
reload: 'aegis:router:reload',
pop: 'aegis:router:pop',
go: 'aegis:router:go',
load: 'aegis:router:load',
submit: 'aegis:router:submit',
};
const DEFAULT_REASONS = [EVENT_TYPES.back, EVENT_TYPES.forward, EVENT_TYPES.navigate, EVENT_TYPES.submit, EVENT_TYPES.reload, EVENT_TYPES.go];
class AegisNavigationEvent extends CustomEvent {
#reason;
#url;
#stack = new AsyncDisposableStack();
#controller = new AbortController();
#promises = [];
#errors = [];
constructor(name = NAV_EVENT, reason = 'unknown', { bubbles = false, cancelable = true, detail = {
oldState: state.getStateObj(),
oldURL: new URL(location.href),
} } = {}) {
super(name, { bubbles, cancelable, detail });
this.#reason = reason;
this.#url = location.href;
}
get aborted() {
return this.#controller.signal.aborted;
}
get disposed() {
return this.#stack.disposed;
}
get error() {
switch(this.#errors.length) {
case 0:
return null;
case 1:
return this.#errors[0];
default:
return new AggregateError(this.#errors);
}
}
get reason() {
return this.#reason;
}
get signal() {
return this.#controller.signal;
}
get stack() {
return this.#stack;
}
get url() {
return this.#url;
}
async [NAV_CLOSE_SYMBOL]() {
const result = await Promise.allSettled(this.#promises).then(results => {
this.#errors.push(...results.filter(result => result.status === 'rejected').map(result => result.reason));
return this.cancelable && this.defaultPrevented;
});
this.#controller.abort();
return result;
}
adopt(obj, callback) {
return this.#stack.adopt(obj, callback);
}
abort(reason) {
this.#controller.abort(reason);
}
defer(callback) {
this.#stack.defer(callback);
}
async disposeAsync() {
await this[Symbol.asyncDispose]();
}
use(obj) {
return this.#stack.use(obj);
}
waitUntil(promiseOrCallback, { signal } = {}) {
const { promise, resolve, reject } = Promise.withResolvers();
this.#promises.push(promise);
if (signal instanceof AbortSignal && ! signal.aborted) {
signal.addEventListener('abort', ({ target }) =>{
reject(target.reason);
if (this.cancelable && ! this.defaultPrevented) {
super.preventDefault();
}
}, {
once: true,
signal: this.#controller.signal,
});
}
if (this.#controller.signal.aborted) {
reject(this.#controller.signal.reason);
} else if (signal instanceof AbortSignal && signal.aborted) {
reject(signal.reason);
if (this.cancelable && ! this.defaultPrevented) {
super.preventDefault();
}
} else if (! this.defaultPrevented && promiseOrCallback instanceof Function) {
Promise.try(() => promiseOrCallback(this, {
signal: signal instanceof AbortSignal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal,
timestamp: performance.now(),
stack: this.#stack,
})).then(resolve, reject);
} else if (! this.defaultPrevented && promiseOrCallback instanceof Promise) {
promiseOrCallback.then(resolve, reject);
}
}
[Symbol.toStringTag]() {
return 'NavigationEvent';
}
async [Symbol.asyncDispose]() {
if (! this.#controller.signal.aborted) {
this.#controller.abort(new DOMException('The stack of the event was disposed.', 'AbortError'));
}
if (! this.#stack.disposed) {
await this.#stack.disposeAsync();
}
}
static get defaultType() {
return NAV_EVENT;
}
static get reasons() {
return EVENT_TYPES;
}
}
// Need this to be "unsafe" to not be restrictive on what modifications can be made to a page
const policy = SUPPORTS_TRUSTED_TYPES
? trustedTypes.createPolicy('aegis-router#html', { createHTML: input => input })
: Object.freeze({ createPolicy: input => input });
async function _popstateHandler(event) {
const diff = state.diffState(event.state ?? {});
const navigate = new AegisNavigationEvent(NAV_EVENT, EVENT_TYPES.pop, {
detail: { newState: event.state, oldState: null, oldURL: new URL(location.href), method: 'GET', formData: null },
});
try {
EVENT_TARGET.dispatchEvent(navigate);
if (! await navigate[NAV_CLOSE_SYMBOL]()) {
const old = history.scrollRestoration;
const [content] = await Promise.all([
getModule(new URL(location.href)),
state.notifyStateChange(diff),
]);
history.scrollRestoration = 'auto';
_updatePage(content);
history.scrollRestoration = old;
}
} finally {
requestAnimationFrame(navigate[Symbol.asyncDispose].bind(navigate));
}
}
function _addStyle(sheet) {
if (sheet instanceof CSSStyleSheet && ! document.adoptedStyleSheets.includes(sheet)) {
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
} else if (Array.isArray(sheet) && sheet.length !== 0) {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
...sheet.filter(s => s instanceof CSSStyleSheet && ! document.adoptedStyleSheets.includes(s))
];
}
}
function _createMeta(props = {}) {
const meta = document.createElement('meta');
Object.entries(props).forEach(([key, val]) => meta.setAttribute(key, val));
return meta;
}
function _loadLink(href, {
relList = [],
crossOrigin = 'anonymous',
referrerPolicy = 'no-referrer',
fetchPriority = 'auto',
signal: passedSignal,
as,
integrity,
media,
type,
} = {}) {
const { promise, resolve, reject } = Promise.withResolvers();
const link = document.createElement('link');
if (passedSignal instanceof AbortSignal && passedSignal.aborted) {
reject(passedSignal.reason);
} else {
link.relList.add(...relList);
if (typeof fetchPriority === 'string') {
link.fetchPriority = fetchPriority;
}
if (typeof crossOrigin === 'string') {
link.crossOrigin = crossOrigin;
}
if (typeof type === 'string') {
link.type = type;
}
if (typeof media === 'string') {
link.media = media;
} else if (media instanceof MediaQueryList) {
link.media = media.media;
}
if (typeof as === 'string') {
link.as = as;
}
if (typeof integrity === 'string') {
link.integrity = integrity;
}
if (link.relList.contains('preload') || link.relList.contains('modulepreload')) {
const controller = new AbortController();
const signal = passedSignal instanceof AbortSignal ? AbortSignal.any([controller.signal, passedSignal]) : controller.signal;
if (passedSignal instanceof AbortSignal) {
passedSignal.addEventListener('abort', ({ target }) => {
reject(target.reason);
}, { signal: controller.signal, once: true });
}
link.referrerPolicy = referrerPolicy;
link.addEventListener('load', () => {
resolve();
controller.abort();
}, { signal });
link.addEventListener('error', () => {
reject(new DOMException(`Error loading ${href}`, 'NotFoundError'));
controller.abort();
}, { signal });
link.href = _resolveModule(href);
document.head.append(link);
return promise.then(() => link.remove()).catch(err => {
if (link.isConnected) {
link.remove();
}
reportError(err);
});
} else {
link.href = href;
document.head.append(link);
resolve();
return promise;
}
}
}
function _isModuleURL(src) {
switch(src[0]) {
case '/':
case '.':
return true;
case 'h':
return src.substring(0, '4') === 'http' && URL.canParse(src);
default:
return false;
}
}
function _resolveModule(src) {
if (_isModuleURL(src)) {
return URL.parse(src, document.baseURI);
} else if (! SUPPORTS_IMPORTMAP) {
throw new TypeError('Importmaps and module specifiers are not supported');
} else if (! isModule) {
throw new TypeError('Cannot resolve a module specifier outside of a module script.');
} else {
return undefined(src);
}
}
function _getLinkStateData(a) {
const entries = Object.entries(a.dataset)
.filter(([name]) => name.startsWith('aegisState'))
.map(([name, value]) => [name[10].toLowerCase() + name.substring(11), value]);
return Object.fromEntries(entries);
}
function _interceptLinkClick(event) {
if (event.target.classList.contains('no-router') || event.target.hasAttribute(onClick)) {
event.target.removeEventListener(_interceptLinkClick);
} else if (
event.isTrusted
&& event.currentTarget.href.startsWith(location.origin)
&& ! (event.metaKey || event.ctrlKey || event.shiftKey)
) {
event.preventDefault();
const state = _getLinkStateData(event.currentTarget);
navigate(event.currentTarget.href, state, {
integrity: event.currentTarget.dataset.integrity,
cache: event.currentTarget.dataset.cache,
referrerPolicy: event.currentTarget.dataset.referrerPolicy,
});
}
}
async function _interceptFormSubmit(event) {
if (event.target.classList.contains('no-router') || event.target.hasAttribute(onSubmit)) {
event.target.removeEventListener('submit', _interceptFormSubmit);
} else if (event.isTrusted && event.target.action.startsWith(location.origin)) {
event.preventDefault();
const { target, submitter } = event;
const { method, action } = target;
const formData = new FormData(target);
const submit = new AegisNavigationEvent(NAV_EVENT, EVENT_TYPES.submit, {
detail: { oldState: state.getStateObj(), oldURL: new URL(location.href), formData },
});
try {
if (submitter instanceof HTMLButtonElement) {
submitter.disabled = true;
}
EVENT_TARGET.dispatchEvent(submit);
if (await submit[NAV_CLOSE_SYMBOL]()) {
return;
} else if (NO_BODY_METHODS.includes(method.toUpperCase())) {
const url = new URL(action);
const params = new URLSearchParams(formData);
for (const [key, val] of params.entries()) {
url.searchParams.append(key, val);
}
await navigate(url, state.getStateObj(), { method });
} else {
await navigate(action, state.getStateObj(), { method, formData });
}
} finally {
if (submitter instanceof HTMLButtonElement) {
submitter.disabled = false;
}
requestAnimationFrame(submit[Symbol.asyncDispose].bind(submit));
}
}
}
async function _getHTML(url, { signal, method = 'GET', body, integrity, cache = 'default', referrerPolicy = 'no-referrer' } = {}) {
const resp = await fetch(url, {
method,
body: NO_BODY_METHODS.includes(method.toUpperCase()) ? null : body,
headers: { 'Accept': 'text/html' },
cache,
referrerPolicy,
integrity,
signal,
}).catch(err => err);
if (resp.ok) {
const html = await resp.text();
return Document.parseHTMLUnsafe(policy.createHTML(html));
} else if (resp instanceof Error) {
return resp;
} else {
return _get404(url, method, { signal });
}
}
async function _updatePage(content) {
const timestamp = performance.now();
await startViewTransition(() => {
if (content instanceof Document) {
if (content.head.childElementCount !== 0) {
setTitle(content.title);
setDescription(content.querySelector(DESC_SELECTOR)?.content);
}
const contentEl = typeof rootSelector === 'string' ? content.body.querySelector(rootSelector) ?? content.body : content.body;
rootEl.replaceChildren(...contentEl.childNodes);
} else if (content instanceof HTMLTemplateElement) {
rootEl.replaceChildren(content.content);
} else if (content instanceof Function && content.prototype instanceof HTMLElement) {
rootEl.replaceChildren(new content({ state: state.getStateObj(), url: new URL(location.href), timestamp }));
} else if (content instanceof Node) {
rootEl.replaceChildren(content);
} else if (content instanceof Function) {
_updatePage(content());
} else if (typeof content === 'string') {
rootEl.setHTMLUnsafe(policy.createHTML(content));
} else if (_isTrustedHTML(content)) {
rootEl.setHTMLUnsafe(content);
} else if (content instanceof Error) {
reportError(content);
rootEl.textContent = content.message;
} else if (content instanceof URL) {
navigate(content);
} else if (! (content === null || typeof content === 'undefined')) {
rootEl.textContent = content;
}
});
const ev = new AegisNavigationEvent(NAV_EVENT, EVENT_TYPES.load, { cancelable: false });
Promise.try(() => EVENT_TARGET.dispatchEvent(ev)).finally(ev[Symbol.asyncDispose].bind(ev));
if (history.scrollRestoration === 'manual') {
if (location.hash.length > 1) {
const target = document.getElementById(location.hash.substring(1)) ?? document.body;
target.scrollIntoView({ behavior: prefersReducedMotion.matches ? 'instant' : 'smooth' });
} else {
const autofocus = rootEl.querySelector('[autofocus]');
if (autofocus instanceof Element) {
autofocus.focus();
} else {
document.body.scrollIntoView({ behavior: prefersReducedMotion.matches ? 'instant' : 'smooth' });
}
}
}
}
async function _handleMetadata({ title, description } = {}, { state, matches, params, url, signal } = {}) {
if (typeof title === 'string') {
setTitle(title);
} else if (typeof title === 'function') {
setTitle(await title({ state, matches, params, url, signal }));
}
if (typeof description === 'string') {
setDescription(description);
} else if (typeof description === 'function') {
setDescription(await description({ state, matches, params, url, signal }));
}
}
async function _handleModule(moduleSrc, {
state: state$1 = state.getStateObj(),
matches = {},
params = {},
stack,
signal,
...args
} = {}) {
const module = await Promise.try(() => {
if (moduleSrc instanceof Function) {
return moduleSrc(args);
} else if (typeof moduleSrc === 'string' || module instanceof URL) {
return _isModuleURL(moduleSrc)
? import(URL.parse(moduleSrc, document.baseURI))
: import(moduleSrc);
} else {
return new TypeError('Invalid module src.');
}
}).catch(err => err);
const url = new URL(location.href);
const timestamp = performance.now();
if (module instanceof URL) {
await navigate(module, state$1, args);
} else if (module instanceof Error) {
return module.message;
} else if (! ('default' in module)) {
return new Error(`${moduleSrc} has no default export.`);
} else if (module.default instanceof Function && module.default.prototype instanceof HTMLElement) {
if (typeof customElements.getName(module.default) !== 'string') {
customElements.define(
module.default[Symbol.for('tagName')] ?? `aegis-el-${crypto.randomUUID()}`,
module.default
);
}
if (typeof module.styles !== 'undefined') {
_addStyle(module.styles);
}
_handleMetadata(module, { state: state$1, matches, params, url, signal });
return new module.default({
url,
matches,
params,
state: state$1,
stack,
timestamp,
signal: getNavSignal({ signal }),
...args
});
} else if (module.default instanceof Function) {
if (typeof module.styles !== 'undefined') {
_addStyle(module.styles);
}
_handleMetadata(module, { state: state$1, matches, params, url, signal });
return await module.default({
url,
matches,
params,
state: state$1,
stack,
timestamp,
signal: getNavSignal({ signal }),
...args
});
} else if (module.default instanceof Node || module.default instanceof Error) {
if (typeof module.styles !== 'undefined') {
_addStyle(module.styles);
}
_handleMetadata(module, { state: state$1, matches, params, url, signal });
_updatePage(module.default);
} else if (module.default instanceof URL && module.default.origin === location.origin) {
navigate(module.default);
} else {
throw new TypeError(`${moduleSrc} has a missing or invalid default export.`);
}
}
let view404 = ({ url = location