@virtualstate/navigation
Version:
Native JavaScript [navigation](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) implementation
1,312 lines (1,286 loc) • 107 kB
JavaScript
function isEvent(value) {
function isLike(value) {
return !!value;
}
return (isLike(value) &&
(typeof value.type === "string" || typeof value.type === "symbol"));
}
function assertEvent(value, type) {
if (!isEvent(value)) {
throw new Error("Expected event");
}
if (typeof type !== "undefined" && value.type !== type) {
throw new Error(`Expected event type ${String(type)}, got ${value.type.toString()}`);
}
}
function isParallelEvent(value) {
return isEvent(value) && value.parallel !== false;
}
class AbortError extends Error {
constructor(message) {
super(`AbortError${message ? `: ${message}` : ""}`);
this.name = "AbortError";
}
}
function isAbortError(error) {
return error instanceof Error && error.name === "AbortError";
}
class InvalidStateError extends Error {
constructor(message) {
super(`InvalidStateError${message ? `: ${message}` : ""}`);
this.name = "InvalidStateError";
}
}
function isInvalidStateError(error) {
return error instanceof Error && error.name === "InvalidStateError";
}
function isAbortSignal(value) {
function isAbortSignalLike(value) {
return typeof value === "object";
}
return (isAbortSignalLike(value) &&
typeof value.aborted === "boolean" &&
typeof value.addEventListener === "function");
}
function isSignalEvent(value) {
function isSignalEventLike(value) {
return value.hasOwnProperty("signal");
}
return (isEvent(value) && isSignalEventLike(value) && isAbortSignal(value.signal));
}
function isSignalHandled(event, error) {
if (isSignalEvent(event) &&
event.signal.aborted &&
error instanceof Error &&
isAbortError(error)) {
return true;
}
}
/**
* @experimental
*/
const EventTargetListeners$1 = Symbol.for("@opennetwork/environment/events/target/listeners");
/**
* @experimental
*/
const EventTargetListenersIgnore = Symbol.for("@opennetwork/environment/events/target/listeners/ignore");
/**
* @experimental
*/
const EventTargetListenersMatch = Symbol.for("@opennetwork/environment/events/target/listeners/match");
/**
* @experimental
*/
const EventTargetListenersThis = Symbol.for("@opennetwork/environment/events/target/listeners/this");
const EventDescriptorSymbol = Symbol.for("@opennetwork/environment/events/descriptor");
function matchEventCallback(type, callback, options) {
const optionsDescriptor = isOptionsDescriptor(options) ? options : undefined;
return (descriptor) => {
if (optionsDescriptor) {
return optionsDescriptor === descriptor;
}
return ((!callback || callback === descriptor.callback) &&
type === descriptor.type);
};
function isOptionsDescriptor(options) {
function isLike(options) {
return !!options;
}
return isLike(options) && options[EventDescriptorSymbol] === true;
}
}
function isFunctionEventCallback(fn) {
return typeof fn === "function";
}
const EventTargetDescriptors = Symbol.for("@virtualstate/navigation/event-target/descriptors");
class EventTargetListeners {
[EventTargetDescriptors] = [];
[EventTargetListenersIgnore] = new WeakSet();
get [EventTargetListeners$1]() {
return [...(this[EventTargetDescriptors] ?? [])];
}
[EventTargetListenersMatch](type) {
const external = this[EventTargetListeners$1];
const matched = [
...new Set([...(external ?? []), ...(this[EventTargetDescriptors] ?? [])]),
]
.filter((descriptor) => descriptor.type === type || descriptor.type === "*")
.filter((descriptor) => !this[EventTargetListenersIgnore]?.has(descriptor));
const listener = typeof type === "string" ? this[`on${type}`] : undefined;
if (typeof listener === "function" && isFunctionEventCallback(listener)) {
matched.push({
type,
callback: listener,
[EventDescriptorSymbol]: true,
});
}
return matched;
}
addEventListener(type, callback, options) {
const listener = {
...options,
isListening: () => !!this[EventTargetDescriptors]?.find(matchEventCallback(type, callback)),
descriptor: {
[EventDescriptorSymbol]: true,
...options,
type,
callback,
},
timestamp: Date.now(),
};
if (listener.isListening()) {
return;
}
this[EventTargetDescriptors]?.push(listener.descriptor);
}
removeEventListener(type, callback, options) {
if (!isFunctionEventCallback(callback)) {
return;
}
const externalListeners = this[EventTargetListeners$1] ?? this[EventTargetDescriptors] ?? [];
const externalIndex = externalListeners.findIndex(matchEventCallback(type, callback, options));
if (externalIndex === -1) {
return;
}
const index = this[EventTargetDescriptors]?.findIndex(matchEventCallback(type, callback, options)) ??
-1;
if (index !== -1) {
this[EventTargetDescriptors]?.splice(index, 1);
}
const descriptor = externalListeners[externalIndex];
if (descriptor) {
this[EventTargetListenersIgnore]?.add(descriptor);
}
}
hasEventListener(type, callback) {
if (callback && !isFunctionEventCallback(callback)) {
return false;
}
const foundIndex = this[EventTargetDescriptors]?.findIndex(matchEventCallback(type, callback)) ?? -1;
return foundIndex > -1;
}
}
class AsyncEventTarget extends EventTargetListeners {
[EventTargetListenersThis];
constructor(thisValue = undefined) {
super();
this[EventTargetListenersThis] = thisValue;
}
async dispatchEvent(event) {
const listeners = this[EventTargetListenersMatch]?.(event.type) ?? [];
// Don't even dispatch an aborted event
if (isSignalEvent(event) && event.signal.aborted) {
throw new AbortError();
}
const parallel = isParallelEvent(event);
const promises = [];
for (let index = 0; index < listeners.length; index += 1) {
const descriptor = listeners[index];
const promise = (async () => {
// Remove the listener before invoking the callback
// This ensures that inside of the callback causes no more additional event triggers to this
// listener
if (descriptor.once) {
// by passing the descriptor as the options, we get an internal redirect
// that forces an instance level object equals, meaning
// we will only remove _this_ descriptor!
this.removeEventListener(descriptor.type, descriptor.callback, descriptor);
}
await descriptor.callback.call(this[EventTargetListenersThis] ?? this, event);
})();
if (!parallel) {
try {
await promise;
}
catch (error) {
if (!isSignalHandled(event, error)) {
await Promise.reject(error);
}
}
if (isSignalEvent(event) && event.signal.aborted) {
// bye
return;
}
}
else {
promises.push(promise);
}
}
if (promises.length) {
// Allows for all promises to settle finish so we can stay within the event, we then
// will utilise Promise.all which will reject with the first rejected promise
const results = await Promise.allSettled(promises);
const rejected = results.filter((result) => {
return result.status === "rejected";
});
if (rejected.length) {
let unhandled = rejected;
// If the event was aborted, then allow abort errors to occur, and handle these as handled errors
// The dispatcher does not care about this because they requested it
//
// There may be other unhandled errors that are more pressing to the task they are doing.
//
// The dispatcher can throw an abort error if they need to throw it up the chain
if (isSignalEvent(event) && event.signal.aborted) {
unhandled = unhandled.filter((result) => !isSignalHandled(event, result.reason));
}
if (unhandled.length === 1) {
await Promise.reject(unhandled[0].reason);
throw unhandled[0].reason; // We shouldn't get here
}
else if (unhandled.length > 1) {
throw new AggregateError(unhandled.map(({ reason }) => reason));
}
}
}
}
}
const defaultEventTargetModule = {
EventTarget: AsyncEventTarget,
AsyncEventTarget,
SyncEventTarget: AsyncEventTarget,
};
let eventTargetModule = defaultEventTargetModule;
//
// try {
// eventTargetModule = await import("@virtualstate/navigation/event-target");
// console.log("Using @virtualstate/navigation/event-target", eventTargetModule);
// } catch {
// console.log("Using defaultEventTargetModule");
// eventTargetModule = defaultEventTargetModule;
// }
const EventTargetImplementation = eventTargetModule.EventTarget || eventTargetModule.SyncEventTarget || eventTargetModule.AsyncEventTarget;
function assertEventTarget(target) {
if (typeof target !== "function") {
throw new Error("Could not load EventTarget implementation");
}
}
class EventTarget extends AsyncEventTarget {
constructor(...args) {
super();
if (EventTargetImplementation) {
assertEventTarget(EventTargetImplementation);
const { dispatchEvent } = new EventTargetImplementation(...args);
this.dispatchEvent = dispatchEvent;
}
}
}
function isInterceptEvent(value) {
function isInterceptEventLike(value) {
return isEvent(value);
}
return (isInterceptEventLike(value) && typeof value.intercept === "function");
}
class NavigationEventTarget extends EventTarget {
addEventListener(type, listener, options) {
assertEventCallback(listener);
return super.addEventListener(type, listener, typeof options === "boolean" ? { once: options } : options);
function assertEventCallback(listener) {
if (typeof listener !== "function")
throw new Error("Please us the function variant of event listener");
}
}
removeEventListener(type, listener, options) {
assertEventCallback(listener);
return super.removeEventListener(type, listener);
function assertEventCallback(listener) {
if (typeof listener !== "function")
throw new Error("Please us the function variant of event listener");
}
}
}
const isWebCryptoSupported = "crypto" in globalThis && typeof globalThis.crypto.randomUUID === "function";
const v4 = isWebCryptoSupported
? globalThis.crypto.randomUUID.bind(globalThis.crypto)
: () => Array.from({ length: 5 }, () => `${Math.random()}`.replace(/^0\./, ""))
.join("-")
.replace(".", "");
// To prevent cyclic imports, where a circular is used, instead use the prototype interface
// and then copy over the "private" symbol
const NavigationGetState$1 = Symbol.for("@virtualstate/navigation/getState");
const NavigationHistoryEntryNavigationType = Symbol.for("@virtualstate/navigation/entry/navigationType");
const NavigationHistoryEntryKnownAs = Symbol.for("@virtualstate/navigation/entry/knownAs");
const NavigationHistoryEntrySetState = Symbol.for("@virtualstate/navigation/entry/setState");
function isPrimitiveValue(state) {
return (typeof state === "number" ||
typeof state === "boolean" ||
typeof state === "symbol" ||
typeof state === "bigint" ||
typeof state === "string");
}
function isValue(state) {
return !!(state || isPrimitiveValue(state));
}
class NavigationHistoryEntry extends NavigationEventTarget {
#index;
#state;
get index() {
return typeof this.#index === "number" ? this.#index : this.#index();
}
key;
id;
url;
sameDocument;
get [NavigationHistoryEntryNavigationType]() {
return this.#options.navigationType;
}
get [NavigationHistoryEntryKnownAs]() {
const set = new Set(this.#options[NavigationHistoryEntryKnownAs]);
set.add(this.id);
return set;
}
#options;
get [EventTargetListeners$1]() {
return [
...(super[EventTargetListeners$1] ?? []),
...(this.#options[EventTargetListeners$1] ?? []),
];
}
constructor(init) {
super();
this.#options = init;
this.key = init.key || v4();
this.id = v4();
this.url = init.url ?? undefined;
this.#index = init.index;
this.sameDocument = init.sameDocument ?? true;
this.#state = init.state ?? undefined;
}
[NavigationGetState$1]() {
return this.#options?.getState?.(this);
}
getState() {
let state = this.#state;
if (!isValue(state)) {
const external = this[NavigationGetState$1]();
if (isValue(external)) {
state = this.#state = external;
}
}
/**
* https://github.com/WICG/app-history/blob/7c0332b30746b14863f717404402bc49e497a2b2/spec.bs#L1406
* Note that in general, unless the state value is a primitive, entry.getState() !== entry.getState(), since a fresh copy is returned each time.
*/
if (typeof state === "undefined" ||
isPrimitiveValue(state)) {
return state;
}
if (typeof state === "function") {
console.warn("State passed to Navigation.navigate was a function, this may be unintentional");
console.warn("Unless a state value is primitive, with a standard implementation of Navigation");
console.warn("your state value will be serialized and deserialized before this point, meaning");
console.warn("a function would not be usable.");
}
return {
...state,
};
}
[NavigationHistoryEntrySetState](state) {
this.#state = state;
}
}
/**
* @param handleCatch rejected promises automatically to allow free usage
*/
function deferred(handleCatch) {
let resolve = undefined, reject = undefined;
const promise = new Promise((resolveFn, rejectFn) => {
resolve = resolveFn;
reject = rejectFn;
});
ok$1(resolve);
ok$1(reject);
return {
resolve,
reject,
promise: handleCatch ? promise.catch(handleCatch) : promise,
};
}
function ok$1(value) {
if (!value) {
throw new Error("Value not provided");
}
}
const GlobalAbortController = typeof AbortController !== "undefined" ? AbortController : undefined;
if (!GlobalAbortController) {
throw new Error("AbortController expected to be available or polyfilled");
}
const AbortController$1 = GlobalAbortController;
function isPromise(value) {
return (like(value) &&
typeof value.then === "function");
}
function ok(value, message = "Expected value") {
if (!value) {
throw new Error(message);
}
}
function isPromiseRejectedResult(value) {
return value.status === "rejected";
}
function like(value) {
return !!value;
}
const THIS_WILL_BE_REMOVED = "This will be removed when the first major release of @virtualstate/navigation is published";
const WARNINGS = {
EVENT_INTERCEPT_HANDLER: `You are using a non standard interface, please update your code to use event.intercept({ async handler() {} })\n${THIS_WILL_BE_REMOVED}`
};
let GLOBAL_IS_WARNINGS_IGNORED = false;
let GLOBAL_IS_WARNINGS_TRACED = true;
function setIgnoreWarnings(ignore) {
GLOBAL_IS_WARNINGS_IGNORED = ignore;
}
function setTraceWarnings(ignore) {
GLOBAL_IS_WARNINGS_TRACED = ignore;
}
function logWarning(warning, ...message) {
if (GLOBAL_IS_WARNINGS_IGNORED) {
return;
}
try {
if (GLOBAL_IS_WARNINGS_TRACED) {
console.trace(WARNINGS[warning], ...message);
}
else {
console.warn(WARNINGS[warning], ...message);
}
}
catch {
// We don't want attempts to log causing issues
// maybe we don't have a console
}
}
const Rollback = Symbol.for("@virtualstate/navigation/rollback");
const Unset = Symbol.for("@virtualstate/navigation/unset");
const NavigationTransitionParentEventTarget = Symbol.for("@virtualstate/navigation/transition/parentEventTarget");
const NavigationTransitionFinishedDeferred = Symbol.for("@virtualstate/navigation/transition/deferred/finished");
const NavigationTransitionCommittedDeferred = Symbol.for("@virtualstate/navigation/transition/deferred/committed");
const NavigationTransitionNavigationType = Symbol.for("@virtualstate/navigation/transition/navigationType");
const NavigationTransitionInitialEntries = Symbol.for("@virtualstate/navigation/transition/entries/initial");
const NavigationTransitionFinishedEntries = Symbol.for("@virtualstate/navigation/transition/entries/finished");
const NavigationTransitionInitialIndex = Symbol.for("@virtualstate/navigation/transition/index/initial");
const NavigationTransitionFinishedIndex = Symbol.for("@virtualstate/navigation/transition/index/finished");
const NavigationTransitionEntry = Symbol.for("@virtualstate/navigation/transition/entry");
const NavigationTransitionIsCommitted = Symbol.for("@virtualstate/navigation/transition/isCommitted");
const NavigationTransitionIsFinished = Symbol.for("@virtualstate/navigation/transition/isFinished");
const NavigationTransitionIsRejected = Symbol.for("@virtualstate/navigation/transition/isRejected");
const NavigationTransitionKnown = Symbol.for("@virtualstate/navigation/transition/known");
const NavigationTransitionPromises = Symbol.for("@virtualstate/navigation/transition/promises");
const NavigationIntercept = Symbol.for("@virtualstate/navigation/intercept");
const NavigationTransitionIsOngoing = Symbol.for("@virtualstate/navigation/transition/isOngoing");
const NavigationTransitionIsPending = Symbol.for("@virtualstate/navigation/transition/isPending");
const NavigationTransitionIsAsync = Symbol.for("@virtualstate/navigation/transition/isAsync");
const NavigationTransitionWait = Symbol.for("@virtualstate/navigation/transition/wait");
const NavigationTransitionPromiseResolved = Symbol.for("@virtualstate/navigation/transition/promise/resolved");
const NavigationTransitionRejected = Symbol.for("@virtualstate/navigation/transition/rejected");
const NavigationTransitionBeforeCommit = Symbol.for("@virtualstate/navigation/transition/beforeCommit");
const NavigationTransitionCommit = Symbol.for("@virtualstate/navigation/transition/commit");
const NavigationTransitionFinish = Symbol.for("@virtualstate/navigation/transition/finish");
const NavigationTransitionStart = Symbol.for("@virtualstate/navigation/transition/start");
const NavigationTransitionStartDeadline = Symbol.for("@virtualstate/navigation/transition/start/deadline");
const NavigationTransitionError = Symbol.for("@virtualstate/navigation/transition/error");
const NavigationTransitionFinally = Symbol.for("@virtualstate/navigation/transition/finally");
const NavigationTransitionAbort = Symbol.for("@virtualstate/navigation/transition/abort");
const NavigationTransitionInterceptOptionsCommit = Symbol.for("@virtualstate/navigation/transition/intercept/options/commit");
const NavigationTransitionCommitIsManual = Symbol.for("@virtualstate/navigation/transition/commit/isManual");
class NavigationTransition extends EventTarget {
finished;
/**
* @experimental
*/
committed;
from;
navigationType;
/**
* true if transition has an async intercept
*/
[NavigationTransitionIsAsync] = false;
/**
* @experimental
*/
[NavigationTransitionInterceptOptionsCommit];
#options;
[NavigationTransitionFinishedDeferred] = deferred();
[NavigationTransitionCommittedDeferred] = deferred();
get [NavigationTransitionIsPending]() {
return !!this.#promises.size;
}
get [NavigationTransitionNavigationType]() {
return this.#options[NavigationTransitionNavigationType];
}
get [NavigationTransitionInitialEntries]() {
return this.#options[NavigationTransitionInitialEntries];
}
get [NavigationTransitionInitialIndex]() {
return this.#options[NavigationTransitionInitialIndex];
}
get [NavigationTransitionCommitIsManual]() {
return !!(this[NavigationTransitionInterceptOptionsCommit]?.includes("after-transition") ||
this[NavigationTransitionInterceptOptionsCommit]?.includes("manual"));
}
[NavigationTransitionFinishedEntries];
[NavigationTransitionFinishedIndex];
[NavigationTransitionIsCommitted] = false;
[NavigationTransitionIsFinished] = false;
[NavigationTransitionIsRejected] = false;
[NavigationTransitionIsOngoing] = false;
[NavigationTransitionKnown] = new Set();
[NavigationTransitionEntry];
#promises = new Set();
#rolledBack = false;
#abortController = new AbortController$1();
get signal() {
return this.#abortController.signal;
}
get [NavigationTransitionPromises]() {
return this.#promises;
}
constructor(init) {
super();
this[NavigationTransitionInterceptOptionsCommit] = [];
this[NavigationTransitionFinishedDeferred] =
init[NavigationTransitionFinishedDeferred] ??
this[NavigationTransitionFinishedDeferred];
this[NavigationTransitionCommittedDeferred] =
init[NavigationTransitionCommittedDeferred] ??
this[NavigationTransitionCommittedDeferred];
this.#options = init;
const finished = (this.finished =
this[NavigationTransitionFinishedDeferred].promise);
const committed = (this.committed =
this[NavigationTransitionCommittedDeferred].promise);
// Auto catching abort
void finished.catch((error) => error);
void committed.catch((error) => error);
this.from = init.from;
this.navigationType = init.navigationType;
this[NavigationTransitionFinishedEntries] =
init[NavigationTransitionFinishedEntries];
this[NavigationTransitionFinishedIndex] =
init[NavigationTransitionFinishedIndex];
const known = init[NavigationTransitionKnown];
if (known) {
for (const entry of known) {
this[NavigationTransitionKnown].add(entry);
}
}
this[NavigationTransitionEntry] = init[NavigationTransitionEntry];
// Event listeners
{
// Events to promises
{
this.addEventListener(NavigationTransitionCommit, this.#onCommitPromise, { once: true });
this.addEventListener(NavigationTransitionFinish, this.#onFinishPromise, { once: true });
}
// Events to property setters
{
this.addEventListener(NavigationTransitionCommit, this.#onCommitSetProperty, { once: true });
this.addEventListener(NavigationTransitionFinish, this.#onFinishSetProperty, { once: true });
}
// Rejection + Abort
{
this.addEventListener(NavigationTransitionError, this.#onError, {
once: true,
});
this.addEventListener(NavigationTransitionAbort, () => {
if (!this[NavigationTransitionIsFinished]) {
return this[NavigationTransitionRejected](new AbortError());
}
});
}
// Proxy all events from this transition onto entry + the parent event target
//
// The parent could be another transition, or the Navigation, this allows us to
// "bubble up" events layer by layer
//
// In this implementation, this allows individual transitions to "intercept" navigate and break the child
// transition from happening
//
// TODO WARN this may not be desired behaviour vs standard spec'd Navigation
{
this.addEventListener("*", this[NavigationTransitionEntry].dispatchEvent.bind(this[NavigationTransitionEntry]));
this.addEventListener("*", init[NavigationTransitionParentEventTarget].dispatchEvent.bind(init[NavigationTransitionParentEventTarget]));
}
}
}
rollback = (options) => {
// console.log({ rolled: this.#rolledBack });
if (this.#rolledBack) {
// TODO
throw new InvalidStateError("Rollback invoked multiple times: Please raise an issue at https://github.com/virtualstate/navigation with the use case where you want to use a rollback multiple times, this may have been unexpected behaviour");
}
this.#rolledBack = true;
return this.#options.rollback(options);
};
#onCommitSetProperty = () => {
this[NavigationTransitionIsCommitted] = true;
};
#onFinishSetProperty = () => {
this[NavigationTransitionIsFinished] = true;
};
#onFinishPromise = () => {
// console.log("onFinishPromise")
this[NavigationTransitionFinishedDeferred].resolve(this[NavigationTransitionEntry]);
};
#onCommitPromise = () => {
if (this.signal.aborted) ;
else {
this[NavigationTransitionCommittedDeferred].resolve(this[NavigationTransitionEntry]);
}
};
#onError = (event) => {
return this[NavigationTransitionRejected](event.error);
};
[NavigationTransitionPromiseResolved] = (...promises) => {
for (const promise of promises) {
this.#promises.delete(promise);
}
};
[NavigationTransitionRejected] = async (reason) => {
if (this[NavigationTransitionIsRejected])
return;
this[NavigationTransitionIsRejected] = true;
this[NavigationTransitionAbort]();
const navigationType = this[NavigationTransitionNavigationType];
// console.log({ navigationType, reason, entry: this[NavigationTransitionEntry] });
if (typeof navigationType === "string" || navigationType === Rollback) {
// console.log("navigateerror", { reason, z: isInvalidStateError(reason) });
await this.dispatchEvent({
type: "navigateerror",
error: reason,
get message() {
if (reason instanceof Error) {
return reason.message;
}
return `${reason}`;
},
});
// console.log("navigateerror finished");
if (navigationType !== Rollback &&
!(isInvalidStateError(reason) || isAbortError(reason))) {
try {
// console.log("Rollback", navigationType);
// console.warn("Rolling back immediately due to internal error", error);
await this.rollback()?.finished;
// console.log("Rollback complete", navigationType);
}
catch (error) {
// console.error("Failed to rollback", error);
throw new InvalidStateError("Failed to rollback, please raise an issue at https://github.com/virtualstate/navigation/issues");
}
}
}
this[NavigationTransitionCommittedDeferred].reject(reason);
this[NavigationTransitionFinishedDeferred].reject(reason);
};
[NavigationIntercept] = (options) => {
const transition = this;
const promise = parseOptions();
this[NavigationTransitionIsOngoing] = true;
if (!promise)
return;
this[NavigationTransitionIsAsync] = true;
const statusPromise = promise
.then(() => ({
status: "fulfilled",
value: undefined,
}))
.catch(async (reason) => {
await this[NavigationTransitionRejected](reason);
return {
status: "rejected",
reason,
};
});
this.#promises.add(statusPromise);
function parseOptions() {
if (!options)
return undefined;
if (isPromise(options)) {
logWarning("EVENT_INTERCEPT_HANDLER");
return options;
}
if (typeof options === "function") {
logWarning("EVENT_INTERCEPT_HANDLER");
return options();
}
const { handler, commit } = options;
if (commit && typeof commit === "string") {
transition[NavigationTransitionInterceptOptionsCommit].push(commit);
}
if (typeof handler !== "function") {
return;
}
return handler();
}
};
[NavigationTransitionWait] = async () => {
if (!this.#promises.size)
return this[NavigationTransitionEntry];
try {
const captured = [...this.#promises];
const results = await Promise.all(captured);
const rejected = results.filter((result) => result.status === "rejected");
// console.log({ rejected, results, captured });
if (rejected.length) {
// TODO handle differently when there are failures, e.g. we could move navigateerror to here
if (rejected.length === 1) {
throw rejected[0].reason;
}
if (typeof AggregateError !== "undefined") {
throw new AggregateError(rejected.map(({ reason }) => reason));
}
throw new Error();
}
this[NavigationTransitionPromiseResolved](...captured);
if (this[NavigationTransitionIsPending]) {
return this[NavigationTransitionWait]();
}
return this[NavigationTransitionEntry];
}
catch (error) {
await this.#onError(error);
throw await Promise.reject(error);
}
finally {
await this[NavigationTransitionFinish]();
}
};
[NavigationTransitionAbort]() {
if (this.#abortController.signal.aborted)
return;
this.#abortController.abort();
this.dispatchEvent({
type: NavigationTransitionAbort,
transition: this,
entry: this[NavigationTransitionEntry],
});
}
[NavigationTransitionFinish] = async () => {
if (this[NavigationTransitionIsFinished]) {
return;
}
await this.dispatchEvent({
type: NavigationTransitionFinish,
transition: this,
entry: this[NavigationTransitionEntry],
intercept: this[NavigationIntercept],
});
};
}
function getWindowBaseURL() {
try {
if (typeof window !== "undefined" && window.location) {
return window.location.href;
}
}
catch { }
}
function getBaseURL(url) {
const baseURL = getWindowBaseURL() ?? "https://html.spec.whatwg.org/";
return new URL(
// Deno wants this to be always a string
(url ?? "").toString(), baseURL);
}
function defer() {
let resolve = undefined, reject = undefined, settled = false, status = "pending";
const promise = new Promise((resolveFn, rejectFn) => {
resolve = (value) => {
status = "fulfilled";
settled = true;
resolveFn(value);
};
reject = (reason) => {
status = "rejected";
settled = true;
rejectFn(reason);
};
});
ok(resolve);
ok(reject);
return {
get settled() {
return settled;
},
get status() {
return status;
},
resolve,
reject,
promise,
};
}
class NavigationCurrentEntryChangeEvent {
type;
from;
navigationType;
constructor(type, init) {
this.type = type;
if (!init) {
throw new TypeError("init required");
}
if (!init.from) {
throw new TypeError("from required");
}
this.from = init.from;
this.navigationType = init.navigationType ?? undefined;
}
}
class NavigateEvent {
type;
canIntercept;
/**
* @deprecated
*/
canTransition;
destination;
downloadRequest;
formData;
hashChange;
info;
signal;
userInitiated;
navigationType;
constructor(type, init) {
this.type = type;
if (!init) {
throw new TypeError("init required");
}
if (!init.destination) {
throw new TypeError("destination required");
}
if (!init.signal) {
throw new TypeError("signal required");
}
this.canIntercept = init.canIntercept ?? false;
this.canTransition = init.canIntercept ?? false;
this.destination = init.destination;
this.downloadRequest = init.downloadRequest;
this.formData = init.formData;
this.hashChange = init.hashChange ?? false;
this.info = init.info;
this.signal = init.signal;
this.userInitiated = init.userInitiated ?? false;
this.navigationType = init.navigationType ?? "push";
}
commit() {
throw new Error("Not implemented");
}
intercept(options) {
throw new Error("Not implemented");
}
preventDefault() {
throw new Error("Not implemented");
}
reportError(reason) {
throw new Error("Not implemented");
}
scroll() {
throw new Error("Not implemented");
}
/**
* @deprecated
*/
transitionWhile(options) {
return this.intercept(options);
}
}
const NavigationFormData = Symbol.for("@virtualstate/navigation/formData");
const NavigationDownloadRequest = Symbol.for("@virtualstate/navigation/downloadRequest");
const NavigationCanIntercept = Symbol.for("@virtualstate/navigation/canIntercept");
const NavigationUserInitiated = Symbol.for("@virtualstate/navigation/userInitiated");
const NavigationOriginalEvent = Symbol.for("@virtualstate/navigation/originalEvent");
function noop() {
return undefined;
}
function getEntryIndex(entries, entry) {
const knownIndex = entry.index;
if (knownIndex !== -1) {
return knownIndex;
}
// TODO find an entry if it has changed id
return -1;
}
function createNavigationTransition(context) {
const { commit: transitionCommit, currentIndex, options, known: initialKnown, currentEntry, transition, transition: { [NavigationTransitionInitialEntries]: previousEntries, [NavigationTransitionEntry]: entry, [NavigationIntercept]: intercept, }, reportError } = context;
let { transition: { [NavigationTransitionNavigationType]: navigationType }, } = context;
let resolvedEntries = [...previousEntries];
const known = new Set(initialKnown);
let destinationIndex = -1, nextIndex = currentIndex;
if (navigationType === Rollback) {
const { index } = options ?? { index: undefined };
if (typeof index !== "number")
throw new InvalidStateError("Expected index to be provided for rollback");
destinationIndex = index;
nextIndex = index;
}
else if (navigationType === "traverse" || navigationType === "reload") {
destinationIndex = getEntryIndex(previousEntries, entry);
nextIndex = destinationIndex;
}
else if (navigationType === "replace") {
if (currentIndex === -1) {
navigationType = "push";
destinationIndex = currentIndex + 1;
nextIndex = destinationIndex;
}
else {
destinationIndex = currentIndex;
nextIndex = currentIndex;
}
}
else {
destinationIndex = currentIndex + 1;
nextIndex = destinationIndex;
}
if (typeof destinationIndex !== "number" || destinationIndex === -1) {
throw new InvalidStateError("Could not resolve next index");
}
// console.log({ navigationType, entry, options });
if (!entry.url) {
console.trace({ navigationType, entry, options });
throw new InvalidStateError("Expected entry url");
}
const destination = {
url: entry.url,
key: entry.key,
id: entry.id,
index: destinationIndex,
sameDocument: entry.sameDocument,
getState() {
return entry.getState();
},
};
let hashChange = false;
const currentUrlInstance = getBaseURL(currentEntry?.url);
const destinationUrlInstance = new URL(destination.url);
const currentHash = currentUrlInstance.hash;
const destinationHash = destinationUrlInstance.hash;
// console.log({ currentHash, destinationHash });
if (currentHash !== destinationHash) {
const currentUrlInstanceWithoutHash = new URL(currentUrlInstance.toString());
currentUrlInstanceWithoutHash.hash = "";
const destinationUrlInstanceWithoutHash = new URL(destinationUrlInstance.toString());
destinationUrlInstanceWithoutHash.hash = "";
hashChange =
currentUrlInstanceWithoutHash.toString() ===
destinationUrlInstanceWithoutHash.toString();
// console.log({ hashChange, currentUrlInstanceWithoutHash: currentUrlInstanceWithoutHash.toString(), before: destinationUrlInstanceWithoutHash.toString() })
}
let contextToCommit;
const { resolve: resolveCommit, promise: waitForCommit } = defer();
function commit() {
ok(contextToCommit, "Expected contextToCommit");
resolveCommit(transitionCommit(contextToCommit));
}
const abortController = new AbortController$1();
const event = new NavigateEvent("navigate", {
signal: abortController.signal,
info: undefined,
...options,
canIntercept: options?.[NavigationCanIntercept] ?? true,
formData: options?.[NavigationFormData] ?? undefined,
downloadRequest: options?.[NavigationDownloadRequest] ?? undefined,
hashChange,
navigationType: options?.navigationType ??
(typeof navigationType === "string" ? navigationType : "replace"),
userInitiated: options?.[NavigationUserInitiated] ?? false,
destination,
});
const originalEvent = options?.[NavigationOriginalEvent];
const preventDefault = transition[NavigationTransitionAbort].bind(transition);
if (originalEvent) {
const definedEvent = originalEvent;
event.intercept = function originalEventIntercept(options) {
definedEvent.preventDefault();
return intercept(options);
};
event.preventDefault = function originalEventPreventDefault() {
definedEvent.preventDefault();
return preventDefault();
};
}
else {
event.intercept = intercept;
event.preventDefault = preventDefault;
}
// Enforce that transitionWhile and intercept match
event.transitionWhile = event.intercept;
event.commit = commit;
if (reportError) {
event.reportError = reportError;
}
event.scroll = noop;
if (originalEvent) {
event.originalEvent = originalEvent;
}
const currentEntryChange = new NavigationCurrentEntryChangeEvent("currententrychange", {
from: currentEntry,
navigationType: event.navigationType,
});
let updatedEntries = [], removedEntries = [], addedEntries = [];
const previousKeys = previousEntries.map(entry => entry.key);
if (navigationType === Rollback) {
const { entries } = options ?? { entries: undefined };
if (!entries)
throw new InvalidStateError("Expected entries to be provided for rollback");
resolvedEntries = entries;
resolvedEntries.forEach((entry) => known.add(entry));
const keys = resolvedEntries.map(entry => entry.key);
removedEntries = previousEntries.filter(entry => !keys.includes(entry.key));
addedEntries = resolvedEntries.filter(entry => !previousKeys.includes(entry.key));
}
// Default next index is current entries length, aka
// console.log({ navigationType, givenNavigationType, index: this.#currentIndex, resolvedNextIndex });
else if (navigationType === "replace" ||
navigationType === "traverse" ||
navigationType === "reload") {
resolvedEntries[destination.index] = entry;
if (navigationType !== "traverse") {
updatedEntries.push(entry);
}
if (navigationType === "replace") {
resolvedEntries = resolvedEntries.slice(0, destination.index + 1);
}
const keys = resolvedEntries.map(entry => entry.key);
removedEntries = previousEntries.filter(entry => !keys.includes(entry.key));
if (previousKeys.includes(entry.id)) {
addedEntries = [entry];
}
}
else if (navigationType === "push") {
let removed = false;
// Trim forward, we have reset our stack
if (resolvedEntries[destination.index]) {
// const before = [...this.#entries];
resolvedEntries = resolvedEntries.slice(0, destination.index);
// console.log({ before, after: [...this.#entries]})
removed = true;
}
resolvedEntries.push(entry);
addedEntries = [entry];
if (removed) {
const keys = resolvedEntries.map(entry => entry.key);
removedEntries = previousEntries.filter(entry => !keys.includes(entry.key));
}
}
known.add(entry);
let entriesChange = undefined;
if (updatedEntries.length || addedEntries.length || removedEntries.length) {
entriesChange = {
updatedEntries,
addedEntries,
removedEntries
};
}
contextToCommit = {
entries: resolvedEntries,
index: nextIndex,
known,
entriesChange
};
return {
entries: resolvedEntries,
known,
index: nextIndex,
currentEntryChange,
destination,
navigate: event,
navigationType,
waitForCommit,
commit,
abortController
};
}
function createEvent(event) {
if (typeof CustomEvent !== "undefined" && typeof event.type === "string") {
if (event instanceof CustomEvent) {
return event;
}
const { type, detail, ...rest } = event;
const customEvent = new CustomEvent(type, {
detail: detail ?? rest,
});
Object.assign(customEvent, rest);
assertEvent(customEvent, event.type);
return customEvent;
}
return event;
}
const NavigationSetOptions = Symbol.for("@virtualstate/navigation/setOptions");
const NavigationSetEntries = Symbol.for("@virtualstate/navigation/setEntries");
const NavigationSetCurrentIndex = Symbol.for("@virtualstate/navigation/setCurrentIndex");
const NavigationSetCurrentKey = Symbol.for("@virtualstate/navigation/setCurrentKey");
const NavigationGetState = Symbol.for("@virtualstate/navigation/getState");
const NavigationSetState = Symbol.for("@virtualstate/navigation/setState");
const NavigationDisposeState = Symbol.for("@virtualstate/navigation/disposeState");
function isNavigationNavigationType(value) {
return (value === "reload" ||
value === "push" ||
value === "replace" ||
value === "traverse");
}
class Navigation extends NavigationEventTarget {
// Should be always 0 or 1
#transitionInProgressCount = 0;
// #activePromise?: Promise<void> = undefined;
#entries = [];
#known = new Set();
#currentIndex = -1;
#activeTransition;
#knownTransitions = new WeakSet();
#baseURL = "";
#initialEntry = undefined;
#options = undefined;
get canGoBack() {
return !!this.#entries[this.#currentIndex - 1];
}
get canGoForward() {
return !!this.#entries[this.#currentIndex + 1];
}
get currentEntry() {
if (this.#currentIndex === -1) {
if (!this.#initialEntry) {
this.#initialEntry = new NavigationHistoryEntry({
getState: this[NavigationGetState],
navigationType: "push",
index: -1,
sameDocument: false,
url: this.#baseURL.toString()
});
}
return this.#initialEntry;
}
return this.#entries[this.#currentIndex];
}
get transition() {
const transition = this.#activeTransition;
// Never let an aborted transition leak, it doesn't need to be accessed any more
return transition?.signal.aborted ? undefined : transition;
}
constructor(options = {}) {
super();
this[NavigationSetOptions](options);
}
[NavigationSetOptions](options) {
this.#options = options;
this.#baseURL = getBaseURL(options?.baseURL);
this.#entries = [];
if (options.entries) {
this[NavigationSetEntries](options.entries);
}
if (options.currentKey) {
this[NavigationSetCurrentKey](options.currentKey);
}
else if (typeof options.currentIndex === "number") {
this[NavigationSetCurrentIndex](options.currentIndex);
}
}
/**
* Set the current entry key without any lifecycle eventing
*
* This would be more exact than providing an index
* @param key
*/
[NavigationSetCurrentKey](key) {
const index = this.#entries.findIndex(entry => entry.key === key);
// If the key can't be found, becomes a no-op
if (index === -1)
return;
this.#currentIndex = index;
}
/**
* Set the current entry index without any lifecycle eventing
* @param index
*/
[NavigationSetCurrentIndex](index) {
if (index <= -1)
return;
if (index >= this.#entries.length)
return;
this.#currentIndex = index;
}
/**
* Set the entries available without any lifecycle eventing
* @param entries
*/
[NavigationSetEntries](entries) {
this.#entries = entries.map(({ key, url, navigationType, state, sameDocument }, index) => new NavigationHistoryEntry({
getState: this[NavigationGetState],
navigationType: isNavigationNavigationType(navigationType) ? navigationType : "push",
sameDocument: sameDocument ?? true,
index,
url,
key,
state
}));
if (this.#currentIndex === -1 && this.#entries.length) {
// Initialise, even if its not the one that was expected
this.#currentIndex = 0;
}
}
[NavigationGetState] = (entry) => {
return this.#options?.getState?.(entry) ?? undefined;
};
[NavigationSetState] = (entry) => {
return this.#options?.setState?.(entry);
};
[NavigationDisposeState] = (entry) => {
return this.#options?.disposeState?.(entry);
};
back(options) {
if (!this.canGoBack)
throw new InvalidStateError("Cannot go back");
const entry = this.#entries[this.#currentIndex - 1];
return this.#pushEntry("traverse", this.#cloneNavigationHistoryEntry(entry, {
...options,
navigationType: "traverse",
}));
}
entries() {
return [...this.#entries];
}
forward(options) {
if (!this.canGoForward)
throw new InvalidStateError();
const entry = this.#entries[this.#currentIndex + 1];
return this.#pushEntry("traverse", this.#cloneNavigationHistoryEntry(entry, {
...options,
navigationType: "traverse",
}));
}
/**
/**
* @deprecated use traverseTo
*/
goTo(key, options) {
return this.traverseTo(key, options);
}
traverseTo(key, options) {
const found = this.#entries.find((entry) => entry.key === key);
if (found) {
return this.#pushEntry("traverse", this.#cloneNavigationHistoryEntry(found, {
...options,
navigationType: "traverse",
}));
}
throw new InvalidStateError();
}
#isSameDocument = (url) => {
function isSameOrigins(a, b) {
return a.origin === b.origin;
}
const currentEntryUrl = this.currentEntry?.url;
if (!currentEntryUrl)
return true;
return isSameOrigins(new URL(currentEntryUrl), new URL(url));
};
navigate(url, options) {
let baseURL = this.#baseURL;
if (this.currentEntry?.url) {
// This allows use to use relative
baseURL = this.currentEntry?.url;
}
const nextUrl = new URL(url, baseURL).toString();
let navigationType = "push";
if (options?.history === "auto" || !options?.hi