@esmx/router 3.0.0-rc.59 → 3.0.0-rc.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/options.mjs CHANGED
@@ -30,7 +30,7 @@ function getBaseUrl(options) {
30
30
  return base;
31
31
  }
32
32
  export function parsedOptions(options = {}) {
33
- var _a, _b, _c, _d, _e, _f;
33
+ var _a, _b, _c, _d, _e, _f, _g;
34
34
  const base = getBaseUrl(options);
35
35
  const routes = (_a = options.routes) != null ? _a : [];
36
36
  const compiledRoutes = createRouteMatches(routes);
@@ -50,9 +50,11 @@ export function parsedOptions(options = {}) {
50
50
  matcher: createMatcher(routes, compiledRoutes),
51
51
  normalizeURL: (_c = options.normalizeURL) != null ? _c : (url) => url,
52
52
  fallback: (_d = options.fallback) != null ? _d : fallback,
53
- handleBackBoundary: (_e = options.handleBackBoundary) != null ? _e : () => {
53
+ nextTick: (_e = options.nextTick) != null ? _e : () => {
54
54
  },
55
- handleLayerClose: (_f = options.handleLayerClose) != null ? _f : () => {
55
+ handleBackBoundary: (_f = options.handleBackBoundary) != null ? _f : () => {
56
+ },
57
+ handleLayerClose: (_g = options.handleLayerClose) != null ? _g : () => {
56
58
  }
57
59
  });
58
60
  }
@@ -20,10 +20,7 @@ export class RouteTaskController {
20
20
  this._aborted = true;
21
21
  }
22
22
  shouldCancel(name) {
23
- if (this._aborted) {
24
- return true;
25
- }
26
- return false;
23
+ return this._aborted;
27
24
  }
28
25
  }
