UNPKG

@virtualstate/app-history

Version:

Native JavaScript [app-history](https://github.com/WICG/app-history) implementation

135 lines 6.05 kB
import AbortController from "abort-controller"; import { InvalidStateError } from "./app-history-errors.js"; import { AppHistoryTransitionAbort, AppHistoryTransitionEntry, AppHistoryTransitionInitialEntries, AppHistoryTransitionNavigationType, AppHistoryTransitionWhile, Rollback } from "./app-history-transition.js"; import { createEvent } from "./event-target/create-event.js"; export const AppHistoryFormData = Symbol.for("@virtualstate/app-history/formData"); export const AppHistoryCanTransition = Symbol.for("@virtualstate/app-history/canTransition"); export const AppHistoryUserInitiated = Symbol.for("@virtualstate/app-history/userInitiated"); const baseUrl = "https://html.spec.whatwg.org/"; export const EventAbortController = Symbol.for("@virtualstate/app-history/event/abortController"); 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 createAppHistoryTransition(context) { const { currentIndex, options, known: initialKnown, current, transition, transition: { [AppHistoryTransitionInitialEntries]: previousEntries, [AppHistoryTransitionEntry]: entry, [AppHistoryTransitionWhile]: transitionWhile } } = context; let { transition: { [AppHistoryTransitionNavigationType]: 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") && currentIndex !== -1) { destinationIndex = currentIndex; nextIndex = currentIndex; } else if (navigationType === "replace") { navigationType = "push"; destinationIndex = currentIndex + 1; nextIndex = destinationIndex; } 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, index: destinationIndex, sameDocument: entry.sameDocument, getState() { return entry.getState(); } }; let hashChange = false; const currentUrlInstance = new URL(current?.url ?? "/", baseUrl); const destinationUrlInstance = new URL(destination.url, baseUrl); const currentHash = currentUrlInstance.hash; const destinationHash = destinationUrlInstance.hash; if (currentHash !== destinationHash) { const currentUrlInstanceWithoutHash = new URL(currentUrlInstance.toString()); currentUrlInstanceWithoutHash.hash = ""; const destinationUrlInstanceWithoutHash = new URL(destinationUrlInstance.toString()); destinationUrlInstanceWithoutHash.hash = ""; hashChange = currentUrlInstanceWithoutHash.toString() === destinationUrlInstanceWithoutHash.toString(); } const navigateController = new AbortController(); const navigate = createEvent({ [EventAbortController]: navigateController, signal: navigateController.signal, info: undefined, ...options, canTransition: options?.[AppHistoryCanTransition] ?? true, formData: options?.[AppHistoryFormData] ?? undefined, hashChange, navigationType: options?.navigationType ?? (typeof navigationType === "string" ? navigationType : "replace"), userInitiated: options?.[AppHistoryUserInitiated] ?? false, destination, preventDefault: transition[AppHistoryTransitionAbort].bind(transition), transitionWhile, type: "navigate" }); const currentChange = createEvent({ from: current, type: "currentchange", navigationType: navigate.navigationType, transitionWhile }); 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)); } else // Default next index is current entries length, aka // console.log({ navigationType, givenNavigationType, index: this.#currentIndex, resolvedNextIndex }); if (navigationType === "replace" || navigationType === "traverse" || navigationType === "reload") { resolvedEntries[destination.index] = entry; if (navigationType === "replace") { resolvedEntries = resolvedEntries.slice(0, destination.index + 1); } } else if (navigationType === "push") { // 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]}) } resolvedEntries.push(entry); } known.add(entry); return { entries: resolvedEntries, known, index: nextIndex, currentChange, destination, navigate, navigationType }; } //# sourceMappingURL=create-app-history-transition.js.map