@real-router/angular 0.2.2 → 0.4.0
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/README.md +27 -1
- package/dist/README.md +27 -1
- package/dist/fesm2022/real-router-angular.mjs +392 -203
- package/dist/fesm2022/real-router-angular.mjs.map +1 -1
- package/dist/types/real-router-angular.d.ts +20 -3
- package/dist/types/real-router-angular.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/RouteView.ts +14 -0
- package/src/directives/RouteSelf.ts +6 -0
- package/src/dom-utils/index.ts +7 -0
- package/src/dom-utils/scroll-restore.ts +217 -0
- package/src/index.ts +2 -0
- package/src/providers.ts +31 -3
|
@@ -1,11 +1,365 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, inject, DestroyRef, InjectionToken, makeEnvironmentProviders, input, TemplateRef, Directive, contentChildren, computed, Component, output, effect, ElementRef } from '@angular/core';
|
|
2
|
+
import { signal, inject, DestroyRef, InjectionToken, provideEnvironmentInitializer, makeEnvironmentProviders, input, TemplateRef, Directive, contentChildren, computed, Component, output, effect, ElementRef } from '@angular/core';
|
|
3
3
|
import { getNavigator, UNKNOWN_ROUTE } from '@real-router/core';
|
|
4
4
|
import { createRouteSource, createRouteNodeSource, getTransitionSource, createActiveRouteSource, createDismissableError } from '@real-router/sources';
|
|
5
5
|
import { getPluginApi } from '@real-router/core/api';
|
|
6
6
|
import { getRouteUtils, startsWithSegment } from '@real-router/route-utils';
|
|
7
7
|
import { NgTemplateOutlet } from '@angular/common';
|
|
8
8
|
|
|
9
|
+
const CLEAR_DELAY = 7000;
|
|
10
|
+
const SAFARI_READY_DELAY = 100;
|
|
11
|
+
const ANNOUNCER_ATTR = "data-real-router-announcer";
|
|
12
|
+
const INTERNAL_ROUTE_PREFIX = "@@";
|
|
13
|
+
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
|
|
14
|
+
function createRouteAnnouncer(router, options) {
|
|
15
|
+
const prefix = options?.prefix ?? "Navigated to ";
|
|
16
|
+
const getCustomText = options?.getAnnouncementText;
|
|
17
|
+
let isInitialNavigation = true;
|
|
18
|
+
let isReady = false;
|
|
19
|
+
let isDestroyed = false;
|
|
20
|
+
let lastAnnouncedText = "";
|
|
21
|
+
let pendingText = null;
|
|
22
|
+
let clearTimeoutId;
|
|
23
|
+
const announcer = getOrCreateAnnouncer();
|
|
24
|
+
const doAnnounce = (text, h1) => {
|
|
25
|
+
lastAnnouncedText = text;
|
|
26
|
+
clearTimeout(clearTimeoutId);
|
|
27
|
+
announcer.textContent = text;
|
|
28
|
+
clearTimeoutId = setTimeout(() => {
|
|
29
|
+
announcer.textContent = "";
|
|
30
|
+
lastAnnouncedText = "";
|
|
31
|
+
}, CLEAR_DELAY);
|
|
32
|
+
manageFocus(h1);
|
|
33
|
+
};
|
|
34
|
+
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
35
|
+
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
36
|
+
// before marking the announcer "ready" — any navigation during that window is
|
|
37
|
+
// buffered in pendingText and flushed once the delay expires.
|
|
38
|
+
const safariTimeoutId = setTimeout(() => {
|
|
39
|
+
isReady = true;
|
|
40
|
+
if (pendingText !== null && !isDestroyed) {
|
|
41
|
+
const text = pendingText;
|
|
42
|
+
pendingText = null;
|
|
43
|
+
doAnnounce(text, document.querySelector("h1"));
|
|
44
|
+
}
|
|
45
|
+
}, SAFARI_READY_DELAY);
|
|
46
|
+
const unsubscribe = router.subscribe(({ route }) => {
|
|
47
|
+
if (isInitialNavigation) {
|
|
48
|
+
isInitialNavigation = false;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
52
|
+
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
53
|
+
// Single rAF fires before the new route's template has been attached,
|
|
54
|
+
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
55
|
+
// document.title / route.name prematurely.
|
|
56
|
+
requestAnimationFrame(() => {
|
|
57
|
+
requestAnimationFrame(() => {
|
|
58
|
+
if (isDestroyed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const h1 = document.querySelector("h1");
|
|
62
|
+
const text = resolveText(route, prefix, getCustomText, h1);
|
|
63
|
+
if (!text || text === lastAnnouncedText) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!isReady) {
|
|
67
|
+
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
68
|
+
pendingText = text;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
doAnnounce(text, h1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
destroy() {
|
|
77
|
+
isDestroyed = true;
|
|
78
|
+
unsubscribe();
|
|
79
|
+
clearTimeout(clearTimeoutId);
|
|
80
|
+
clearTimeout(safariTimeoutId);
|
|
81
|
+
removeAnnouncer();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getOrCreateAnnouncer() {
|
|
86
|
+
const existing = document.querySelector(`[${ANNOUNCER_ATTR}]`);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return existing;
|
|
89
|
+
}
|
|
90
|
+
const element = document.createElement("div");
|
|
91
|
+
element.setAttribute("style", VISUALLY_HIDDEN);
|
|
92
|
+
element.setAttribute("aria-live", "assertive");
|
|
93
|
+
element.setAttribute("aria-atomic", "true");
|
|
94
|
+
element.setAttribute(ANNOUNCER_ATTR, "");
|
|
95
|
+
document.body.prepend(element);
|
|
96
|
+
return element;
|
|
97
|
+
}
|
|
98
|
+
function removeAnnouncer() {
|
|
99
|
+
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
100
|
+
}
|
|
101
|
+
function resolveText(route, prefix, getCustomText, h1) {
|
|
102
|
+
if (getCustomText) {
|
|
103
|
+
return getCustomText(route);
|
|
104
|
+
}
|
|
105
|
+
const h1Text = h1?.textContent.trim() ?? "";
|
|
106
|
+
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
|
|
107
|
+
? ""
|
|
108
|
+
: route.name;
|
|
109
|
+
const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
|
|
110
|
+
return `${prefix}${rawText}`;
|
|
111
|
+
}
|
|
112
|
+
function manageFocus(h1) {
|
|
113
|
+
if (!h1) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!h1.hasAttribute("tabindex")) {
|
|
117
|
+
h1.setAttribute("tabindex", "-1");
|
|
118
|
+
}
|
|
119
|
+
h1.focus({ preventScroll: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const STORAGE_KEY = "real-router:scroll";
|
|
123
|
+
const NOOP_INSTANCE = Object.freeze({
|
|
124
|
+
destroy: () => {
|
|
125
|
+
/* no-op */
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
function createScrollRestoration(router, options) {
|
|
129
|
+
if (typeof globalThis.window === "undefined") {
|
|
130
|
+
return NOOP_INSTANCE;
|
|
131
|
+
}
|
|
132
|
+
const mode = options?.mode ?? "restore";
|
|
133
|
+
// mode "manual" = utility does nothing. Don't flip history.scrollRestoration,
|
|
134
|
+
// don't subscribe, don't register pagehide — leave the browser's native
|
|
135
|
+
// auto-restore intact for the app to override if it wants to.
|
|
136
|
+
if (mode === "manual") {
|
|
137
|
+
return NOOP_INSTANCE;
|
|
138
|
+
}
|
|
139
|
+
const anchorEnabled = options?.anchorScrolling ?? true;
|
|
140
|
+
const getContainer = options?.scrollContainer;
|
|
141
|
+
const prevScrollRestoration = history.scrollRestoration;
|
|
142
|
+
try {
|
|
143
|
+
history.scrollRestoration = "manual";
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore — some embedded contexts may reject the assignment.
|
|
147
|
+
}
|
|
148
|
+
// Resolve the container lazily on every event so containers mounted AFTER
|
|
149
|
+
// the provider still get correct scroll handling. Falls back to window when
|
|
150
|
+
// the getter is absent or returns null (pre-mount).
|
|
151
|
+
const readPos = () => {
|
|
152
|
+
const element = getContainer?.();
|
|
153
|
+
return element ? element.scrollTop : globalThis.scrollY;
|
|
154
|
+
};
|
|
155
|
+
const writePos = (top) => {
|
|
156
|
+
const element = getContainer?.();
|
|
157
|
+
if (element) {
|
|
158
|
+
element.scrollTop = top;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
globalThis.scrollTo(0, top);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const scrollToHashOrTop = () => {
|
|
165
|
+
const hash = globalThis.location.hash;
|
|
166
|
+
if (anchorEnabled && hash.length > 1) {
|
|
167
|
+
// location.hash is percent-encoded; ids in the DOM are the raw string.
|
|
168
|
+
// Decode for the match. Fall back to the raw slice if the hash contains
|
|
169
|
+
// a malformed escape sequence (decodeURIComponent throws on those).
|
|
170
|
+
let id;
|
|
171
|
+
try {
|
|
172
|
+
id = decodeURIComponent(hash.slice(1));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
id = hash.slice(1);
|
|
176
|
+
}
|
|
177
|
+
// eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars
|
|
178
|
+
const element = document.getElementById(id);
|
|
179
|
+
if (element) {
|
|
180
|
+
element.scrollIntoView();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
writePos(0);
|
|
185
|
+
};
|
|
186
|
+
let destroyed = false;
|
|
187
|
+
const unsubscribe = router.subscribe(({ route, previousRoute }) => {
|
|
188
|
+
const nav = route.context
|
|
189
|
+
.navigation;
|
|
190
|
+
// Browsers dispatch reload as the initial navigation after refresh, so
|
|
191
|
+
// previousRoute is undefined and capture is naturally skipped. The
|
|
192
|
+
// pre-refresh position was already persisted via pagehide.
|
|
193
|
+
if (previousRoute) {
|
|
194
|
+
putPos(keyOf(previousRoute), readPos());
|
|
195
|
+
}
|
|
196
|
+
// Single rAF so DOM is committed before we read anchors / write scroll.
|
|
197
|
+
// Guard against destroy() racing with the callback.
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
if (destroyed) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (mode === "top" || !nav) {
|
|
203
|
+
scrollToHashOrTop();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (nav.navigationType === "replace") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (nav.direction === "back" ||
|
|
210
|
+
nav.navigationType === "traverse" ||
|
|
211
|
+
nav.navigationType === "reload") {
|
|
212
|
+
writePos(loadStore()[keyOf(route)] ?? 0);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
scrollToHashOrTop();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
const onPageHide = () => {
|
|
219
|
+
const current = router.getState();
|
|
220
|
+
if (current) {
|
|
221
|
+
putPos(keyOf(current), readPos());
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
globalThis.addEventListener("pagehide", onPageHide);
|
|
225
|
+
return {
|
|
226
|
+
destroy: () => {
|
|
227
|
+
if (destroyed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
destroyed = true;
|
|
231
|
+
unsubscribe();
|
|
232
|
+
globalThis.removeEventListener("pagehide", onPageHide);
|
|
233
|
+
try {
|
|
234
|
+
history.scrollRestoration = prevScrollRestoration;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Ignore.
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function keyOf(state) {
|
|
243
|
+
return `${state.name}:${canonicalJson(state.params)}`;
|
|
244
|
+
}
|
|
245
|
+
function loadStore() {
|
|
246
|
+
try {
|
|
247
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
248
|
+
return raw ? JSON.parse(raw) : {};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function putPos(key, pos) {
|
|
255
|
+
try {
|
|
256
|
+
const store = loadStore();
|
|
257
|
+
store[key] = pos;
|
|
258
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Ignore quota / security errors.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function canonicalJson(value) {
|
|
265
|
+
return JSON.stringify(value, canonicalReplacer);
|
|
266
|
+
}
|
|
267
|
+
function canonicalReplacer(_key, val) {
|
|
268
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
269
|
+
const sorted = {};
|
|
270
|
+
// eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable
|
|
271
|
+
const keys = Object.keys(val).sort((left, right) => left.localeCompare(right));
|
|
272
|
+
for (const key of keys) {
|
|
273
|
+
sorted[key] = val[key];
|
|
274
|
+
}
|
|
275
|
+
return sorted;
|
|
276
|
+
}
|
|
277
|
+
return val;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function shouldNavigate(evt) {
|
|
281
|
+
return (evt.button === 0 &&
|
|
282
|
+
!evt.metaKey &&
|
|
283
|
+
!evt.altKey &&
|
|
284
|
+
!evt.ctrlKey &&
|
|
285
|
+
!evt.shiftKey);
|
|
286
|
+
}
|
|
287
|
+
function buildHref(router, routeName, routeParams) {
|
|
288
|
+
try {
|
|
289
|
+
const buildUrl = router.buildUrl;
|
|
290
|
+
if (buildUrl) {
|
|
291
|
+
const url = buildUrl(routeName, routeParams);
|
|
292
|
+
if (url !== undefined) {
|
|
293
|
+
return url;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return router.buildPath(routeName, routeParams);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
console.error(`[real-router] Route "${routeName}" is not defined. The element will render without an href attribute.`);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function parseTokens(value) {
|
|
304
|
+
return value ? (value.match(/\S+/g) ?? []) : [];
|
|
305
|
+
}
|
|
306
|
+
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
307
|
+
if (isActive && activeClassName) {
|
|
308
|
+
const activeTokens = parseTokens(activeClassName);
|
|
309
|
+
if (activeTokens.length === 0) {
|
|
310
|
+
return baseClassName ?? undefined;
|
|
311
|
+
}
|
|
312
|
+
if (!baseClassName) {
|
|
313
|
+
return activeTokens.join(" ");
|
|
314
|
+
}
|
|
315
|
+
const baseTokens = parseTokens(baseClassName);
|
|
316
|
+
const seen = new Set(baseTokens);
|
|
317
|
+
for (const token of activeTokens) {
|
|
318
|
+
if (!seen.has(token)) {
|
|
319
|
+
seen.add(token);
|
|
320
|
+
baseTokens.push(token);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return baseTokens.join(" ");
|
|
324
|
+
}
|
|
325
|
+
return baseClassName ?? undefined;
|
|
326
|
+
}
|
|
327
|
+
function shallowEqual(prev, next) {
|
|
328
|
+
if (Object.is(prev, next)) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (!prev || !next) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const prevKeys = Object.keys(prev);
|
|
335
|
+
if (prevKeys.length !== Object.keys(next).length) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const prevRecord = prev;
|
|
339
|
+
const nextRecord = next;
|
|
340
|
+
for (const key of prevKeys) {
|
|
341
|
+
if (!Object.is(prevRecord[key], nextRecord[key])) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
function applyLinkA11y(element) {
|
|
348
|
+
if (!element) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (element instanceof HTMLAnchorElement ||
|
|
352
|
+
element instanceof HTMLButtonElement) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!element.hasAttribute("role")) {
|
|
356
|
+
element.setAttribute("role", "link");
|
|
357
|
+
}
|
|
358
|
+
if (!element.hasAttribute("tabindex")) {
|
|
359
|
+
element.setAttribute("tabindex", "0");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
9
363
|
/** Must be called within an injection context (constructor, field initializer, runInInjectionContext). */
|
|
10
364
|
function sourceToSignal(source) {
|
|
11
365
|
const sig = signal(source.getSnapshot(), ...(ngDevMode ? [{ debugName: "sig" }] : /* istanbul ignore next */ []));
|
|
@@ -23,9 +377,9 @@ function sourceToSignal(source) {
|
|
|
23
377
|
const ROUTER = new InjectionToken("ROUTER");
|
|
24
378
|
const NAVIGATOR = new InjectionToken("NAVIGATOR");
|
|
25
379
|
const ROUTE = new InjectionToken("ROUTE");
|
|
26
|
-
function provideRealRouter(router) {
|
|
380
|
+
function provideRealRouter(router, options) {
|
|
27
381
|
const navigator = getNavigator(router);
|
|
28
|
-
|
|
382
|
+
const providers = [
|
|
29
383
|
{ provide: ROUTER, useValue: router },
|
|
30
384
|
{ provide: NAVIGATOR, useValue: navigator },
|
|
31
385
|
{
|
|
@@ -35,7 +389,17 @@ function provideRealRouter(router) {
|
|
|
35
389
|
navigator,
|
|
36
390
|
}),
|
|
37
391
|
},
|
|
38
|
-
]
|
|
392
|
+
];
|
|
393
|
+
if (options?.scrollRestoration) {
|
|
394
|
+
const scrollOpts = options.scrollRestoration;
|
|
395
|
+
providers.push(provideEnvironmentInitializer(() => {
|
|
396
|
+
const sr = createScrollRestoration(router, scrollOpts);
|
|
397
|
+
inject(DestroyRef).onDestroy(() => {
|
|
398
|
+
sr.destroy();
|
|
399
|
+
});
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return makeEnvironmentProviders(providers);
|
|
39
403
|
}
|
|
40
404
|
|
|
41
405
|
function injectOrThrow(token, fnName) {
|
|
@@ -107,6 +471,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
107
471
|
args: [{ selector: "ng-template[routeNotFound]" }]
|
|
108
472
|
}] });
|
|
109
473
|
|
|
474
|
+
class RouteSelf {
|
|
475
|
+
templateRef = inject(TemplateRef);
|
|
476
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteSelf, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
477
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.8", type: RouteSelf, isStandalone: true, selector: "ng-template[routeSelf]", ngImport: i0 });
|
|
478
|
+
}
|
|
479
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteSelf, decorators: [{
|
|
480
|
+
type: Directive,
|
|
481
|
+
args: [{ selector: "ng-template[routeSelf]" }]
|
|
482
|
+
}] });
|
|
483
|
+
|
|
110
484
|
const EMPTY_SNAPSHOT = Object.freeze({
|
|
111
485
|
route: undefined,
|
|
112
486
|
previousRoute: undefined,
|
|
@@ -114,6 +488,7 @@ const EMPTY_SNAPSHOT = Object.freeze({
|
|
|
114
488
|
class RouteView {
|
|
115
489
|
nodeName = input("", { ...(ngDevMode ? { debugName: "nodeName" } : /* istanbul ignore next */ {}), alias: "routeNode" });
|
|
116
490
|
matches = contentChildren(RouteMatch, { ...(ngDevMode ? { debugName: "matches" } : /* istanbul ignore next */ {}), descendants: true });
|
|
491
|
+
selfs = contentChildren(RouteSelf, { ...(ngDevMode ? { debugName: "selfs" } : /* istanbul ignore next */ {}), descendants: true });
|
|
117
492
|
notFounds = contentChildren(RouteNotFound, { ...(ngDevMode ? { debugName: "notFounds" } : /* istanbul ignore next */ {}), descendants: true });
|
|
118
493
|
activeTemplate = computed(() => {
|
|
119
494
|
const snapshot = this.routeState();
|
|
@@ -128,6 +503,16 @@ class RouteView {
|
|
|
128
503
|
return match.templateRef;
|
|
129
504
|
}
|
|
130
505
|
}
|
|
506
|
+
// Self has priority over NotFound. First-wins to mirror NotFound's
|
|
507
|
+
// last-wins inversion would be inconsistent with React/Preact/Solid/Vue
|
|
508
|
+
// adapters where Self is "first wins"; Angular's contentChildren returns
|
|
509
|
+
// declaration order, so picking [0] gives first-wins.
|
|
510
|
+
if (routeName === this.nodeName()) {
|
|
511
|
+
const first = this.selfs().at(0);
|
|
512
|
+
if (first) {
|
|
513
|
+
return first.templateRef;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
131
516
|
if (routeName === UNKNOWN_ROUTE) {
|
|
132
517
|
const last = this.notFounds().at(-1);
|
|
133
518
|
if (last) {
|
|
@@ -161,7 +546,7 @@ class RouteView {
|
|
|
161
546
|
});
|
|
162
547
|
}
|
|
163
548
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: RouteView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
164
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: RouteView, isStandalone: true, selector: "route-view", inputs: { nodeName: { classPropertyName: "nodeName", publicName: "routeNode", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "matches", predicate: RouteMatch, descendants: true, isSignal: true }, { propertyName: "notFounds", predicate: RouteNotFound, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
549
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: RouteView, isStandalone: true, selector: "route-view", inputs: { nodeName: { classPropertyName: "nodeName", publicName: "routeNode", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "matches", predicate: RouteMatch, descendants: true, isSignal: true }, { propertyName: "selfs", predicate: RouteSelf, descendants: true, isSignal: true }, { propertyName: "notFounds", predicate: RouteNotFound, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
165
550
|
@if (activeTemplate()) {
|
|
166
551
|
<ng-container [ngTemplateOutlet]="activeTemplate()!" />
|
|
167
552
|
}
|
|
@@ -178,7 +563,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
178
563
|
`,
|
|
179
564
|
imports: [NgTemplateOutlet],
|
|
180
565
|
}]
|
|
181
|
-
}], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
566
|
+
}], propDecorators: { nodeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeNode", required: false }] }], matches: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteMatch), { ...{ descendants: true }, isSignal: true }] }], selfs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteSelf), { ...{ descendants: true }, isSignal: true }] }], notFounds: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RouteNotFound), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
182
567
|
|
|
183
568
|
class RouterErrorBoundary {
|
|
184
569
|
errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
|
|
@@ -235,202 +620,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
235
620
|
}]
|
|
236
621
|
}], ctorParameters: () => [], propDecorators: { errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
|
|
237
622
|
|
|
238
|
-
const CLEAR_DELAY = 7000;
|
|
239
|
-
const SAFARI_READY_DELAY = 100;
|
|
240
|
-
const ANNOUNCER_ATTR = "data-real-router-announcer";
|
|
241
|
-
const INTERNAL_ROUTE_PREFIX = "@@";
|
|
242
|
-
const VISUALLY_HIDDEN = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0";
|
|
243
|
-
function createRouteAnnouncer(router, options) {
|
|
244
|
-
const prefix = options?.prefix ?? "Navigated to ";
|
|
245
|
-
const getCustomText = options?.getAnnouncementText;
|
|
246
|
-
let isInitialNavigation = true;
|
|
247
|
-
let isReady = false;
|
|
248
|
-
let isDestroyed = false;
|
|
249
|
-
let lastAnnouncedText = "";
|
|
250
|
-
let pendingText = null;
|
|
251
|
-
let clearTimeoutId;
|
|
252
|
-
const announcer = getOrCreateAnnouncer();
|
|
253
|
-
const doAnnounce = (text, h1) => {
|
|
254
|
-
lastAnnouncedText = text;
|
|
255
|
-
clearTimeout(clearTimeoutId);
|
|
256
|
-
announcer.textContent = text;
|
|
257
|
-
clearTimeoutId = setTimeout(() => {
|
|
258
|
-
announcer.textContent = "";
|
|
259
|
-
lastAnnouncedText = "";
|
|
260
|
-
}, CLEAR_DELAY);
|
|
261
|
-
manageFocus(h1);
|
|
262
|
-
};
|
|
263
|
-
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
264
|
-
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
265
|
-
// before marking the announcer "ready" — any navigation during that window is
|
|
266
|
-
// buffered in pendingText and flushed once the delay expires.
|
|
267
|
-
const safariTimeoutId = setTimeout(() => {
|
|
268
|
-
isReady = true;
|
|
269
|
-
if (pendingText !== null && !isDestroyed) {
|
|
270
|
-
const text = pendingText;
|
|
271
|
-
pendingText = null;
|
|
272
|
-
doAnnounce(text, document.querySelector("h1"));
|
|
273
|
-
}
|
|
274
|
-
}, SAFARI_READY_DELAY);
|
|
275
|
-
const unsubscribe = router.subscribe(({ route }) => {
|
|
276
|
-
if (isInitialNavigation) {
|
|
277
|
-
isInitialNavigation = false;
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
281
|
-
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
282
|
-
// Single rAF fires before the new route's template has been attached,
|
|
283
|
-
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
284
|
-
// document.title / route.name prematurely.
|
|
285
|
-
requestAnimationFrame(() => {
|
|
286
|
-
requestAnimationFrame(() => {
|
|
287
|
-
if (isDestroyed) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const h1 = document.querySelector("h1");
|
|
291
|
-
const text = resolveText(route, prefix, getCustomText, h1);
|
|
292
|
-
if (!text || text === lastAnnouncedText) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (!isReady) {
|
|
296
|
-
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
297
|
-
pendingText = text;
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
doAnnounce(text, h1);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
destroy() {
|
|
306
|
-
isDestroyed = true;
|
|
307
|
-
unsubscribe();
|
|
308
|
-
clearTimeout(clearTimeoutId);
|
|
309
|
-
clearTimeout(safariTimeoutId);
|
|
310
|
-
removeAnnouncer();
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
function getOrCreateAnnouncer() {
|
|
315
|
-
const existing = document.querySelector(`[${ANNOUNCER_ATTR}]`);
|
|
316
|
-
if (existing) {
|
|
317
|
-
return existing;
|
|
318
|
-
}
|
|
319
|
-
const element = document.createElement("div");
|
|
320
|
-
element.setAttribute("style", VISUALLY_HIDDEN);
|
|
321
|
-
element.setAttribute("aria-live", "assertive");
|
|
322
|
-
element.setAttribute("aria-atomic", "true");
|
|
323
|
-
element.setAttribute(ANNOUNCER_ATTR, "");
|
|
324
|
-
document.body.prepend(element);
|
|
325
|
-
return element;
|
|
326
|
-
}
|
|
327
|
-
function removeAnnouncer() {
|
|
328
|
-
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
329
|
-
}
|
|
330
|
-
function resolveText(route, prefix, getCustomText, h1) {
|
|
331
|
-
if (getCustomText) {
|
|
332
|
-
return getCustomText(route);
|
|
333
|
-
}
|
|
334
|
-
const h1Text = h1?.textContent.trim() ?? "";
|
|
335
|
-
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
|
|
336
|
-
? ""
|
|
337
|
-
: route.name;
|
|
338
|
-
const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
|
|
339
|
-
return `${prefix}${rawText}`;
|
|
340
|
-
}
|
|
341
|
-
function manageFocus(h1) {
|
|
342
|
-
if (!h1) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (!h1.hasAttribute("tabindex")) {
|
|
346
|
-
h1.setAttribute("tabindex", "-1");
|
|
347
|
-
}
|
|
348
|
-
h1.focus({ preventScroll: true });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function shouldNavigate(evt) {
|
|
352
|
-
return (evt.button === 0 &&
|
|
353
|
-
!evt.metaKey &&
|
|
354
|
-
!evt.altKey &&
|
|
355
|
-
!evt.ctrlKey &&
|
|
356
|
-
!evt.shiftKey);
|
|
357
|
-
}
|
|
358
|
-
function buildHref(router, routeName, routeParams) {
|
|
359
|
-
try {
|
|
360
|
-
const buildUrl = router.buildUrl;
|
|
361
|
-
if (buildUrl) {
|
|
362
|
-
const url = buildUrl(routeName, routeParams);
|
|
363
|
-
if (url !== undefined) {
|
|
364
|
-
return url;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return router.buildPath(routeName, routeParams);
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
console.error(`[real-router] Route "${routeName}" is not defined. The element will render without an href attribute.`);
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
function parseTokens(value) {
|
|
375
|
-
return value ? (value.match(/\S+/g) ?? []) : [];
|
|
376
|
-
}
|
|
377
|
-
function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
378
|
-
if (isActive && activeClassName) {
|
|
379
|
-
const activeTokens = parseTokens(activeClassName);
|
|
380
|
-
if (activeTokens.length === 0) {
|
|
381
|
-
return baseClassName ?? undefined;
|
|
382
|
-
}
|
|
383
|
-
if (!baseClassName) {
|
|
384
|
-
return activeTokens.join(" ");
|
|
385
|
-
}
|
|
386
|
-
const baseTokens = parseTokens(baseClassName);
|
|
387
|
-
const seen = new Set(baseTokens);
|
|
388
|
-
for (const token of activeTokens) {
|
|
389
|
-
if (!seen.has(token)) {
|
|
390
|
-
seen.add(token);
|
|
391
|
-
baseTokens.push(token);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return baseTokens.join(" ");
|
|
395
|
-
}
|
|
396
|
-
return baseClassName ?? undefined;
|
|
397
|
-
}
|
|
398
|
-
function shallowEqual(prev, next) {
|
|
399
|
-
if (Object.is(prev, next)) {
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
if (!prev || !next) {
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
const prevKeys = Object.keys(prev);
|
|
406
|
-
if (prevKeys.length !== Object.keys(next).length) {
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
409
|
-
const prevRecord = prev;
|
|
410
|
-
const nextRecord = next;
|
|
411
|
-
for (const key of prevKeys) {
|
|
412
|
-
if (!Object.is(prevRecord[key], nextRecord[key])) {
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
function applyLinkA11y(element) {
|
|
419
|
-
if (!element) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
if (element instanceof HTMLAnchorElement ||
|
|
423
|
-
element instanceof HTMLButtonElement) {
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (!element.hasAttribute("role")) {
|
|
427
|
-
element.setAttribute("role", "link");
|
|
428
|
-
}
|
|
429
|
-
if (!element.hasAttribute("tabindex")) {
|
|
430
|
-
element.setAttribute("tabindex", "0");
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
623
|
class NavigationAnnouncer {
|
|
435
624
|
announcer = createRouteAnnouncer(injectRouter());
|
|
436
625
|
constructor() {
|
|
@@ -563,5 +752,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
563
752
|
* Generated bundle index. Do not edit.
|
|
564
753
|
*/
|
|
565
754
|
|
|
566
|
-
export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
|
|
755
|
+
export { NAVIGATOR, NavigationAnnouncer, ROUTE, ROUTER, RealLink, RealLinkActive, RouteMatch, RouteNotFound, RouteSelf, RouteView, RouterErrorBoundary, injectIsActiveRoute, injectNavigator, injectRoute, injectRouteNode, injectRouteUtils, injectRouter, injectRouterTransition, provideRealRouter, sourceToSignal };
|
|
567
756
|
//# sourceMappingURL=real-router-angular.mjs.map
|