@virtualstate/navigation
Version:
Native JavaScript [navigation](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) implementation
216 lines • 9.21 kB
JavaScript
import { AbortController } from "./import-abort-controller.js";
import { InvalidStateError } from "./navigation-errors.js";
import { NavigationTransitionAbort, NavigationTransitionEntry, NavigationTransitionInitialEntries, NavigationTransitionNavigationType, NavigationIntercept, Rollback, } from "./navigation-transition.js";
import { getBaseURL } from "./base-url.js";
import { defer } from "./defer.js";
import { ok } from "./is.js";
import { NavigateEvent, NavigationCurrentEntryChangeEvent } from "./events/index.js";
export const NavigationFormData = Symbol.for("@virtualstate/navigation/formData");
export const NavigationDownloadRequest = Symbol.for("@virtualstate/navigation/downloadRequest");
export const NavigationCanIntercept = Symbol.for("@virtualstate/navigation/canIntercept");
export const NavigationUserInitiated = Symbol.for("@virtualstate/navigation/userInitiated");
export const NavigationOriginalEvent = Symbol.for("@virtualstate/navigation/originalEvent");
export const EventAbortController = Symbol.for("@virtualstate/navigation/event/abortController");
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;
}
export 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();
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
};
}
//# sourceMappingURL=create-navigation-transition.js.map