@real-router/angular 0.9.0 → 0.11.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 +24 -3
- package/dist/README.md +24 -3
- package/dist/fesm2022/real-router-angular-ssr.mjs +9 -9
- package/dist/fesm2022/real-router-angular-ssr.mjs.map +1 -1
- package/dist/fesm2022/real-router-angular.mjs +561 -41
- package/dist/fesm2022/real-router-angular.mjs.map +1 -1
- package/dist/types/real-router-angular.d.ts +62 -0
- package/dist/types/real-router-angular.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/dom-utils/index.ts +4 -0
- package/src/dom-utils/scroll-restore.ts +107 -12
- package/src/dom-utils/scroll-spy.ts +688 -0
- package/src/internal/install.ts +15 -2
- package/src/providers.ts +13 -1
- package/src/providersFactory.ts +21 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, DestroyRef, ApplicationRef, signal, InjectionToken, provideEnvironmentInitializer, makeEnvironmentProviders, makeStateKey, REQUEST, provideAppInitializer, TransferState, assertInInjectionContext, effect, input, TemplateRef, Directive, contentChildren, computed, Component, output, ElementRef } from '@angular/core';
|
|
3
3
|
import { getNavigator, UNKNOWN_ROUTE } from '@real-router/core';
|
|
4
|
-
import { createRouteSource, createRouteNodeSource,
|
|
4
|
+
import { getTransitionSource, createRouteSource, createRouteNodeSource, createActiveRouteSource, createDismissableError } from '@real-router/sources';
|
|
5
5
|
import { cloneRouter, getPluginApi } from '@real-router/core/api';
|
|
6
6
|
import { hydrateRouter, serializeRouterState } from '@real-router/core/utils';
|
|
7
7
|
import { getRouteUtils, startsWithSegment } from '@real-router/route-utils';
|
|
8
8
|
import { NgTemplateOutlet } from '@angular/common';
|
|
9
9
|
|
|
10
|
-
const NOOP_INSTANCE$
|
|
10
|
+
const NOOP_INSTANCE$4 = Object.freeze({
|
|
11
11
|
destroy: () => {
|
|
12
12
|
/* no-op */
|
|
13
13
|
},
|
|
@@ -35,7 +35,7 @@ const NOOP_INSTANCE$3 = Object.freeze({
|
|
|
35
35
|
*/
|
|
36
36
|
function createDirectionTracker(router) {
|
|
37
37
|
if (typeof document === "undefined") {
|
|
38
|
-
return NOOP_INSTANCE$
|
|
38
|
+
return NOOP_INSTANCE$4;
|
|
39
39
|
}
|
|
40
40
|
let popstateFlag = false;
|
|
41
41
|
document.documentElement.dataset.navDirection = "forward";
|
|
@@ -70,7 +70,7 @@ const SAFARI_READY_DELAY = 100;
|
|
|
70
70
|
const ANNOUNCER_ATTR = "data-real-router-announcer";
|
|
71
71
|
const INTERNAL_ROUTE_PREFIX = "@@";
|
|
72
72
|
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";
|
|
73
|
-
const NOOP_INSTANCE$
|
|
73
|
+
const NOOP_INSTANCE$3 = Object.freeze({
|
|
74
74
|
destroy: () => {
|
|
75
75
|
/* no-op */
|
|
76
76
|
},
|
|
@@ -86,7 +86,7 @@ function createRouteAnnouncer(router, options) {
|
|
|
86
86
|
// bootstrap. Closes review-2026-05-10 §5.10 ⛔ "NavigationAnnouncer
|
|
87
87
|
// SSR mode" MED.
|
|
88
88
|
if (typeof document === "undefined") {
|
|
89
|
-
return NOOP_INSTANCE$
|
|
89
|
+
return NOOP_INSTANCE$3;
|
|
90
90
|
}
|
|
91
91
|
const prefix = options?.prefix ?? "Navigated to ";
|
|
92
92
|
const getCustomText = options?.getAnnouncementText;
|
|
@@ -228,14 +228,21 @@ function manageFocus(h1) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
const DEFAULT_STORAGE_KEY = "real-router:scroll";
|
|
231
|
-
|
|
231
|
+
// Bounded retry budget for resolving a late-mounting scroll container on the
|
|
232
|
+
// restore path. A per-route container (e.g. an `overflow:auto` div rendered
|
|
233
|
+
// only on one route) can be committed to the DOM a few frames after the
|
|
234
|
+
// navigation settles — heavier routes paint later than the subscribe's rAF.
|
|
235
|
+
// ~10 frames (≈160ms at 60fps) comfortably covers a React commit of a large
|
|
236
|
+
// route without being perceptible. See the doc-block on `restorePos`.
|
|
237
|
+
const RESTORE_RETRY_FRAMES = 10;
|
|
238
|
+
const NOOP_INSTANCE$2 = Object.freeze({
|
|
232
239
|
destroy: () => {
|
|
233
240
|
/* no-op */
|
|
234
241
|
},
|
|
235
242
|
});
|
|
236
243
|
function createScrollRestoration(router, options) {
|
|
237
244
|
if (typeof globalThis.window === "undefined") {
|
|
238
|
-
return NOOP_INSTANCE$
|
|
245
|
+
return NOOP_INSTANCE$2;
|
|
239
246
|
}
|
|
240
247
|
const mode = options?.mode ?? "restore";
|
|
241
248
|
// mode "native" = utility does nothing. Don't flip history.scrollRestoration,
|
|
@@ -245,7 +252,7 @@ function createScrollRestoration(router, options) {
|
|
|
245
252
|
// === "manual"` — utility's "native" leaves the DOM property at "auto" so
|
|
246
253
|
// the browser is in charge.)
|
|
247
254
|
if (mode === "native") {
|
|
248
|
-
return NOOP_INSTANCE$
|
|
255
|
+
return NOOP_INSTANCE$2;
|
|
249
256
|
}
|
|
250
257
|
const anchorEnabled = options?.anchorScrolling ?? true;
|
|
251
258
|
const getContainer = options?.scrollContainer;
|
|
@@ -309,6 +316,65 @@ function createScrollRestoration(router, options) {
|
|
|
309
316
|
globalThis.scrollTo({ top, left: 0, behavior });
|
|
310
317
|
}
|
|
311
318
|
};
|
|
319
|
+
// Restore path (back / traverse / reload). Unlike `writePos`, this tolerates a
|
|
320
|
+
// scroll container that both MOUNTS and LAYS OUT a few frames AFTER the
|
|
321
|
+
// navigation settles.
|
|
322
|
+
//
|
|
323
|
+
// The capture-side `readPos` always runs against an already-mounted DOM (the
|
|
324
|
+
// route being left). On restore the target route — and its container — is
|
|
325
|
+
// still being committed by the view layer. The subscribe callback schedules a
|
|
326
|
+
// single rAF; for a heavy route (e.g. a long virtual list) the framework's
|
|
327
|
+
// commit can land AFTER that frame. Two distinct failures follow, each losing
|
|
328
|
+
// the saved position (Scenario 6 e2e, reproduced under CI's slower runner):
|
|
329
|
+
//
|
|
330
|
+
// 1. Container not mounted yet → `getContainer()` is `null`, the scroll
|
|
331
|
+
// silently falls back to `window`, which on a container-only route has
|
|
332
|
+
// nothing to scroll.
|
|
333
|
+
// 2. Container mounted but its content not laid out yet → `scrollHeight`
|
|
334
|
+
// is still small, so a single `scrollTo({ top })` clamps short of the
|
|
335
|
+
// saved position and never re-applies once layout grows.
|
|
336
|
+
//
|
|
337
|
+
// With no `scrollContainer` getter the target is always `window`, present
|
|
338
|
+
// from the first frame — restore in a single shot (unchanged behaviour). When
|
|
339
|
+
// a getter is configured we cannot tell "this route legitimately uses window"
|
|
340
|
+
// from "the container is still mounting", so re-apply the scroll on every
|
|
341
|
+
// frame for a bounded budget: window as a fallback while the container is
|
|
342
|
+
// absent (harmless clamp on container routes), the container itself once it
|
|
343
|
+
// appears. For instant restores we stop early the moment the position sticks;
|
|
344
|
+
// smooth restores animate asynchronously, so they run the full budget. The
|
|
345
|
+
// frame budget is the hard backstop against an unreachable target (saved
|
|
346
|
+
// position taller than the restored content).
|
|
347
|
+
const restorePos = (top) => {
|
|
348
|
+
if (!getContainer) {
|
|
349
|
+
globalThis.scrollTo({ top, left: 0, behavior });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let frames = 0;
|
|
353
|
+
const attempt = () => {
|
|
354
|
+
if (destroyed) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const element = getContainer();
|
|
358
|
+
if (element) {
|
|
359
|
+
element.scrollTo({ top, left: 0, behavior });
|
|
360
|
+
// Instant restore landed within rounding tolerance → done; no point
|
|
361
|
+
// re-applying. Smooth restore never matches synchronously, so let it
|
|
362
|
+
// ride the budget.
|
|
363
|
+
if (behavior !== "smooth" && Math.abs(element.scrollTop - top) <= 1) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
globalThis.scrollTo({ top, left: 0, behavior });
|
|
369
|
+
}
|
|
370
|
+
if (frames >= RESTORE_RETRY_FRAMES) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
frames += 1;
|
|
374
|
+
requestAnimationFrame(attempt);
|
|
375
|
+
};
|
|
376
|
+
attempt();
|
|
377
|
+
};
|
|
312
378
|
const scrollToHashOrTop = (route) => {
|
|
313
379
|
// URL plugin path (#532): `state.context.url.hash` is the source of truth
|
|
314
380
|
// when one of the URL plugins (browser-plugin / navigation-plugin) is
|
|
@@ -384,24 +450,42 @@ function createScrollRestoration(router, options) {
|
|
|
384
450
|
putPos(prevKey, readPos());
|
|
385
451
|
}
|
|
386
452
|
}
|
|
387
|
-
// Single rAF so DOM is committed before we read anchors / write scroll.
|
|
388
|
-
// Guard against destroy() racing with the callback.
|
|
389
453
|
requestAnimationFrame(() => {
|
|
390
454
|
if (destroyed) {
|
|
391
455
|
return;
|
|
392
456
|
}
|
|
393
|
-
if (mode === "top"
|
|
457
|
+
if (mode === "top") {
|
|
394
458
|
scrollToHashOrTop(route);
|
|
395
459
|
return;
|
|
396
460
|
}
|
|
397
|
-
|
|
461
|
+
// Restore branches (reload, back/traverse) MUST be evaluated before the
|
|
462
|
+
// replace-skip below. Since #657 lifted `replace` into TransitionMeta, a
|
|
463
|
+
// history TRAVERSAL (back/forward) under navigation-plugin carries
|
|
464
|
+
// `transition.replace === true` — a traversal reuses an existing history
|
|
465
|
+
// entry, which is replace-shaped at the history level. If the replace-skip
|
|
466
|
+
// ran first it would swallow every back/forward navigation and restore
|
|
467
|
+
// would never fire (the Scenario 6 e2e regression). Genuine in-place
|
|
468
|
+
// replaces (`router.navigate({ replace: true })`, navigateToNotFound) are
|
|
469
|
+
// not traversals and fall through to the skip below.
|
|
470
|
+
//
|
|
471
|
+
// Both arms of each check are required: `transition.reload` only fires for
|
|
472
|
+
// programmatic `router.navigate({reload:true})`. F5 under navigation-plugin
|
|
473
|
+
// primes `nav.navigationType === "reload"` via #531 getActivationType but
|
|
474
|
+
// leaves opts.reload undefined, so dropping the plugin arm would regress F5
|
|
475
|
+
// scroll-restore. Browser-plugin's F5 is not covered (no priming, out of
|
|
476
|
+
// scope).
|
|
477
|
+
if (route.transition.reload || nav?.navigationType === "reload") {
|
|
478
|
+
const key = safeKeyOf(route);
|
|
479
|
+
restorePos(key === null ? 0 : (loadStore()[key] ?? 0));
|
|
398
480
|
return;
|
|
399
481
|
}
|
|
400
|
-
if (nav
|
|
401
|
-
nav.navigationType === "traverse" ||
|
|
402
|
-
nav.navigationType === "reload") {
|
|
482
|
+
if (nav?.direction === "back" || nav?.navigationType === "traverse") {
|
|
403
483
|
const key = safeKeyOf(route);
|
|
404
|
-
|
|
484
|
+
restorePos(key === null ? 0 : (loadStore()[key] ?? 0));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Genuine in-place replace (not a traversal) — leave scroll untouched.
|
|
488
|
+
if (route.transition.replace || nav?.navigationType === "replace") {
|
|
405
489
|
return;
|
|
406
490
|
}
|
|
407
491
|
scrollToHashOrTop(route);
|
|
@@ -541,6 +625,424 @@ function canonicalReplacer(_key, val) {
|
|
|
541
625
|
return val;
|
|
542
626
|
}
|
|
543
627
|
|
|
628
|
+
const NOOP_INSTANCE$1 = Object.freeze({
|
|
629
|
+
destroy: () => {
|
|
630
|
+
/* no-op */
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
// Hardcoded internals (RFC §5.1 — promote only with evidence).
|
|
634
|
+
const RAF_DEBOUNCE_MS = 150;
|
|
635
|
+
const MUTATION_DEBOUNCE_MS = 250;
|
|
636
|
+
const COOLDOWN_TIMEOUT_MS = 500;
|
|
637
|
+
const DEFAULT_ROOT_MARGIN = "-20% 0px -60% 0px";
|
|
638
|
+
const getUrlContext = (state) => state.context?.url;
|
|
639
|
+
// =============================================================================
|
|
640
|
+
// Picker — pure, no state. RFC §5.2 selection rule.
|
|
641
|
+
// =============================================================================
|
|
642
|
+
// Pick the anchor closest to the active zone top in viewport coordinates.
|
|
643
|
+
// `entry.rootBounds.top` already reflects `rootMargin` (per W3C IO spec
|
|
644
|
+
// §3.3) — for `rootMargin: "-20% 0px -60% 0px"` it returns 20% of root
|
|
645
|
+
// height, for `"-50% 0px -50% 0px"` it returns the center, etc. Distance
|
|
646
|
+
// = boundingClientRect.top − zoneTop in viewport pixels: positive = anchor
|
|
647
|
+
// below zone top (just entered), negative = anchor above zone top (body
|
|
648
|
+
// crossing zone from above). We prefer smallest non-negative; fall back to
|
|
649
|
+
// least-negative when no entry has crossed yet.
|
|
650
|
+
// Falls back to zoneTop = 0 when rootBounds is null (cross-origin roots,
|
|
651
|
+
// unit tests). Single pass — handles `Iterable` so flushes can pass
|
|
652
|
+
// `Map.values()` directly without realising the array.
|
|
653
|
+
const pickTopmost = (entries) => {
|
|
654
|
+
let bestPositive = null;
|
|
655
|
+
let bestPositiveDist = Number.POSITIVE_INFINITY;
|
|
656
|
+
let bestNegative = null;
|
|
657
|
+
let bestNegativeDist = Number.NEGATIVE_INFINITY;
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
if (!entry.isIntersecting) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const zoneTop = entry.rootBounds?.top ?? 0;
|
|
663
|
+
const distance = entry.boundingClientRect.top - zoneTop;
|
|
664
|
+
if (distance >= 0) {
|
|
665
|
+
if (distance < bestPositiveDist) {
|
|
666
|
+
bestPositive = entry;
|
|
667
|
+
bestPositiveDist = distance;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else if (distance > bestNegativeDist) {
|
|
671
|
+
bestNegative = entry;
|
|
672
|
+
bestNegativeDist = distance;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return bestPositive ?? bestNegative;
|
|
676
|
+
};
|
|
677
|
+
const createUrlPluginDetector = (router, onMissing) => {
|
|
678
|
+
let detectionUnsub = null;
|
|
679
|
+
const verify = (state) => {
|
|
680
|
+
const context = state.context;
|
|
681
|
+
if (context && context.url === undefined) {
|
|
682
|
+
console.warn("[real-router] scroll-spy: state.context.url is not claimed. " +
|
|
683
|
+
"Spy requires browser-plugin or navigation-plugin. Disabling.");
|
|
684
|
+
onMissing();
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
const peekState = router.getState();
|
|
688
|
+
if (peekState) {
|
|
689
|
+
verify(peekState);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
// Re-entry guard: `router.subscribe` MAY invoke the callback synchronously
|
|
693
|
+
// from inside `.subscribe(...)` before the function returns. In that case
|
|
694
|
+
// `detectionUnsub` is still `null` when the callback fires. Without this
|
|
695
|
+
// boolean, a hypothetical multi-fire would double-warn.
|
|
696
|
+
let detectionConsumed = false;
|
|
697
|
+
detectionUnsub = router.subscribe(({ route }) => {
|
|
698
|
+
if (detectionConsumed) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
detectionConsumed = true;
|
|
702
|
+
verify(route);
|
|
703
|
+
detectionUnsub?.();
|
|
704
|
+
detectionUnsub = null;
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
destroy() {
|
|
709
|
+
detectionUnsub?.();
|
|
710
|
+
detectionUnsub = null;
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
};
|
|
714
|
+
const createCooldown = (getContainer) => {
|
|
715
|
+
let active = false;
|
|
716
|
+
let timeout = null;
|
|
717
|
+
let listenerContainer = null;
|
|
718
|
+
let listener = null;
|
|
719
|
+
const clear = () => {
|
|
720
|
+
if (timeout !== null) {
|
|
721
|
+
clearTimeout(timeout);
|
|
722
|
+
timeout = null;
|
|
723
|
+
}
|
|
724
|
+
if (listener) {
|
|
725
|
+
const target = listenerContainer ?? globalThis;
|
|
726
|
+
target.removeEventListener("scrollend", listener);
|
|
727
|
+
}
|
|
728
|
+
listener = null;
|
|
729
|
+
listenerContainer = null;
|
|
730
|
+
active = false;
|
|
731
|
+
};
|
|
732
|
+
return {
|
|
733
|
+
get active() {
|
|
734
|
+
return active;
|
|
735
|
+
},
|
|
736
|
+
start() {
|
|
737
|
+
// Reset rather than stack timers if cooldown is already active.
|
|
738
|
+
clear();
|
|
739
|
+
active = true;
|
|
740
|
+
const lift = () => {
|
|
741
|
+
clear();
|
|
742
|
+
};
|
|
743
|
+
listener = lift;
|
|
744
|
+
listenerContainer = getContainer();
|
|
745
|
+
const target = listenerContainer ?? globalThis;
|
|
746
|
+
target.addEventListener("scrollend", lift, { once: true });
|
|
747
|
+
timeout = setTimeout(lift, COOLDOWN_TIMEOUT_MS);
|
|
748
|
+
},
|
|
749
|
+
destroy() {
|
|
750
|
+
clear();
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
const createDebouncer = (callback, trailingMs) => {
|
|
755
|
+
let raf = null;
|
|
756
|
+
let timeout = null;
|
|
757
|
+
return {
|
|
758
|
+
schedule() {
|
|
759
|
+
if (raf !== null) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
raf = requestAnimationFrame(() => {
|
|
763
|
+
raf = null;
|
|
764
|
+
if (timeout !== null) {
|
|
765
|
+
clearTimeout(timeout);
|
|
766
|
+
}
|
|
767
|
+
timeout = setTimeout(() => {
|
|
768
|
+
timeout = null;
|
|
769
|
+
callback();
|
|
770
|
+
}, trailingMs);
|
|
771
|
+
});
|
|
772
|
+
},
|
|
773
|
+
destroy() {
|
|
774
|
+
if (raf !== null) {
|
|
775
|
+
cancelAnimationFrame(raf);
|
|
776
|
+
raf = null;
|
|
777
|
+
}
|
|
778
|
+
if (timeout !== null) {
|
|
779
|
+
clearTimeout(timeout);
|
|
780
|
+
timeout = null;
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
const createObserverPair = (selector, rootMargin, getContainer, onIntersection, onInvalidSelector, isStopped) => {
|
|
786
|
+
const observed = new Set();
|
|
787
|
+
// Latest IO entry per target — accumulated across batches. IO delivers
|
|
788
|
+
// entries only for targets whose intersection state CHANGED (W3C IO
|
|
789
|
+
// §3.2.1), so a fast scroll that lands two callbacks inside the same
|
|
790
|
+
// debounce window must merge by target, not overwrite. Entries are
|
|
791
|
+
// dropped from the map when their target leaves the DOM (see `reconcile`)
|
|
792
|
+
// and on `destroy()`.
|
|
793
|
+
const pending = new Map();
|
|
794
|
+
let duplicateIdWarned = false;
|
|
795
|
+
let mutationTimer = null;
|
|
796
|
+
const handleIntersection = (entries) => {
|
|
797
|
+
// Defensive: IO callback may fire AFTER `destroy()` if a queued event
|
|
798
|
+
// was already scheduled by the browser before `disconnect()`. Cheap
|
|
799
|
+
// belt-and-suspenders.
|
|
800
|
+
if (isStopped()) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
for (const entry of entries) {
|
|
804
|
+
pending.set(entry.target, entry);
|
|
805
|
+
}
|
|
806
|
+
onIntersection();
|
|
807
|
+
};
|
|
808
|
+
const io = new IntersectionObserver(handleIntersection, {
|
|
809
|
+
root: getContainer(),
|
|
810
|
+
rootMargin,
|
|
811
|
+
threshold: 0,
|
|
812
|
+
});
|
|
813
|
+
const observeMatches = () => {
|
|
814
|
+
const scope = getContainer() ?? document;
|
|
815
|
+
let candidates;
|
|
816
|
+
try {
|
|
817
|
+
candidates = scope.querySelectorAll(selector);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
onInvalidSelector();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const seenIds = new Set();
|
|
824
|
+
for (const element of candidates) {
|
|
825
|
+
// Detect duplicate ids once (RFC §7.7). The DOM permits duplicate ids
|
|
826
|
+
// even though it is a markup bug; the spy keeps working but picks the
|
|
827
|
+
// first one deterministically via the topmost-visible rule.
|
|
828
|
+
const id = element.id;
|
|
829
|
+
if (id && !duplicateIdWarned) {
|
|
830
|
+
if (seenIds.has(id)) {
|
|
831
|
+
duplicateIdWarned = true;
|
|
832
|
+
console.warn(`[real-router] scroll-spy: duplicate id "${id}" observed. ` +
|
|
833
|
+
"Selection picks the topmost visible match deterministically.");
|
|
834
|
+
}
|
|
835
|
+
seenIds.add(id);
|
|
836
|
+
}
|
|
837
|
+
if (observed.has(element)) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
io.observe(element);
|
|
841
|
+
observed.add(element);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
const reconcile = () => {
|
|
845
|
+
// Drop observed elements that left the DOM. Avoids observer holding
|
|
846
|
+
// strong refs to detached nodes. Also drop their accumulated entry so
|
|
847
|
+
// stale "was intersecting" state for a removed node cannot be picked
|
|
848
|
+
// by `pickTopmost` after the node is gone.
|
|
849
|
+
for (const element of observed) {
|
|
850
|
+
if (!element.isConnected) {
|
|
851
|
+
io.unobserve(element);
|
|
852
|
+
observed.delete(element);
|
|
853
|
+
pending.delete(element);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
observeMatches();
|
|
857
|
+
};
|
|
858
|
+
observeMatches();
|
|
859
|
+
// MutationObserver targets the scroll container (or document.body for
|
|
860
|
+
// window viewport). `childList: true, subtree: true` catches structural
|
|
861
|
+
// changes; `attributes: true, attributeFilter: ["id"]` catches anchor
|
|
862
|
+
// id renames (typical for client-rendered docs).
|
|
863
|
+
const mutationTarget = getContainer() ?? document.body;
|
|
864
|
+
const mo = new MutationObserver(() => {
|
|
865
|
+
if (mutationTimer !== null) {
|
|
866
|
+
clearTimeout(mutationTimer);
|
|
867
|
+
}
|
|
868
|
+
mutationTimer = setTimeout(() => {
|
|
869
|
+
mutationTimer = null;
|
|
870
|
+
reconcile();
|
|
871
|
+
}, MUTATION_DEBOUNCE_MS);
|
|
872
|
+
});
|
|
873
|
+
mo.observe(mutationTarget, {
|
|
874
|
+
childList: true,
|
|
875
|
+
subtree: true,
|
|
876
|
+
attributes: true,
|
|
877
|
+
attributeFilter: ["id"],
|
|
878
|
+
});
|
|
879
|
+
return {
|
|
880
|
+
pending,
|
|
881
|
+
destroy() {
|
|
882
|
+
io.disconnect();
|
|
883
|
+
mo.disconnect();
|
|
884
|
+
if (mutationTimer !== null) {
|
|
885
|
+
clearTimeout(mutationTimer);
|
|
886
|
+
mutationTimer = null;
|
|
887
|
+
}
|
|
888
|
+
observed.clear();
|
|
889
|
+
pending.clear();
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
};
|
|
893
|
+
// =============================================================================
|
|
894
|
+
// Main: compositional wiring
|
|
895
|
+
// =============================================================================
|
|
896
|
+
function createScrollSpy(router, options) {
|
|
897
|
+
// SSR guard (RFC §7.5) — return early without warnings.
|
|
898
|
+
if (typeof document === "undefined") {
|
|
899
|
+
return NOOP_INSTANCE$1;
|
|
900
|
+
}
|
|
901
|
+
// Feature-detect IntersectionObserver — no polyfill ships (RFC §4).
|
|
902
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
903
|
+
return NOOP_INSTANCE$1;
|
|
904
|
+
}
|
|
905
|
+
const { selector } = options;
|
|
906
|
+
// Empty selector → disabled. Documented opt-out for conditional enabling
|
|
907
|
+
// (RFC §5.4 `scrollSpy={{ selector: enable ? "[id]" : "" }}`).
|
|
908
|
+
if (!selector) {
|
|
909
|
+
return NOOP_INSTANCE$1;
|
|
910
|
+
}
|
|
911
|
+
const rootMargin = options.rootMargin ?? DEFAULT_ROOT_MARGIN;
|
|
912
|
+
const getContainer = options.scrollContainer;
|
|
913
|
+
const resolveContainer = () => getContainer?.() ?? null;
|
|
914
|
+
// Shared lifecycle flags (Oracle Q1 — `silenced` has multiple unrelated
|
|
915
|
+
// triggers; Oracle Q3 — `selfEmitting` synchronously bracketed around
|
|
916
|
+
// `router.navigate()` cannot cleanly extract). Kept in main scope.
|
|
917
|
+
let destroyed = false;
|
|
918
|
+
let silenced = false;
|
|
919
|
+
let selfEmitting = false;
|
|
920
|
+
const isStopped = () => silenced || destroyed;
|
|
921
|
+
// Symmetric late-binding (Oracle Q2): declare `flush` as nullable, wire
|
|
922
|
+
// debouncer + observers, then assign the real implementation. Reads as
|
|
923
|
+
// intentional wiring rather than accidental closure capture ordering.
|
|
924
|
+
// The `flush?.()` call below safely no-ops if a callback somehow fires
|
|
925
|
+
// before assignment (impossible in practice — IO/debounce are async).
|
|
926
|
+
let flush = null;
|
|
927
|
+
const transitionSource = getTransitionSource(router);
|
|
928
|
+
const detector = createUrlPluginDetector(router, () => {
|
|
929
|
+
silenced = true;
|
|
930
|
+
});
|
|
931
|
+
const cooldown = createCooldown(resolveContainer);
|
|
932
|
+
const debouncer = createDebouncer(() => {
|
|
933
|
+
flush?.();
|
|
934
|
+
}, RAF_DEBOUNCE_MS);
|
|
935
|
+
const observers = createObserverPair(selector, rootMargin, resolveContainer, () => {
|
|
936
|
+
debouncer.schedule();
|
|
937
|
+
}, () => {
|
|
938
|
+
if (silenced) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
silenced = true;
|
|
942
|
+
console.warn(`[real-router] scroll-spy: invalid selector "${selector}". Disabling.`);
|
|
943
|
+
}, isStopped);
|
|
944
|
+
flush = () => {
|
|
945
|
+
if (destroyed || silenced) {
|
|
946
|
+
observers.pending.clear();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
// Gate-skipped flushes keep `pendingEntries` populated — the merged
|
|
950
|
+
// state is still the best-known snapshot, and the next non-gated flush
|
|
951
|
+
// consumes it. Clearing under a gate would re-introduce the overwrite
|
|
952
|
+
// bug for any anchor whose intersection state did not change during
|
|
953
|
+
// the gate window.
|
|
954
|
+
if (transitionSource.getSnapshot().isTransitioning) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (cooldown.active) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (observers.pending.size === 0) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Successful flush consumes the merged snapshot. We clear so that the
|
|
964
|
+
// next debounce window starts fresh; an anchor that is still
|
|
965
|
+
// intersecting will only stay observable if IO emits another event for
|
|
966
|
+
// it (which it does whenever the anchor's intersection state actually
|
|
967
|
+
// changes). Skipping the clear here would leak state from one user-
|
|
968
|
+
// perceived "scroll stop" into the next.
|
|
969
|
+
const picked = pickTopmost(observers.pending.values());
|
|
970
|
+
observers.pending.clear();
|
|
971
|
+
if (!picked) {
|
|
972
|
+
// No anchor visible / above zone — preserve last hash (RFC §10 #5).
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const newHash = picked.target.id;
|
|
976
|
+
if (!newHash) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const state = router.getState();
|
|
980
|
+
if (!state) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const currentHash = getUrlContext(state)?.hash ?? "";
|
|
984
|
+
if (newHash === currentHash) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
// Emit the same-route same-params hash-only transition. URL plugin
|
|
988
|
+
// writes `state.context.url.hash = newHash` + `hashChanged = true` in
|
|
989
|
+
// its `onTransitionSuccess` claim.
|
|
990
|
+
const opts = {
|
|
991
|
+
hash: newHash,
|
|
992
|
+
replace: true,
|
|
993
|
+
force: true,
|
|
994
|
+
hashChange: true,
|
|
995
|
+
};
|
|
996
|
+
// Self-emit guard (RFC §5.2): set synchronously around our own
|
|
997
|
+
// `router.navigate()` so the `router.subscribe` callback skips the
|
|
998
|
+
// cooldown setup for spy-emitted transitions — otherwise spy would
|
|
999
|
+
// rate-limit itself to ≤ 2 emits/s, contradicting the ≤ 10/s benchmark
|
|
1000
|
+
// target. Test coupling (Q8): preserve exact `.catch(noop).finally(reset)`
|
|
1001
|
+
// chain — migrating to `try/finally` over `await router.navigate(...)`
|
|
1002
|
+
// changes microtask schedule and breaks "spy continues after rejection".
|
|
1003
|
+
selfEmitting = true;
|
|
1004
|
+
router
|
|
1005
|
+
.navigate(state.name, state.params, opts)
|
|
1006
|
+
.catch(() => {
|
|
1007
|
+
// Fire-and-forget — suppress expected rejections (concurrent
|
|
1008
|
+
// navigate, router stopped, etc.) consistent with `<Link>` adapter
|
|
1009
|
+
// patterns.
|
|
1010
|
+
})
|
|
1011
|
+
.finally(() => {
|
|
1012
|
+
selfEmitting = false;
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
// Cooldown setup on user-driven hash transitions. Spy's own emits are
|
|
1016
|
+
// distinguished via the synchronous `selfEmitting` flag (see `flush`).
|
|
1017
|
+
const unsubscribeRouter = router.subscribe(({ route }) => {
|
|
1018
|
+
if (selfEmitting) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (getUrlContext(route)?.hashChanged) {
|
|
1022
|
+
cooldown.start();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
return {
|
|
1026
|
+
destroy() {
|
|
1027
|
+
if (destroyed) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
destroyed = true;
|
|
1031
|
+
// Unsubscribe FIRST to prevent late-arriving router transition
|
|
1032
|
+
// callback from calling `cooldown.start()` on a half-destroyed
|
|
1033
|
+
// instance. Without this ordering, a transition with `hashChanged:
|
|
1034
|
+
// true` firing between subsystem teardown and `unsubscribeRouter()`
|
|
1035
|
+
// would re-install a 500ms timer that survives `destroy()`. Verified
|
|
1036
|
+
// via Oracle review (Q5/Q7).
|
|
1037
|
+
unsubscribeRouter();
|
|
1038
|
+
observers.destroy();
|
|
1039
|
+
debouncer.destroy();
|
|
1040
|
+
cooldown.destroy();
|
|
1041
|
+
detector.destroy();
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
544
1046
|
const NOOP_INSTANCE = Object.freeze({
|
|
545
1047
|
destroy: () => {
|
|
546
1048
|
/* no-op */
|
|
@@ -908,6 +1410,13 @@ function installScrollRestoration(options) {
|
|
|
908
1410
|
sr.destroy();
|
|
909
1411
|
});
|
|
910
1412
|
}
|
|
1413
|
+
function installScrollSpy(options) {
|
|
1414
|
+
const router = inject(ROUTER);
|
|
1415
|
+
const spy = createScrollSpy(router, options);
|
|
1416
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1417
|
+
spy.destroy();
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
911
1420
|
function installViewTransitions() {
|
|
912
1421
|
const router = inject(ROUTER);
|
|
913
1422
|
// Feature-detect `document.startViewTransition` once at install time. The
|
|
@@ -1000,6 +1509,12 @@ function provideRealRouter(router, options) {
|
|
|
1000
1509
|
installScrollRestoration(scrollOpts);
|
|
1001
1510
|
}));
|
|
1002
1511
|
}
|
|
1512
|
+
if (options?.scrollSpy && options.scrollSpy.selector !== "") {
|
|
1513
|
+
const spyOpts = options.scrollSpy;
|
|
1514
|
+
providers.push(provideEnvironmentInitializer(() => {
|
|
1515
|
+
installScrollSpy(spyOpts);
|
|
1516
|
+
}));
|
|
1517
|
+
}
|
|
1003
1518
|
if (options?.viewTransitions === true) {
|
|
1004
1519
|
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1005
1520
|
}
|
|
@@ -1048,7 +1563,7 @@ const ROUTER_STATE_KEY = makeStateKey("@real-router/angular:ssrState");
|
|
|
1048
1563
|
* @returns `EnvironmentProviders` to spread into `ApplicationConfig.providers`.
|
|
1049
1564
|
*/
|
|
1050
1565
|
function provideRealRouterFactory(options) {
|
|
1051
|
-
const { baseRouter, plugins, deps, scrollRestoration, viewTransitions } = options;
|
|
1566
|
+
const { baseRouter, plugins, deps, scrollRestoration, scrollSpy, viewTransitions, } = options;
|
|
1052
1567
|
const providers = [
|
|
1053
1568
|
{
|
|
1054
1569
|
provide: ROUTER,
|
|
@@ -1145,6 +1660,11 @@ function provideRealRouterFactory(options) {
|
|
|
1145
1660
|
installScrollRestoration(scrollRestoration);
|
|
1146
1661
|
}));
|
|
1147
1662
|
}
|
|
1663
|
+
if (scrollSpy && scrollSpy.selector !== "") {
|
|
1664
|
+
providers.push(provideEnvironmentInitializer(() => {
|
|
1665
|
+
installScrollSpy(scrollSpy);
|
|
1666
|
+
}));
|
|
1667
|
+
}
|
|
1148
1668
|
if (viewTransitions === true) {
|
|
1149
1669
|
providers.push(provideEnvironmentInitializer(installViewTransitions));
|
|
1150
1670
|
}
|
|
@@ -1413,30 +1933,30 @@ function injectRouteEnter(handler, options) {
|
|
|
1413
1933
|
class RouteMatch {
|
|
1414
1934
|
routeMatch = input.required(...(ngDevMode ? [{ debugName: "routeMatch" }] : /* istanbul ignore next */ []));
|
|
1415
1935
|
templateRef = inject(TemplateRef);
|
|
1416
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1417
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
1936
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteMatch, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1937
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: RouteMatch, isStandalone: true, selector: "ng-template[routeMatch]", inputs: { routeMatch: { classPropertyName: "routeMatch", publicName: "routeMatch", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
1418
1938
|
}
|
|
1419
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1939
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteMatch, decorators: [{
|
|
1420
1940
|
type: Directive,
|
|
1421
1941
|
args: [{ selector: "ng-template[routeMatch]" }]
|
|
1422
1942
|
}], propDecorators: { routeMatch: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeMatch", required: true }] }] } });
|
|
1423
1943
|
|
|
1424
1944
|
class RouteNotFound {
|
|
1425
1945
|
templateRef = inject(TemplateRef);
|
|
1426
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1427
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.
|
|
1946
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteNotFound, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1947
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: RouteNotFound, isStandalone: true, selector: "ng-template[routeNotFound]", ngImport: i0 });
|
|
1428
1948
|
}
|
|
1429
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1949
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteNotFound, decorators: [{
|
|
1430
1950
|
type: Directive,
|
|
1431
1951
|
args: [{ selector: "ng-template[routeNotFound]" }]
|
|
1432
1952
|
}] });
|
|
1433
1953
|
|
|
1434
1954
|
class RouteSelf {
|
|
1435
1955
|
templateRef = inject(TemplateRef);
|
|
1436
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1437
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.
|
|
1956
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteSelf, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1957
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.13", type: RouteSelf, isStandalone: true, selector: "ng-template[routeSelf]", ngImport: i0 });
|
|
1438
1958
|
}
|
|
1439
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1959
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteSelf, decorators: [{
|
|
1440
1960
|
type: Directive,
|
|
1441
1961
|
args: [{ selector: "ng-template[routeSelf]" }]
|
|
1442
1962
|
}] });
|
|
@@ -1562,14 +2082,14 @@ class RouteView {
|
|
|
1562
2082
|
}));
|
|
1563
2083
|
});
|
|
1564
2084
|
}
|
|
1565
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1566
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2085
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2086
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", 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: `
|
|
1567
2087
|
@if (activeTemplate()) {
|
|
1568
2088
|
<ng-container [ngTemplateOutlet]="activeTemplate()!" />
|
|
1569
2089
|
}
|
|
1570
2090
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1571
2091
|
}
|
|
1572
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2092
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouteView, decorators: [{
|
|
1573
2093
|
type: Component,
|
|
1574
2094
|
args: [{
|
|
1575
2095
|
selector: "route-view",
|
|
@@ -1615,8 +2135,8 @@ class RouterErrorBoundary {
|
|
|
1615
2135
|
}
|
|
1616
2136
|
});
|
|
1617
2137
|
}
|
|
1618
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1619
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2138
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouterErrorBoundary, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2139
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: RouterErrorBoundary, isStandalone: true, selector: "router-error-boundary", inputs: { errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onError: "onError" }, ngImport: i0, template: `
|
|
1620
2140
|
<ng-content />
|
|
1621
2141
|
@if (errorContext() && errorTemplate()) {
|
|
1622
2142
|
<ng-container
|
|
@@ -1626,7 +2146,7 @@ class RouterErrorBoundary {
|
|
|
1626
2146
|
}
|
|
1627
2147
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1628
2148
|
}
|
|
1629
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2149
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RouterErrorBoundary, decorators: [{
|
|
1630
2150
|
type: Component,
|
|
1631
2151
|
args: [{
|
|
1632
2152
|
selector: "router-error-boundary",
|
|
@@ -1650,10 +2170,10 @@ class NavigationAnnouncer {
|
|
|
1650
2170
|
this.announcer.destroy();
|
|
1651
2171
|
});
|
|
1652
2172
|
}
|
|
1653
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1654
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.
|
|
2173
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NavigationAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2174
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.13", type: NavigationAnnouncer, isStandalone: true, selector: "navigation-announcer", ngImport: i0, template: "", isInline: true });
|
|
1655
2175
|
}
|
|
1656
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: NavigationAnnouncer, decorators: [{
|
|
1657
2177
|
type: Component,
|
|
1658
2178
|
args: [{
|
|
1659
2179
|
selector: "navigation-announcer",
|
|
@@ -1742,10 +2262,10 @@ class RealLink {
|
|
|
1742
2262
|
}
|
|
1743
2263
|
this.prevActiveClass = activeClass;
|
|
1744
2264
|
}
|
|
1745
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1746
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
2265
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2266
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: RealLink, isStandalone: true, selector: "a[realLink]", inputs: { routeName: { classPropertyName: "routeName", publicName: "routeName", isSignal: true, isRequired: false, transformFunction: null }, routeParams: { classPropertyName: "routeParams", publicName: "routeParams", isSignal: true, isRequired: false, transformFunction: null }, routeOptions: { classPropertyName: "routeOptions", publicName: "routeOptions", isSignal: true, isRequired: false, transformFunction: null }, activeClassName: { classPropertyName: "activeClassName", publicName: "activeClassName", isSignal: true, isRequired: false, transformFunction: null }, activeStrict: { classPropertyName: "activeStrict", publicName: "activeStrict", isSignal: true, isRequired: false, transformFunction: null }, ignoreQueryParams: { classPropertyName: "ignoreQueryParams", publicName: "ignoreQueryParams", isSignal: true, isRequired: false, transformFunction: null }, hash: { classPropertyName: "hash", publicName: "hash", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)" } }, ngImport: i0 });
|
|
1747
2267
|
}
|
|
1748
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2268
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLink, decorators: [{
|
|
1749
2269
|
type: Directive,
|
|
1750
2270
|
args: [{
|
|
1751
2271
|
selector: "a[realLink]",
|
|
@@ -1794,10 +2314,10 @@ class RealLinkActive {
|
|
|
1794
2314
|
}
|
|
1795
2315
|
this.element.classList.toggle(className, this.isActive());
|
|
1796
2316
|
}
|
|
1797
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1798
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
2317
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLinkActive, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2318
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: RealLinkActive, isStandalone: true, selector: "[realLinkActive]", inputs: { realLinkActive: { classPropertyName: "realLinkActive", publicName: "realLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routeName: { classPropertyName: "routeName", publicName: "routeName", isSignal: true, isRequired: false, transformFunction: null }, routeParams: { classPropertyName: "routeParams", publicName: "routeParams", isSignal: true, isRequired: false, transformFunction: null }, activeStrict: { classPropertyName: "activeStrict", publicName: "activeStrict", isSignal: true, isRequired: false, transformFunction: null }, ignoreQueryParams: { classPropertyName: "ignoreQueryParams", publicName: "ignoreQueryParams", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
1799
2319
|
}
|
|
1800
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2320
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RealLinkActive, decorators: [{
|
|
1801
2321
|
type: Directive,
|
|
1802
2322
|
args: [{ selector: "[realLinkActive]" }]
|
|
1803
2323
|
}], ctorParameters: () => [], propDecorators: { realLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "realLinkActive", required: false }] }], routeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeName", required: false }] }], routeParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "routeParams", required: false }] }], activeStrict: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeStrict", required: false }] }], ignoreQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreQueryParams", required: false }] }] } });
|