29
26
  export async function createRouteTask(opts) {
@@ -6,8 +6,14 @@ import {
6
6
  RouteTaskController,
7
7
  createRouteTask
8
8
  } from "./route-task.mjs";
9
+ import {
10
+ getSavedScrollPosition,
11
+ saveScrollPosition,
12
+ scrollToPosition
13
+ } from "./scroll.mjs";
9
14
  import { RouteType } from "./types.mjs";
10
15
  import {
16
+ isBrowser,
11
17
  isRouteMatched,
12
18
  isUrlEqual,
13
19
  isValidConfirmHookResult,
@@ -106,6 +112,44 @@ export const ROUTE_TRANSITION_HOOKS = {
106
112
  return result;
107
113
  }
108
114
  }
115
+ if (isBrowser && "scrollRestoration" in window.history)
116
+ window.history.scrollRestoration = "manual";
117
+ if (from && isBrowser && !router.isLayer)
118
+ switch (to.type) {
119
+ case RouteType.push:
120
+ case RouteType.replace: {
121
+ if (!to.keepScrollPosition) {
122
+ saveScrollPosition(from.url.href);
123
+ scrollToPosition({ left: 0, top: 0 });
124
+ } else {
125
+ to.applyNavigationState({
126
+ __keepScrollPosition: to.keepScrollPosition
127
+ });
128
+ }
129
+ break;
130
+ }
131
+ case RouteType.go:
132
+ case RouteType.forward:
133
+ case RouteType.back:
134
+ // for popstate
135
+ case RouteType.unknown: {
136
+ saveScrollPosition(from.url.href);
137
+ setTimeout(async () => {
138
+ const state = window.history.state;
139
+ if (state == null ? void 0 : state.__keepScrollPosition) {
140
+ return;
141
+ }
142
+ const savedPosition = getSavedScrollPosition(
143
+ to.url.href,
144
+ { left: 0, top: 0 }
145
+ );
146
+ if (!savedPosition) return;
147
+ await router.parsedOptions.nextTick();
148
+ scrollToPosition(savedPosition);
149
+ });
150
+ break;
151
+ }
152
+ }
109
153
  switch (to.type) {
110
154
  case RouteType.push:
111
155
  return ROUTE_TYPE_HANDLERS.push;
@@ -0,0 +1,33 @@
1
+ /** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
2
+ interface _ScrollPosition extends ScrollToOptions {
3
+ left: number;
4
+ top: number;
5
+ }
6
+ export interface ScrollPositionElement extends ScrollToOptions {
7
+ /**
8
+ * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
9
+ * @example
10
+ * Here are some examples:
11
+ *
12
+ * - `.title`
13
+ * - `.content:first-child`
14
+ * - `#marker`
15
+ * - `#marker\~with\~symbols`
16
+ * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
17
+ *
18
+ */
19
+ el: string | Element;
20
+ }
21
+ /** Scroll parameters */
22
+ export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
23
+ /** Get current window scroll position */
24
+ export declare const winScrollPos: () => _ScrollPosition;
25
+ /** Scroll to specified position */
26
+ export declare function scrollToPosition(position: ScrollPosition): void;
27
+ /** Stored scroll positions */
28
+ export declare const scrollPositions: Map<string, _ScrollPosition>;
29
+ /** Save scroll position */
30
+ export declare function saveScrollPosition(key: string, scrollPosition?: _ScrollPosition): void;
31
+ /** Get saved scroll position */
32
+ export declare function getSavedScrollPosition(key: string, defaultValue?: _ScrollPosition | null): _ScrollPosition | null;
33
+ export {};
@@ -0,0 +1,49 @@
1
+ export const winScrollPos = () => ({
2
+ left: window.scrollX,
3
+ top: window.scrollY
4
+ });
5
+ function getElementPosition(el, offset) {
6
+ const docRect = document.documentElement.getBoundingClientRect();
7
+ const elRect = el.getBoundingClientRect();
8
+ return {
9
+ behavior: offset.behavior,
10
+ left: elRect.left - docRect.left - (offset.left || 0),
11
+ top: elRect.top - docRect.top - (offset.top || 0)
12
+ };
13
+ }
14
+ export function scrollToPosition(position) {
15
+ if ("el" in position) {
16
+ const positionEl = position.el;
17
+ const el = typeof positionEl === "string" ? document.querySelector(positionEl) : positionEl;
18
+ if (!el) return;
19
+ position = getElementPosition(el, position);
20
+ }
21
+ if ("scrollBehavior" in document.documentElement.style) {
22
+ window.scrollTo(position);
23
+ } else {
24
+ window.scrollTo(
25
+ Number.isFinite(position.left) ? position.left : window.scrollX,
26
+ Number.isFinite(position.top) ? position.top : window.scrollY
27
+ );
28
+ }
29
+ }
30
+ export const scrollPositions = /* @__PURE__ */ new Map();
31
+ const POSITION_KEY = "__scroll_position_key";
32
+ export function saveScrollPosition(key, scrollPosition = winScrollPos()) {
33
+ scrollPosition = { ...scrollPosition };
34
+ scrollPositions.set(key, scrollPosition);
35
+ try {
36
+ if (location.href !== key) return;
37
+ const stateCopy = {
38
+ ...history.state || {},
39
+ [POSITION_KEY]: scrollPosition
40
+ };
41
+ history.replaceState(stateCopy, "");
42
+ } catch (error) {
43
+ }
44
+ }
45
+ export function getSavedScrollPosition(key, defaultValue = null) {
46
+ const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
47
+ scrollPositions.delete(key);
48
+ return scroll || defaultValue;
49
+ }
package/dist/types.d.ts CHANGED
@@ -43,6 +43,7 @@ export interface RouteLocation {
43
43
  queryArray?: Record<string, string[] | undefined>;
44
44
  hash?: string;
45
45
  state?: RouteState;
46
+ /** When `true`, maintains current scroll position after navigation (default behavior scrolls to top) */
46
47
  keepScrollPosition?: boolean;
47
48
  statusCode?: number | null;
48
49
  layer?: RouteLayerOptions | null;
@@ -194,6 +195,7 @@ export interface RouterOptions {
194
195
  apps?: RouterMicroApp;
195
196
  normalizeURL?: (to: URL, from: URL | null) => URL;
196
197
  fallback?: RouteHandleHook;
198
+ nextTick?: () => Awaitable<void>;
197
199
  rootStyle?: Partial<CSSStyleDeclaration> | false | null;
198
200
  layer?: boolean;
199
201
  zIndex?: number;
package/package.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "unbuild": "3.6.0",
40
40
  "vitest": "3.2.4"
41
41
  },
42
- "version": "3.0.0-rc.59",
42
+ "version": "3.0.0-rc.60",
43
43
  "type": "module",
44
44
  "private": false,
45
45
  "exports": {
@@ -58,5 +58,5 @@
58
58
  "template",
59
59
  "public"
60
60
  ],
61
- "gitHead": "d221aba2a43064b5666e714f42904ae80b2b57ad"
61
+ "gitHead": "615e91c617e0a58796c591643c6a2e1d2a1f0a76"
62
62
  }
package/src/options.ts CHANGED
@@ -83,6 +83,7 @@ export function parsedOptions(
83
83
  matcher: createMatcher(routes, compiledRoutes),
84
84
  normalizeURL: options.normalizeURL ?? ((url) => url),
85
85
  fallback: options.fallback ?? fallback,
86
+ nextTick: options.nextTick ?? (() => {}),
86
87
  handleBackBoundary: options.handleBackBoundary ?? (() => {}),
87
88
  handleLayerClose: options.handleLayerClose ?? (() => {})
88
89
  });
