UNPKG

@virtualstate/navigation

Version:

Native JavaScript [navigation](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API) implementation

216 lines 9.21 kB
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