source: imaps-frontend/node_modules/@remix-run/router/history.ts

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 21.1 KB
Line 
1////////////////////////////////////////////////////////////////////////////////
2//#region Types and Constants
3////////////////////////////////////////////////////////////////////////////////
4
5/**
6 * Actions represent the type of change to a location value.
7 */
8export enum Action {
9 /**
10 * A POP indicates a change to an arbitrary index in the history stack, such
11 * as a back or forward navigation. It does not describe the direction of the
12 * navigation, only that the current index changed.
13 *
14 * Note: This is the default action for newly created history objects.
15 */
16 Pop = "POP",
17
18 /**
19 * A PUSH indicates a new entry being added to the history stack, such as when
20 * a link is clicked and a new page loads. When this happens, all subsequent
21 * entries in the stack are lost.
22 */
23 Push = "PUSH",
24
25 /**
26 * A REPLACE indicates the entry at the current index in the history stack
27 * being replaced by a new one.
28 */
29 Replace = "REPLACE",
30}
31
32/**
33 * The pathname, search, and hash values of a URL.
34 */
35export interface Path {
36 /**
37 * A URL pathname, beginning with a /.
38 */
39 pathname: string;
40
41 /**
42 * A URL search string, beginning with a ?.
43 */
44 search: string;
45
46 /**
47 * A URL fragment identifier, beginning with a #.
48 */
49 hash: string;
50}
51
52// TODO: (v7) Change the Location generic default from `any` to `unknown` and
53// remove Remix `useLocation` wrapper.
54
55/**
56 * An entry in a history stack. A location contains information about the
57 * URL path, as well as possibly some arbitrary state and a key.
58 */
59export interface Location<State = any> extends Path {
60 /**
61 * A value of arbitrary data associated with this location.
62 */
63 state: State;
64
65 /**
66 * A unique string associated with this location. May be used to safely store
67 * and retrieve data in some other storage API, like `localStorage`.
68 *
69 * Note: This value is always "default" on the initial location.
70 */
71 key: string;
72}
73
74/**
75 * A change to the current location.
76 */
77export interface Update {
78 /**
79 * The action that triggered the change.
80 */
81 action: Action;
82
83 /**
84 * The new location.
85 */
86 location: Location;
87
88 /**
89 * The delta between this location and the former location in the history stack
90 */
91 delta: number | null;
92}
93
94/**
95 * A function that receives notifications about location changes.
96 */
97export interface Listener {
98 (update: Update): void;
99}
100
101/**
102 * Describes a location that is the destination of some navigation, either via
103 * `history.push` or `history.replace`. This may be either a URL or the pieces
104 * of a URL path.
105 */
106export type To = string | Partial<Path>;
107
108/**
109 * A history is an interface to the navigation stack. The history serves as the
110 * source of truth for the current location, as well as provides a set of
111 * methods that may be used to change it.
112 *
113 * It is similar to the DOM's `window.history` object, but with a smaller, more
114 * focused API.
115 */
116export interface History {
117 /**
118 * The last action that modified the current location. This will always be
119 * Action.Pop when a history instance is first created. This value is mutable.
120 */
121 readonly action: Action;
122
123 /**
124 * The current location. This value is mutable.
125 */
126 readonly location: Location;
127
128 /**
129 * Returns a valid href for the given `to` value that may be used as
130 * the value of an <a href> attribute.
131 *
132 * @param to - The destination URL
133 */
134 createHref(to: To): string;
135
136 /**
137 * Returns a URL for the given `to` value
138 *
139 * @param to - The destination URL
140 */
141 createURL(to: To): URL;
142
143 /**
144 * Encode a location the same way window.history would do (no-op for memory
145 * history) so we ensure our PUSH/REPLACE navigations for data routers
146 * behave the same as POP
147 *
148 * @param to Unencoded path
149 */
150 encodeLocation(to: To): Path;
151
152 /**
153 * Pushes a new location onto the history stack, increasing its length by one.
154 * If there were any entries in the stack after the current one, they are
155 * lost.
156 *
157 * @param to - The new URL
158 * @param state - Data to associate with the new location
159 */
160 push(to: To, state?: any): void;
161
162 /**
163 * Replaces the current location in the history stack with a new one. The
164 * location that was replaced will no longer be available.
165 *
166 * @param to - The new URL
167 * @param state - Data to associate with the new location
168 */
169 replace(to: To, state?: any): void;
170
171 /**
172 * Navigates `n` entries backward/forward in the history stack relative to the
173 * current index. For example, a "back" navigation would use go(-1).
174 *
175 * @param delta - The delta in the stack index
176 */
177 go(delta: number): void;
178
179 /**
180 * Sets up a listener that will be called whenever the current location
181 * changes.
182 *
183 * @param listener - A function that will be called when the location changes
184 * @returns unlisten - A function that may be used to stop listening
185 */
186 listen(listener: Listener): () => void;
187}
188
189type HistoryState = {
190 usr: any;
191 key?: string;
192 idx: number;
193};
194
195const PopStateEventType = "popstate";
196//#endregion
197
198////////////////////////////////////////////////////////////////////////////////
199//#region Memory History
200////////////////////////////////////////////////////////////////////////////////
201
202/**
203 * A user-supplied object that describes a location. Used when providing
204 * entries to `createMemoryHistory` via its `initialEntries` option.
205 */
206export type InitialEntry = string | Partial<Location>;
207
208export type MemoryHistoryOptions = {
209 initialEntries?: InitialEntry[];
210 initialIndex?: number;
211 v5Compat?: boolean;
212};
213
214/**
215 * A memory history stores locations in memory. This is useful in stateful
216 * environments where there is no web browser, such as node tests or React
217 * Native.
218 */
219export interface MemoryHistory extends History {
220 /**
221 * The current index in the history stack.
222 */
223 readonly index: number;
224}
225
226/**
227 * Memory history stores the current location in memory. It is designed for use
228 * in stateful non-browser environments like tests and React Native.
229 */
230export function createMemoryHistory(
231 options: MemoryHistoryOptions = {}
232): MemoryHistory {
233 let { initialEntries = ["/"], initialIndex, v5Compat = false } = options;
234 let entries: Location[]; // Declare so we can access from createMemoryLocation
235 entries = initialEntries.map((entry, index) =>
236 createMemoryLocation(
237 entry,
238 typeof entry === "string" ? null : entry.state,
239 index === 0 ? "default" : undefined
240 )
241 );
242 let index = clampIndex(
243 initialIndex == null ? entries.length - 1 : initialIndex
244 );
245 let action = Action.Pop;
246 let listener: Listener | null = null;
247
248 function clampIndex(n: number): number {
249 return Math.min(Math.max(n, 0), entries.length - 1);
250 }
251 function getCurrentLocation(): Location {
252 return entries[index];
253 }
254 function createMemoryLocation(
255 to: To,
256 state: any = null,
257 key?: string
258 ): Location {
259 let location = createLocation(
260 entries ? getCurrentLocation().pathname : "/",
261 to,
262 state,
263 key
264 );
265 warning(
266 location.pathname.charAt(0) === "/",
267 `relative pathnames are not supported in memory history: ${JSON.stringify(
268 to
269 )}`
270 );
271 return location;
272 }
273
274 function createHref(to: To) {
275 return typeof to === "string" ? to : createPath(to);
276 }
277
278 let history: MemoryHistory = {
279 get index() {
280 return index;
281 },
282 get action() {
283 return action;
284 },
285 get location() {
286 return getCurrentLocation();
287 },
288 createHref,
289 createURL(to) {
290 return new URL(createHref(to), "http://localhost");
291 },
292 encodeLocation(to: To) {
293 let path = typeof to === "string" ? parsePath(to) : to;
294 return {
295 pathname: path.pathname || "",
296 search: path.search || "",
297 hash: path.hash || "",
298 };
299 },
300 push(to, state) {
301 action = Action.Push;
302 let nextLocation = createMemoryLocation(to, state);
303 index += 1;
304 entries.splice(index, entries.length, nextLocation);
305 if (v5Compat && listener) {
306 listener({ action, location: nextLocation, delta: 1 });
307 }
308 },
309 replace(to, state) {
310 action = Action.Replace;
311 let nextLocation = createMemoryLocation(to, state);
312 entries[index] = nextLocation;
313 if (v5Compat && listener) {
314 listener({ action, location: nextLocation, delta: 0 });
315 }
316 },
317 go(delta) {
318 action = Action.Pop;
319 let nextIndex = clampIndex(index + delta);
320 let nextLocation = entries[nextIndex];
321 index = nextIndex;
322 if (listener) {
323 listener({ action, location: nextLocation, delta });
324 }
325 },
326 listen(fn: Listener) {
327 listener = fn;
328 return () => {
329 listener = null;
330 };
331 },
332 };
333
334 return history;
335}
336//#endregion
337
338////////////////////////////////////////////////////////////////////////////////
339//#region Browser History
340////////////////////////////////////////////////////////////////////////////////
341
342/**
343 * A browser history stores the current location in regular URLs in a web
344 * browser environment. This is the standard for most web apps and provides the
345 * cleanest URLs the browser's address bar.
346 *
347 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory
348 */
349export interface BrowserHistory extends UrlHistory {}
350
351export type BrowserHistoryOptions = UrlHistoryOptions;
352
353/**
354 * Browser history stores the location in regular URLs. This is the standard for
355 * most web apps, but it requires some configuration on the server to ensure you
356 * serve the same app at multiple URLs.
357 *
358 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
359 */
360export function createBrowserHistory(
361 options: BrowserHistoryOptions = {}
362): BrowserHistory {
363 function createBrowserLocation(
364 window: Window,
365 globalHistory: Window["history"]
366 ) {
367 let { pathname, search, hash } = window.location;
368 return createLocation(
369 "",
370 { pathname, search, hash },
371 // state defaults to `null` because `window.history.state` does
372 (globalHistory.state && globalHistory.state.usr) || null,
373 (globalHistory.state && globalHistory.state.key) || "default"
374 );
375 }
376
377 function createBrowserHref(window: Window, to: To) {
378 return typeof to === "string" ? to : createPath(to);
379 }
380
381 return getUrlBasedHistory(
382 createBrowserLocation,
383 createBrowserHref,
384 null,
385 options
386 );
387}
388//#endregion
389
390////////////////////////////////////////////////////////////////////////////////
391//#region Hash History
392////////////////////////////////////////////////////////////////////////////////
393
394/**
395 * A hash history stores the current location in the fragment identifier portion
396 * of the URL in a web browser environment.
397 *
398 * This is ideal for apps that do not control the server for some reason
399 * (because the fragment identifier is never sent to the server), including some
400 * shared hosting environments that do not provide fine-grained controls over
401 * which pages are served at which URLs.
402 *
403 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory
404 */
405export interface HashHistory extends UrlHistory {}
406
407export type HashHistoryOptions = UrlHistoryOptions;
408
409/**
410 * Hash history stores the location in window.location.hash. This makes it ideal
411 * for situations where you don't want to send the location to the server for
412 * some reason, either because you do cannot configure it or the URL space is
413 * reserved for something else.
414 *
415 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
416 */
417export function createHashHistory(
418 options: HashHistoryOptions = {}
419): HashHistory {
420 function createHashLocation(
421 window: Window,
422 globalHistory: Window["history"]
423 ) {
424 let {
425 pathname = "/",
426 search = "",
427 hash = "",
428 } = parsePath(window.location.hash.substr(1));
429
430 // Hash URL should always have a leading / just like window.location.pathname
431 // does, so if an app ends up at a route like /#something then we add a
432 // leading slash so all of our path-matching behaves the same as if it would
433 // in a browser router. This is particularly important when there exists a
434 // root splat route (<Route path="*">) since that matches internally against
435 // "/*" and we'd expect /#something to 404 in a hash router app.
436 if (!pathname.startsWith("/") && !pathname.startsWith(".")) {
437 pathname = "/" + pathname;
438 }
439
440 return createLocation(
441 "",
442 { pathname, search, hash },
443 // state defaults to `null` because `window.history.state` does
444 (globalHistory.state && globalHistory.state.usr) || null,
445 (globalHistory.state && globalHistory.state.key) || "default"
446 );
447 }
448
449 function createHashHref(window: Window, to: To) {
450 let base = window.document.querySelector("base");
451 let href = "";
452
453 if (base && base.getAttribute("href")) {
454 let url = window.location.href;
455 let hashIndex = url.indexOf("#");
456 href = hashIndex === -1 ? url : url.slice(0, hashIndex);
457 }
458
459 return href + "#" + (typeof to === "string" ? to : createPath(to));
460 }
461
462 function validateHashLocation(location: Location, to: To) {
463 warning(
464 location.pathname.charAt(0) === "/",
465 `relative pathnames are not supported in hash history.push(${JSON.stringify(
466 to
467 )})`
468 );
469 }
470
471 return getUrlBasedHistory(
472 createHashLocation,
473 createHashHref,
474 validateHashLocation,
475 options
476 );
477}
478//#endregion
479
480////////////////////////////////////////////////////////////////////////////////
481//#region UTILS
482////////////////////////////////////////////////////////////////////////////////
483
484/**
485 * @private
486 */
487export function invariant(value: boolean, message?: string): asserts value;
488export function invariant<T>(
489 value: T | null | undefined,
490 message?: string
491): asserts value is T;
492export function invariant(value: any, message?: string) {
493 if (value === false || value === null || typeof value === "undefined") {
494 throw new Error(message);
495 }
496}
497
498export function warning(cond: any, message: string) {
499 if (!cond) {
500 // eslint-disable-next-line no-console
501 if (typeof console !== "undefined") console.warn(message);
502
503 try {
504 // Welcome to debugging history!
505 //
506 // This error is thrown as a convenience, so you can more easily
507 // find the source for a warning that appears in the console by
508 // enabling "pause on exceptions" in your JavaScript debugger.
509 throw new Error(message);
510 // eslint-disable-next-line no-empty
511 } catch (e) {}
512 }
513}
514
515function createKey() {
516 return Math.random().toString(36).substr(2, 8);
517}
518
519/**
520 * For browser-based histories, we combine the state and key into an object
521 */
522function getHistoryState(location: Location, index: number): HistoryState {
523 return {
524 usr: location.state,
525 key: location.key,
526 idx: index,
527 };
528}
529
530/**
531 * Creates a Location object with a unique key from the given Path
532 */
533export function createLocation(
534 current: string | Location,
535 to: To,
536 state: any = null,
537 key?: string
538): Readonly<Location> {
539 let location: Readonly<Location> = {
540 pathname: typeof current === "string" ? current : current.pathname,
541 search: "",
542 hash: "",
543 ...(typeof to === "string" ? parsePath(to) : to),
544 state,
545 // TODO: This could be cleaned up. push/replace should probably just take
546 // full Locations now and avoid the need to run through this flow at all
547 // But that's a pretty big refactor to the current test suite so going to
548 // keep as is for the time being and just let any incoming keys take precedence
549 key: (to && (to as Location).key) || key || createKey(),
550 };
551 return location;
552}
553
554/**
555 * Creates a string URL path from the given pathname, search, and hash components.
556 */
557export function createPath({
558 pathname = "/",
559 search = "",
560 hash = "",
561}: Partial<Path>) {
562 if (search && search !== "?")
563 pathname += search.charAt(0) === "?" ? search : "?" + search;
564 if (hash && hash !== "#")
565 pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
566 return pathname;
567}
568
569/**
570 * Parses a string URL path into its separate pathname, search, and hash components.
571 */
572export function parsePath(path: string): Partial<Path> {
573 let parsedPath: Partial<Path> = {};
574
575 if (path) {
576 let hashIndex = path.indexOf("#");
577 if (hashIndex >= 0) {
578 parsedPath.hash = path.substr(hashIndex);
579 path = path.substr(0, hashIndex);
580 }
581
582 let searchIndex = path.indexOf("?");
583 if (searchIndex >= 0) {
584 parsedPath.search = path.substr(searchIndex);
585 path = path.substr(0, searchIndex);
586 }
587
588 if (path) {
589 parsedPath.pathname = path;
590 }
591 }
592
593 return parsedPath;
594}
595
596export interface UrlHistory extends History {}
597
598export type UrlHistoryOptions = {
599 window?: Window;
600 v5Compat?: boolean;
601};
602
603function getUrlBasedHistory(
604 getLocation: (window: Window, globalHistory: Window["history"]) => Location,
605 createHref: (window: Window, to: To) => string,
606 validateLocation: ((location: Location, to: To) => void) | null,
607 options: UrlHistoryOptions = {}
608): UrlHistory {
609 let { window = document.defaultView!, v5Compat = false } = options;
610 let globalHistory = window.history;
611 let action = Action.Pop;
612 let listener: Listener | null = null;
613
614 let index = getIndex()!;
615 // Index should only be null when we initialize. If not, it's because the
616 // user called history.pushState or history.replaceState directly, in which
617 // case we should log a warning as it will result in bugs.
618 if (index == null) {
619 index = 0;
620 globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
621 }
622
623 function getIndex(): number {
624 let state = globalHistory.state || { idx: null };
625 return state.idx;
626 }
627
628 function handlePop() {
629 action = Action.Pop;
630 let nextIndex = getIndex();
631 let delta = nextIndex == null ? null : nextIndex - index;
632 index = nextIndex;
633 if (listener) {
634 listener({ action, location: history.location, delta });
635 }
636 }
637
638 function push(to: To, state?: any) {
639 action = Action.Push;
640 let location = createLocation(history.location, to, state);
641 if (validateLocation) validateLocation(location, to);
642
643 index = getIndex() + 1;
644 let historyState = getHistoryState(location, index);
645 let url = history.createHref(location);
646
647 // try...catch because iOS limits us to 100 pushState calls :/
648 try {
649 globalHistory.pushState(historyState, "", url);
650 } catch (error) {
651 // If the exception is because `state` can't be serialized, let that throw
652 // outwards just like a replace call would so the dev knows the cause
653 // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
654 // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
655 if (error instanceof DOMException && error.name === "DataCloneError") {
656 throw error;
657 }
658 // They are going to lose state here, but there is no real
659 // way to warn them about it since the page will refresh...
660 window.location.assign(url);
661 }
662
663 if (v5Compat && listener) {
664 listener({ action, location: history.location, delta: 1 });
665 }
666 }
667
668 function replace(to: To, state?: any) {
669 action = Action.Replace;
670 let location = createLocation(history.location, to, state);
671 if (validateLocation) validateLocation(location, to);
672
673 index = getIndex();
674 let historyState = getHistoryState(location, index);
675 let url = history.createHref(location);
676 globalHistory.replaceState(historyState, "", url);
677
678 if (v5Compat && listener) {
679 listener({ action, location: history.location, delta: 0 });
680 }
681 }
682
683 function createURL(to: To): URL {
684 // window.location.origin is "null" (the literal string value) in Firefox
685 // under certain conditions, notably when serving from a local HTML file
686 // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
687 let base =
688 window.location.origin !== "null"
689 ? window.location.origin
690 : window.location.href;
691
692 let href = typeof to === "string" ? to : createPath(to);
693 // Treating this as a full URL will strip any trailing spaces so we need to
694 // pre-encode them since they might be part of a matching splat param from
695 // an ancestor route
696 href = href.replace(/ $/, "%20");
697 invariant(
698 base,
699 `No window.location.(origin|href) available to create URL for href: ${href}`
700 );
701 return new URL(href, base);
702 }
703
704 let history: History = {
705 get action() {
706 return action;
707 },
708 get location() {
709 return getLocation(window, globalHistory);
710 },
711 listen(fn: Listener) {
712 if (listener) {
713 throw new Error("A history only accepts one active listener");
714 }
715 window.addEventListener(PopStateEventType, handlePop);
716 listener = fn;
717
718 return () => {
719 window.removeEventListener(PopStateEventType, handlePop);
720 listener = null;
721 };
722 },
723 createHref(to) {
724 return createHref(window, to);
725 },
726 createURL,
727 encodeLocation(to) {
728 // Encode a Location the same way window.location would
729 let url = createURL(to);
730 return {
731 pathname: url.pathname,
732 search: url.search,
733 hash: url.hash,
734 };
735 },
736 push,
737 replace,
738 go(n) {
739 return globalHistory.go(n);
740 },
741 };
742
743 return history;
744}
745
746//#endregion
Note: See TracBrowser for help on using the repository browser.