package/src/route-task.ts CHANGED
@@ -23,10 +23,7 @@ export class RouteTaskController {
23
23
  }
24
24
 
25
25
  shouldCancel(name: string): boolean {
26
- if (this._aborted) {
27
- return true;
28
- }
29
- return false;
26
+ return this._aborted;
30
27
  }
31
28
  }
32
29
 
@@ -5,6 +5,12 @@ import {
5
5
  createRouteTask
6
6
  } from './route-task';
7
7
  import type { Router } from './router';
8
+ import {
9
+ getSavedScrollPosition,
10
+ saveScrollPosition,
11
+ scrollToPosition,
12
+ winScrollPos
13
+ } from './scroll';
8
14
  import { RouteType } from './types';
9
15
 
10
16
  import type {
@@ -15,6 +21,7 @@ import type {
15
21
  RouteNotifyHook
16
22
  } from './types';
17
23
  import {
24
+ isBrowser,
18
25
  isRouteMatched,
19
26
  isUrlEqual,
20
27
  isValidConfirmHookResult,
@@ -174,6 +181,46 @@ export const ROUTE_TRANSITION_HOOKS = {
174
181
  }
175
182
  }
176
183
 
184
+ if (isBrowser && 'scrollRestoration' in window.history)
185
+ window.history.scrollRestoration = 'manual';
186
+ // handle scroll position
187
+ if (from && isBrowser && !router.isLayer)
188
+ switch (to.type) {
189
+ case RouteType.push:
190
+ case RouteType.replace: {
191
+ if (!to.keepScrollPosition) {
192
+ saveScrollPosition(from.url.href);
193
+ scrollToPosition({ left: 0, top: 0 });
194
+ } else {
195
+ to.applyNavigationState({
196
+ __keepScrollPosition: to.keepScrollPosition
197
+ });
198
+ }
199
+ break;
200
+ }
201
+ case RouteType.go:
202
+ case RouteType.forward:
203
+ case RouteType.back:
204
+ // for popstate
205
+ case RouteType.unknown: {
206
+ saveScrollPosition(from.url.href);
207
+ setTimeout(async () => {
208
+ const state = window.history.state;
209
+ if (state?.__keepScrollPosition) {
210
+ return;
211
+ }
212
+ const savedPosition = getSavedScrollPosition(
213
+ to.url.href,
214
+ { left: 0, top: 0 }
215
+ );
216
+ if (!savedPosition) return;
217
+ await router.parsedOptions.nextTick();
218
+ scrollToPosition(savedPosition);
219
+ });
220
+ break;
221
+ }
222
+ }
223
+
177
224
  switch (to.type) {
178
225
  case RouteType.push:
179
226
  return ROUTE_TYPE_HANDLERS.push;
package/src/scroll.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { isBrowser } from './util';
2
+
3
+ /** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
4
+ interface _ScrollPosition extends ScrollToOptions {
5
+ left: number;
6
+ top: number;
7
+ }
8
+
9
+ export interface ScrollPositionElement extends ScrollToOptions {
10
+ /**
11
+ * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
12
+ * @example
13
+ * Here are some examples:
14
+ *
15
+ * - `.title`
16
+ * - `.content:first-child`
17
+ * - `#marker`
18
+ * - `#marker\~with\~symbols`
19
+ * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
20
+ *
21
+ */
22
+ el: string | Element;
23
+ }
24
+
25
+ /** Scroll parameters */
26
+ export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
27
+
28
+ /** Get current window scroll position */
29
+ export const winScrollPos = (): _ScrollPosition => ({
30
+ left: window.scrollX,
31
+ top: window.scrollY
32
+ });
33
+
34
+ /** Get element position for scrolling in document */
35
+ function getElementPosition(
36
+ el: Element,
37
+ offset: ScrollToOptions
38
+ ): _ScrollPosition {
39
+ const docRect = document.documentElement.getBoundingClientRect();
40
+ const elRect = el.getBoundingClientRect();
41
+
42
+ return {
43
+ behavior: offset.behavior,
44
+ left: elRect.left - docRect.left - (offset.left || 0),
45
+ top: elRect.top - docRect.top - (offset.top || 0)
46
+ };
47
+ }
48
+
49
+ /** Scroll to specified position */
50
+ export function scrollToPosition(position: ScrollPosition): void {
51
+ if ('el' in position) {
52
+ const positionEl = position.el;
53
+
54
+ const el =
55
+ typeof positionEl === 'string'
56
+ ? document.querySelector(positionEl)
57
+ : positionEl;
58
+
59
+ if (!el) return;
60
+
61
+ position = getElementPosition(el, position);
62
+ }
63
+
64
+ if ('scrollBehavior' in document.documentElement.style) {
65
+ window.scrollTo(position);
66
+ } else {
67
+ window.scrollTo(
68
+ Number.isFinite(position.left) ? position.left! : window.scrollX,
69
+ Number.isFinite(position.top) ? position.top! : window.scrollY
70
+ );
71
+ }
72
+ }
73
+
74
+ /** Stored scroll positions */
75
+ export const scrollPositions = new Map<string, _ScrollPosition>();
76
+
77
+ const POSITION_KEY = '__scroll_position_key';
78
+
79
+ /** Save scroll position */
80
+ export function saveScrollPosition(
81
+ key: string,
82
+ scrollPosition = winScrollPos()
83
+ ) {
84
+ scrollPosition = { ...scrollPosition };
85
+ scrollPositions.set(key, scrollPosition);
86
+
87
+ try {
88
+ if (location.href !== key) return;
89
+ // preserve the existing history state as it could be overridden by the user
90
+ const stateCopy = {
91
+ ...(history.state || {}),
92
+ [POSITION_KEY]: scrollPosition
93
+ };
94
+ history.replaceState(stateCopy, '');
95
+ } catch (error) {}
96
+ }
97
+
98
+ /** Get saved scroll position */
99
+ export function getSavedScrollPosition(
100
+ key: string,
101
+ defaultValue: _ScrollPosition | null = null
102
+ ): _ScrollPosition | null {
103
+ const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
104
+
105
+ // Saved scroll position should not be used multiple times, next time should use newly saved position
106
+ scrollPositions.delete(key);
107
+ return scroll || defaultValue;
108
+ }
package/src/types.ts CHANGED
@@ -91,6 +91,7 @@ export interface RouteLocation {
91
91
  queryArray?: Record<string, string[] | undefined>;
92
92
  hash?: string;
93
93
  state?: RouteState;
94
+ /** When `true`, maintains current scroll position after navigation (default behavior scrolls to top) */
94
95
  keepScrollPosition?: boolean;
95
96
  statusCode?: number | null;
96
97
  layer?: RouteLayerOptions | null;
@@ -266,6 +267,7 @@ export interface RouterOptions {
266
267
  apps?: RouterMicroApp;
267
268
  normalizeURL?: (to: URL, from: URL | null) => URL;
268
269
  fallback?: RouteHandleHook;
270
+ nextTick?: () => Awaitable<void>;
269
271
 
270
272
  rootStyle?: Partial<CSSStyleDeclaration> | false | null;
271
273
  layer?: boolean;