@mmstack/router-core 21.0.2 → 21.0.4

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.
@@ -1,63 +1,191 @@
1
- import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, computed, Injectable, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
3
1
  import * as i1 from '@angular/router';
4
- import { EventType, Router, PRIMARY_OUTLET, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
5
- import { mutable, mapArray, until, elementVisibility, toWritable } from '@mmstack/primitives';
2
+ import { PRIMARY_OUTLET, EventType, Router, createUrlTreeFromSnapshot, UrlTree, RouterLink, RouterLinkWithHref, ActivatedRoute } from '@angular/router';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, computed, Injectable, InjectionToken, input, booleanAttribute, output, untracked, effect, HostListener, Directive, isSignal, linkedSignal } from '@angular/core';
6
5
  import { toSignal } from '@angular/core/rxjs-interop';
7
6
  import { filter, map } from 'rxjs/operators';
7
+ import { mutable, mapArray, until, elementVisibility, toWritable } from '@mmstack/primitives';
8
8
  import { Subject, EMPTY, filter as filter$1, take, switchMap, finalize } from 'rxjs';
9
9
  import { Title } from '@angular/platform-browser';
10
10
 
11
- /**
12
- * @internal
13
- */
14
- const token$1 = new InjectionToken('MMSTACK_BREADCRUMB_CONFIG');
15
- /**
16
- * Provides configuration for the breadcrumb system.
17
- * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
18
- * @see BreadcrumbConfig
19
- * @example
20
- * ```typescript
21
- * // In your app.module.ts or a standalone component's providers:
22
- * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
23
- * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
24
- *
25
- * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
26
- * // return (leaf: ResolvedLeafRoute): string => {
27
- * // // Example: Prioritize a 'navTitle' data property
28
- * // if (leaf.route.data?.['navTitle']) {
29
- * // return leaf.route.data['navTitle'];
30
- * // }
31
- * // // Fallback to a default mechanism
32
- * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
33
- * // };
34
- * // };
35
- *
36
- * export const appConfig = [
37
- * // ...rest
38
- * provideBreadcrumbConfig({
39
- * generation: customLabelStrategy, // or 'manual' to disable auto-generation
40
- * }),
41
- * ]
42
- * ```
43
- */
44
- function provideBreadcrumbConfig(config) {
45
- return {
46
- provide: token$1,
47
- useValue: {
48
- ...config,
49
- },
11
+ function parsePathSegment$1(segmentString) {
12
+ const parts = segmentString.split(';');
13
+ const pathPart = parts[0];
14
+ const matrixParams = {};
15
+ for (let i = 1; i < parts.length; i++) {
16
+ const [key, value = 'true'] = parts[i].split('=');
17
+ if (key) {
18
+ matrixParams[key] = value;
19
+ }
20
+ }
21
+ return { pathPart, matrixParams };
22
+ }
23
+ function createBasePredicate(path) {
24
+ const partPredicates = path
25
+ .split('/')
26
+ .filter((part) => !!part.trim())
27
+ .map((configSegmentString) => {
28
+ const { pathPart: configPathPart, matrixParams: configMatrixParams } = parsePathSegment$1(configSegmentString);
29
+ let singlePathPartPredicate;
30
+ if (configPathPart.startsWith(':')) {
31
+ singlePathPartPredicate = () => true;
32
+ }
33
+ else {
34
+ singlePathPartPredicate = (linkSegmentPathPart) => linkSegmentPathPart === configPathPart;
35
+ }
36
+ const configSegmentHasMatrixParams = Object.keys(configMatrixParams).length > 0;
37
+ return (linkSegmentString) => {
38
+ const { pathPart: linkPathPart, matrixParams: linkMatrixParams } = parsePathSegment$1(linkSegmentString);
39
+ if (!singlePathPartPredicate(linkPathPart)) {
40
+ return false;
41
+ }
42
+ if (!configSegmentHasMatrixParams) {
43
+ return true;
44
+ }
45
+ return Object.entries(configMatrixParams).every(([key, value]) => Object.prototype.hasOwnProperty.call(linkMatrixParams, key) &&
46
+ linkMatrixParams[key] === value);
47
+ };
48
+ });
49
+ return (path) => {
50
+ const linkPathOnly = path.split(/[?#]/).at(0) ?? '';
51
+ if (!linkPathOnly && partPredicates.length > 0)
52
+ return false;
53
+ if (!linkPathOnly && partPredicates.length === 0)
54
+ return true;
55
+ const parts = linkPathOnly.split('/').filter((part) => !!part.trim());
56
+ if (parts.length < partPredicates.length)
57
+ return false;
58
+ return parts.every((seg, idx) => {
59
+ const pred = partPredicates.at(idx);
60
+ if (!pred)
61
+ return true;
62
+ return pred(seg);
63
+ });
50
64
  };
51
65
  }
52
- /**
53
- * @internal
54
- */
55
- function injectBreadcrumbConfig() {
56
- return (inject(token$1, {
57
- optional: true,
58
- }) ?? {});
66
+ function singleSegmentMatches(configSegment, linkSegment) {
67
+ if (configSegment.pathPart === ':') {
68
+ return true;
69
+ }
70
+ else if (configSegment.pathPart !== linkSegment.pathPart) {
71
+ return false;
72
+ }
73
+ const configMatrix = configSegment.matrixParams;
74
+ const linkMatrix = linkSegment.matrixParams;
75
+ for (const key in configMatrix) {
76
+ if (!Object.prototype.hasOwnProperty.call(linkMatrix, key) ||
77
+ linkMatrix[key] !== configMatrix[key]) {
78
+ return false;
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+ function matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx) {
84
+ if (configIdx === configSegments.length) {
85
+ return linkIdx === linkSegments.length;
86
+ }
87
+ if (linkIdx === linkSegments.length) {
88
+ for (let i = configIdx; i < configSegments.length; i++) {
89
+ if (configSegments[i].pathPart !== '**') {
90
+ return false;
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+ const currentConfigSegment = configSegments[configIdx];
96
+ if (currentConfigSegment.pathPart === '**') {
97
+ if (matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx)) {
98
+ return true;
99
+ }
100
+ if (linkIdx < linkSegments.length) {
101
+ if (matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx + 1)) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ }
107
+ else {
108
+ if (linkIdx < linkSegments.length &&
109
+ singleSegmentMatches(currentConfigSegment, linkSegments[linkIdx])) {
110
+ return matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx + 1);
111
+ }
112
+ return false;
113
+ }
114
+ }
115
+ function createWildcardPredicate(path) {
116
+ const configSegments = path
117
+ .split('/')
118
+ .filter((p) => !!p.trim())
119
+ .map((segment) => parsePathSegment$1(segment));
120
+ return (linkPath) => {
121
+ const linkPathOnly = linkPath.split(/[?#]/).at(0) ?? '';
122
+ const linkSegments = linkPathOnly
123
+ .split('/')
124
+ .filter((p) => !!p.trim())
125
+ .map((segment) => parsePathSegment$1(segment));
126
+ return matchSegmentsRecursive(configSegments, linkSegments, 0, 0);
127
+ };
128
+ }
129
+ function createRoutePredicate(path) {
130
+ return path.includes('**')
131
+ ? createWildcardPredicate(path)
132
+ : createBasePredicate(path);
59
133
  }
60
134
 
135
+ // The following functions are adapted from ngx-quicklink,
136
+ // (https://github.com/mgechev/ngx-quicklink)
137
+ // Copyright (c) Minko Gechev and contributors, licensed under the MIT License.
138
+ function isPrimaryRoute(route) {
139
+ return route.outlet === PRIMARY_OUTLET || !route.outlet;
140
+ }
141
+ const findPath = (config, route) => {
142
+ const configQueue = config.slice();
143
+ const parent = new Map();
144
+ const visited = new Set();
145
+ while (configQueue.length) {
146
+ const el = configQueue.shift();
147
+ if (!el) {
148
+ continue;
149
+ }
150
+ visited.add(el);
151
+ if (el === route) {
152
+ break;
153
+ }
154
+ (el.children || []).forEach((childRoute) => {
155
+ if (!visited.has(childRoute)) {
156
+ parent.set(childRoute, el);
157
+ configQueue.push(childRoute);
158
+ }
159
+ });
160
+ const lazyRoutes = el._loadedRoutes || [];
161
+ if (Array.isArray(lazyRoutes)) {
162
+ lazyRoutes.forEach((lazyRoute) => {
163
+ if (lazyRoute && !visited.has(lazyRoute)) {
164
+ parent.set(lazyRoute, el);
165
+ configQueue.push(lazyRoute);
166
+ }
167
+ });
168
+ }
169
+ }
170
+ let path = '';
171
+ let currentRoute = route;
172
+ while (currentRoute) {
173
+ const currentPath = currentRoute.path || '';
174
+ if (isPrimaryRoute(currentRoute)) {
175
+ path = `/${currentPath}${path}`;
176
+ }
177
+ else {
178
+ path = `/(${currentRoute.outlet}:${currentPath})${path}`;
179
+ }
180
+ currentRoute = parent.get(currentRoute);
181
+ }
182
+ let normalizedPath = path.replaceAll(/\/+/g, '/');
183
+ if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
184
+ normalizedPath = normalizedPath.slice(0, -1);
185
+ }
186
+ return normalizedPath;
187
+ };
188
+
61
189
  /**
62
190
  * Type guard to check if a Router Event is a NavigationEnd event.
63
191
  * @internal
@@ -95,8 +223,9 @@ function isNavigationEnd(e) {
95
223
  * }
96
224
  * ```
97
225
  */
98
- function url() {
99
- const router = inject(Router);
226
+ function url(router) {
227
+ if (!router)
228
+ router = inject(Router);
100
229
  return toSignal(router.events.pipe(filter(isNavigationEnd), map((e) => e.urlAfterRedirects)), {
101
230
  initialValue: router.url,
102
231
  });
@@ -139,15 +268,15 @@ function leafRoutes() {
139
268
  const leafRoutes = computed(() => {
140
269
  currentUrl();
141
270
  return getLeafRoutes(router.routerState.snapshot);
142
- }, ...(ngDevMode ? [{ debugName: "leafRoutes" }] : []));
271
+ }, ...(ngDevMode ? [{ debugName: "leafRoutes" }] : /* istanbul ignore next */ []));
143
272
  return leafRoutes;
144
273
  }
145
274
  class RouteLeafStore {
146
275
  leaves = leafRoutes();
147
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RouteLeafStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
148
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RouteLeafStore, providedIn: 'root' });
276
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
277
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, providedIn: 'root' });
149
278
  }
150
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RouteLeafStore, decorators: [{
279
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RouteLeafStore, decorators: [{
151
280
  type: Injectable,
152
281
  args: [{
153
282
  providedIn: 'root',
@@ -158,6 +287,15 @@ function injectLeafRoutes() {
158
287
  return store.leaves;
159
288
  }
160
289
 
290
+ function injectSnapshotPathResolver() {
291
+ const router = inject(Router);
292
+ return (route) => {
293
+ const segments = route.pathFromRoot.flatMap((snap) => snap.routeConfig?.path ?? []);
294
+ const joinedSegments = segments.filter(Boolean).join('/');
295
+ return router.serializeUrl(router.parseUrl(joinedSegments));
296
+ };
297
+ }
298
+
161
299
  /**
162
300
  * @internal
163
301
  */
@@ -187,6 +325,56 @@ function isInternalBreadcrumb(breadcrumb) {
187
325
  return !!breadcrumb[INTERNAL_BREADCRUMB_SYMBOL];
188
326
  }
189
327
 
328
+ /**
329
+ * @internal
330
+ */
331
+ const token$2 = new InjectionToken('@mmstack/router-core:breadcrumb-config');
332
+ /**
333
+ * Provides configuration for the breadcrumb system.
334
+ * @param config - A partial `BreadcrumbConfig` object with the desired settings. *
335
+ * @see BreadcrumbConfig
336
+ * @example
337
+ * ```typescript
338
+ * // In your app.module.ts or a standalone component's providers:
339
+ * // import { provideBreadcrumbConfig } from './breadcrumb.config'; // Adjust path
340
+ * // import { ResolvedLeafRoute } from './breadcrumb.type'; // Adjust path
341
+ *
342
+ * // const customLabelStrategy: GenerateBreadcrumbFn = () => {
343
+ * // return (leaf: ResolvedLeafRoute): string => {
344
+ * // // Example: Prioritize a 'navTitle' data property
345
+ * // if (leaf.route.data?.['navTitle']) {
346
+ * // return leaf.route.data['navTitle'];
347
+ * // }
348
+ * // // Fallback to a default mechanism
349
+ * // return leaf.route.title || leaf.segment.resolved || 'Unnamed';
350
+ * // };
351
+ * // };
352
+ *
353
+ * export const appConfig = [
354
+ * // ...rest
355
+ * provideBreadcrumbConfig({
356
+ * generation: customLabelStrategy, // or 'manual' to disable auto-generation
357
+ * }),
358
+ * ]
359
+ * ```
360
+ */
361
+ function provideBreadcrumbConfig(config) {
362
+ return {
363
+ provide: token$2,
364
+ useValue: {
365
+ ...config,
366
+ },
367
+ };
368
+ }
369
+ /**
370
+ * @internal
371
+ */
372
+ function injectBreadcrumbConfig() {
373
+ return (inject(token$2, {
374
+ optional: true,
375
+ }) ?? {});
376
+ }
377
+
190
378
  function uppercaseFirst(str) {
191
379
  const lcs = str.toLowerCase();
192
380
  return lcs.charAt(0).toUpperCase() + lcs.slice(1);
@@ -195,7 +383,7 @@ function removeMatrixAndQueryParams(path) {
195
383
  const [cleanPath] = path.split(';');
196
384
  return cleanPath.split('?')[0];
197
385
  }
198
- function parsePathSegment$1(pathSegment) {
386
+ function parsePathSegment(pathSegment) {
199
387
  return pathSegment
200
388
  .split('/')
201
389
  .flatMap((part) => part.split('.'))
@@ -206,319 +394,133 @@ function parsePathSegment$1(pathSegment) {
206
394
  function generateLabel(leaf) {
207
395
  const title = leaf.route.title ?? leaf.route.data?.['title'];
208
396
  if (title && typeof title === 'string')
209
- return title;
210
- if (leaf.segment.path.includes(':'))
211
- return leaf.segment.resolved;
212
- return parsePathSegment$1(leaf.segment.path);
213
- }
214
- function autoGenerateBreadcrumb(id, leaf, autoGenerateFn) {
215
- const label = computed(() => autoGenerateFn()(leaf()), ...(ngDevMode ? [{ debugName: "label" }] : []));
216
- return createInternalBreadcrumb({
217
- id,
218
- label,
219
- ariaLabel: label,
220
- link: computed(() => leaf().link),
221
- }, computed(() => leaf().route.data?.['skipBreadcrumb'] !== true &&
222
- id !== '' &&
223
- id !== '/' &&
224
- leaf().segment.path !== '' &&
225
- leaf().segment.path !== '/' &&
226
- !leaf().segment.path.endsWith('/') &&
227
- !!label()));
228
- }
229
- function injectGenerateLabelFn() {
230
- const { generation } = injectBreadcrumbConfig();
231
- if (typeof generation !== 'function')
232
- return computed(() => generateLabel);
233
- const provided = generation();
234
- return computed(() => provided);
235
- }
236
- function injectIsManual() {
237
- return injectBreadcrumbConfig().generation === 'manual';
238
- }
239
- function exposeActiveSignal(crumbSignal, manual) {
240
- const active = manual
241
- ? computed(() => {
242
- const crumb = crumbSignal();
243
- return (isInternalBreadcrumb(crumb) &&
244
- getBreadcrumbInternals(crumb).registered &&
245
- getBreadcrumbInternals(crumb).active());
246
- })
247
- : computed(() => {
248
- const crumb = crumbSignal();
249
- if (!isInternalBreadcrumb(crumb))
250
- return true;
251
- return getBreadcrumbInternals(crumb).active();
252
- });
253
- const sig = crumbSignal;
254
- sig.active = active;
255
- return sig;
256
- }
257
- class BreadcrumbStore {
258
- map = mutable(new Map());
259
- isManual = injectIsManual();
260
- autoGenerateLabelFn = injectGenerateLabelFn();
261
- leafRoutes = injectLeafRoutes();
262
- all = mapArray(this.leafRoutes, (leaf) => {
263
- const stableId = computed(() => leaf().path, ...(ngDevMode ? [{ debugName: "stableId" }] : []));
264
- return exposeActiveSignal(computed(() => {
265
- const id = stableId();
266
- const found = this.map().get(id);
267
- if (!found)
268
- return autoGenerateBreadcrumb(id, leaf, this.autoGenerateLabelFn);
269
- if (!id.includes(':'))
270
- return found;
271
- return {
272
- ...found,
273
- link: computed(() => leaf().link),
274
- };
275
- }, {
276
- equal: (a, b) => a.id === b.id,
277
- }), this.isManual);
278
- }, {
279
- equal: (a, b) => a.link === b.link,
280
- });
281
- crumbs = computed(() => this.all().filter((c) => c.active()), ...(ngDevMode ? [{ debugName: "crumbs" }] : []));
282
- unwrapped = computed(() => this.crumbs().map((c) => c()), ...(ngDevMode ? [{ debugName: "unwrapped" }] : []));
283
- register(breadcrumb) {
284
- this.map.inline((m) => m.set(breadcrumb.id, breadcrumb));
285
- }
286
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
287
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, providedIn: 'root' });
288
- }
289
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BreadcrumbStore, decorators: [{
290
- type: Injectable,
291
- args: [{
292
- providedIn: 'root',
293
- }]
294
- }] });
295
- /**
296
- * Injects and provides access to a reactive list of breadcrumbs.
297
- *
298
- * The breadcrumbs are ordered and reflect the current active navigation path.
299
- * @see Breadcrumb
300
- * @returns `Signal<Breadcrumb[]>`
301
- *
302
- * @example
303
- * ```typescript
304
- * @Component({
305
- * selector: 'app-breadcrumbs',
306
- * template: `
307
- * <nav aria-label="breadcrumb">
308
- * <ol>
309
- * @for (crumb of breadcrumbs(); track crumb.id) {
310
- * <li>
311
- * <a [href]="crumb.link()" [attr.aria-label]="crumb.ariaLabel()">{{ crumb.label() }}</a>
312
- * </li>
313
- * }
314
- * </ol>
315
- * </nav>
316
- * `
317
- * })
318
- * export class MyBreadcrumbsComponent {
319
- * breadcrumbs = injectBreadcrumbs();
320
- * }
321
- * ```
322
- */
323
- function injectBreadcrumbs() {
324
- const store = inject(BreadcrumbStore);
325
- return store.unwrapped;
326
- }
327
-
328
- function parsePathSegment(segmentString) {
329
- const parts = segmentString.split(';');
330
- const pathPart = parts[0];
331
- const matrixParams = {};
332
- for (let i = 1; i < parts.length; i++) {
333
- const [key, value = 'true'] = parts[i].split('=');
334
- if (key) {
335
- matrixParams[key] = value;
336
- }
337
- }
338
- return { pathPart, matrixParams };
339
- }
340
- function createBasePredicate(path) {
341
- const partPredicates = path
342
- .split('/')
343
- .filter((part) => !!part.trim())
344
- .map((configSegmentString) => {
345
- const { pathPart: configPathPart, matrixParams: configMatrixParams } = parsePathSegment(configSegmentString);
346
- let singlePathPartPredicate;
347
- if (configPathPart.startsWith(':')) {
348
- singlePathPartPredicate = () => true;
349
- }
350
- else {
351
- singlePathPartPredicate = (linkSegmentPathPart) => linkSegmentPathPart === configPathPart;
352
- }
353
- const configSegmentHasMatrixParams = Object.keys(configMatrixParams).length > 0;
354
- return (linkSegmentString) => {
355
- const { pathPart: linkPathPart, matrixParams: linkMatrixParams } = parsePathSegment(linkSegmentString);
356
- if (!singlePathPartPredicate(linkPathPart)) {
357
- return false;
358
- }
359
- if (!configSegmentHasMatrixParams) {
360
- return true;
361
- }
362
- return Object.entries(configMatrixParams).every(([key, value]) => Object.prototype.hasOwnProperty.call(linkMatrixParams, key) &&
363
- linkMatrixParams[key] === value);
364
- };
365
- });
366
- return (path) => {
367
- const linkPathOnly = path.split(/[?#]/).at(0) ?? '';
368
- if (!linkPathOnly && partPredicates.length > 0)
369
- return false;
370
- if (!linkPathOnly && partPredicates.length === 0)
371
- return true;
372
- const parts = linkPathOnly.split('/').filter((part) => !!part.trim());
373
- if (parts.length < partPredicates.length)
374
- return false;
375
- return parts.every((seg, idx) => {
376
- const pred = partPredicates.at(idx);
377
- if (!pred)
378
- return true;
379
- return pred(seg);
380
- });
381
- };
382
- }
383
- function singleSegmentMatches(configSegment, linkSegment) {
384
- if (configSegment.pathPart === ':') {
385
- return true;
386
- }
387
- else if (configSegment.pathPart !== linkSegment.pathPart) {
388
- return false;
389
- }
390
- const configMatrix = configSegment.matrixParams;
391
- const linkMatrix = linkSegment.matrixParams;
392
- for (const key in configMatrix) {
393
- if (!Object.prototype.hasOwnProperty.call(linkMatrix, key) ||
394
- linkMatrix[key] !== configMatrix[key]) {
395
- return false;
396
- }
397
- }
398
- return true;
399
- }
400
- function matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx) {
401
- if (configIdx === configSegments.length) {
402
- return linkIdx === linkSegments.length;
403
- }
404
- if (linkIdx === linkSegments.length) {
405
- for (let i = configIdx; i < configSegments.length; i++) {
406
- if (configSegments[i].pathPart !== '**') {
407
- return false;
408
- }
409
- }
410
- return true;
411
- }
412
- const currentConfigSegment = configSegments[configIdx];
413
- if (currentConfigSegment.pathPart === '**') {
414
- if (matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx)) {
415
- return true;
416
- }
417
- if (linkIdx < linkSegments.length) {
418
- if (matchSegmentsRecursive(configSegments, linkSegments, configIdx, linkIdx + 1)) {
419
- return true;
420
- }
421
- }
422
- return false;
423
- }
424
- else {
425
- if (linkIdx < linkSegments.length &&
426
- singleSegmentMatches(currentConfigSegment, linkSegments[linkIdx])) {
427
- return matchSegmentsRecursive(configSegments, linkSegments, configIdx + 1, linkIdx + 1);
428
- }
429
- return false;
430
- }
431
- }
432
- function createWildcardPredicate(path) {
433
- const configSegments = path
434
- .split('/')
435
- .filter((p) => !!p.trim())
436
- .map((segment) => parsePathSegment(segment));
437
- return (linkPath) => {
438
- const linkPathOnly = linkPath.split(/[?#]/).at(0) ?? '';
439
- const linkSegments = linkPathOnly
440
- .split('/')
441
- .filter((p) => !!p.trim())
442
- .map((segment) => parsePathSegment(segment));
443
- return matchSegmentsRecursive(configSegments, linkSegments, 0, 0);
444
- };
397
+ return title;
398
+ if (leaf.segment.path.includes(':'))
399
+ return leaf.segment.resolved;
400
+ return parsePathSegment(leaf.segment.path);
445
401
  }
446
- function createRoutePredicate(path) {
447
- return path.includes('**')
448
- ? createWildcardPredicate(path)
449
- : createBasePredicate(path);
402
+ function autoGenerateBreadcrumb(id, leaf, autoGenerateFn) {
403
+ const label = computed(() => autoGenerateFn()(leaf()), ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
404
+ return createInternalBreadcrumb({
405
+ id,
406
+ label,
407
+ ariaLabel: label,
408
+ link: computed(() => leaf().link),
409
+ }, computed(() => leaf().route.data?.['skipBreadcrumb'] !== true &&
410
+ id !== '' &&
411
+ id !== '/' &&
412
+ leaf().segment.path !== '' &&
413
+ leaf().segment.path !== '/' &&
414
+ !leaf().segment.path.endsWith('/') &&
415
+ !!label()), false);
450
416
  }
451
-
452
- // The following functions are adapted from ngx-quicklink,
453
- // (https://github.com/mgechev/ngx-quicklink)
454
- // Copyright (c) Minko Gechev and contributors, licensed under the MIT License.
455
- function isPrimaryRoute(route) {
456
- return route.outlet === PRIMARY_OUTLET || !route.outlet;
417
+ function injectGenerateLabelFn() {
418
+ const { generation } = injectBreadcrumbConfig();
419
+ if (typeof generation !== 'function')
420
+ return computed(() => generateLabel);
421
+ const provided = generation();
422
+ return computed(() => provided);
457
423
  }
458
- const findPath = (config, route) => {
459
- const configQueue = config.slice();
460
- const parent = new Map();
461
- const visited = new Set();
462
- while (configQueue.length) {
463
- const el = configQueue.shift();
464
- if (!el) {
465
- continue;
466
- }
467
- visited.add(el);
468
- if (el === route) {
469
- break;
470
- }
471
- (el.children || []).forEach((childRoute) => {
472
- if (!visited.has(childRoute)) {
473
- parent.set(childRoute, el);
474
- configQueue.push(childRoute);
475
- }
424
+ function injectIsManual() {
425
+ return injectBreadcrumbConfig().generation === 'manual';
426
+ }
427
+ function exposeActiveSignal(crumbSignal, manual) {
428
+ const active = manual
429
+ ? computed(() => {
430
+ const crumb = crumbSignal();
431
+ return (isInternalBreadcrumb(crumb) &&
432
+ getBreadcrumbInternals(crumb).registered &&
433
+ getBreadcrumbInternals(crumb).active());
434
+ })
435
+ : computed(() => {
436
+ const crumb = crumbSignal();
437
+ if (!isInternalBreadcrumb(crumb))
438
+ return true;
439
+ return getBreadcrumbInternals(crumb).active();
476
440
  });
477
- const lazyRoutes = el._loadedRoutes || [];
478
- if (Array.isArray(lazyRoutes)) {
479
- lazyRoutes.forEach((lazyRoute) => {
480
- if (lazyRoute && !visited.has(lazyRoute)) {
481
- parent.set(lazyRoute, el);
482
- configQueue.push(lazyRoute);
483
- }
484
- });
485
- }
486
- }
487
- let path = '';
488
- let currentRoute = route;
489
- while (currentRoute) {
490
- const currentPath = currentRoute.path || '';
491
- if (isPrimaryRoute(currentRoute)) {
492
- path = `/${currentPath}${path}`;
493
- }
494
- else {
495
- path = `/(${currentRoute.outlet}:${currentPath})${path}`;
496
- }
497
- currentRoute = parent.get(currentRoute);
498
- }
499
- let normalizedPath = path.replaceAll(/\/+/g, '/');
500
- if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
501
- normalizedPath = normalizedPath.slice(0, -1);
441
+ const sig = crumbSignal;
442
+ sig.active = active;
443
+ return sig;
444
+ }
445
+ class BreadcrumbStore {
446
+ map = mutable(new Map());
447
+ isManual = injectIsManual();
448
+ autoGenerateLabelFn = injectGenerateLabelFn();
449
+ leafRoutes = injectLeafRoutes();
450
+ all = mapArray(this.leafRoutes, (leaf) => {
451
+ const stableId = computed(() => leaf().path, ...(ngDevMode ? [{ debugName: "stableId" }] : /* istanbul ignore next */ []));
452
+ return exposeActiveSignal(computed(() => {
453
+ const id = stableId();
454
+ const found = this.map().get(id);
455
+ if (!found)
456
+ return autoGenerateBreadcrumb(id, leaf, this.autoGenerateLabelFn);
457
+ if (!id.includes(':'))
458
+ return found;
459
+ return {
460
+ ...found,
461
+ link: computed(() => leaf().link),
462
+ };
463
+ }, {
464
+ equal: (a, b) => a.id === b.id,
465
+ }), this.isManual);
466
+ }, {
467
+ equal: (a, b) => a.link === b.link,
468
+ });
469
+ crumbs = computed(() => this.all().filter((c) => c.active()), ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
470
+ unwrapped = computed(() => this.crumbs().map((c) => c()), ...(ngDevMode ? [{ debugName: "unwrapped" }] : /* istanbul ignore next */ []));
471
+ register(breadcrumb) {
472
+ this.map.inline((m) => m.set(breadcrumb.id, breadcrumb));
502
473
  }
503
- return normalizedPath;
504
- };
505
-
506
- function injectSnapshotPathResolver() {
507
- const router = inject(Router);
508
- return (route) => {
509
- const segments = route.pathFromRoot.flatMap((snap) => snap.routeConfig?.path ?? []);
510
- const joinedSegments = segments.filter(Boolean).join('/');
511
- return router.serializeUrl(router.parseUrl(joinedSegments));
512
- };
474
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
475
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, providedIn: 'root' });
476
+ }
477
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BreadcrumbStore, decorators: [{
478
+ type: Injectable,
479
+ args: [{
480
+ providedIn: 'root',
481
+ }]
482
+ }] });
483
+ /**
484
+ * Injects and provides access to a reactive list of breadcrumbs.
485
+ *
486
+ * The breadcrumbs are ordered and reflect the current active navigation path.
487
+ * @see Breadcrumb
488
+ * @returns `Signal<Breadcrumb[]>`
489
+ *
490
+ * @example
491
+ * ```typescript
492
+ * @Component({
493
+ * selector: 'app-breadcrumbs',
494
+ * template: `
495
+ * <nav aria-label="breadcrumb">
496
+ * <ol>
497
+ * @for (crumb of breadcrumbs(); track crumb.id) {
498
+ * <li>
499
+ * <a [href]="crumb.link()" [attr.aria-label]="crumb.ariaLabel()">{{ crumb.label() }}</a>
500
+ * </li>
501
+ * }
502
+ * </ol>
503
+ * </nav>
504
+ * `
505
+ * })
506
+ * export class MyBreadcrumbsComponent {
507
+ * breadcrumbs = injectBreadcrumbs();
508
+ * }
509
+ * ```
510
+ */
511
+ function injectBreadcrumbs() {
512
+ const store = inject(BreadcrumbStore);
513
+ return store.unwrapped;
513
514
  }
514
515
 
515
516
  /**
516
517
  * Creates and registers a breadcrumb for a specific route.
517
518
  * This function is designed to be used as an Angular Route `ResolveFn`.
518
- * It handles the registration of the breadcrumb with the `BreadcrumbStore`
519
- * and ensures automatic deregistration when the route is destroyed.
520
519
  *
521
- * @param factory A function that returns a `CreateBreadcrumbOptions` object.
520
+ * Accepts a static label (`string`), a static options object, or a factory returning either —
521
+ * use a factory when you need `inject()` for dynamic data.
522
+ *
523
+ * @param factoryOrValue A static label, a static `CreateBreadcrumbOptions`, or a factory returning either.
522
524
  * @see CreateBreadcrumbOptions
523
525
  *
524
526
  * @example
@@ -528,25 +530,34 @@ function injectSnapshotPathResolver() {
528
530
  * path: 'home',
529
531
  * component: HomeComponent,
530
532
  * resolve: {
531
- * breadcrumb: createBreadcrumb(() => ({
532
- * label: 'Home',
533
- * });
533
+ * // shorthand for { label: 'Home' }
534
+ * breadcrumb: createBreadcrumb('Home'),
534
535
  * },
536
+ * },
537
+ * {
535
538
  * path: 'users/:userId',
536
539
  * component: UserProfileComponent,
537
540
  * resolve: {
538
541
  * breadcrumb: createBreadcrumb(() => {
539
542
  * const userStore = inject(UserStore);
540
543
  * return {
541
- * label: () => userStore.user().name ?? 'Loading...
542
- * };
543
- * })
544
+ * label: () => userStore.user().name ?? 'Loading...',
545
+ * };
546
+ * }),
544
547
  * },
545
- * }
548
+ * },
546
549
  * ];
547
550
  * ```
548
551
  */
549
- function createBreadcrumb(factory) {
552
+ function createBreadcrumb(factoryOrValue) {
553
+ const factory = typeof factoryOrValue === 'string'
554
+ ? () => ({ label: factoryOrValue })
555
+ : typeof factoryOrValue === 'function'
556
+ ? () => {
557
+ const result = factoryOrValue();
558
+ return typeof result === 'string' ? { label: result } : result;
559
+ }
560
+ : () => factoryOrValue;
550
561
  return async (route) => {
551
562
  const router = inject(Router);
552
563
  const store = inject(BreadcrumbStore);
@@ -554,7 +565,7 @@ function createBreadcrumb(factory) {
554
565
  const fp = resolver(route);
555
566
  const tree = createUrlTreeFromSnapshot(route, [], route.queryParams, route.fragment);
556
567
  const provided = factory();
557
- const link = computed(() => router.serializeUrl(tree), ...(ngDevMode ? [{ debugName: "link" }] : []));
568
+ const link = computed(() => router.serializeUrl(tree), ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
558
569
  const { label, ariaLabel = label } = provided;
559
570
  const bc = {
560
571
  id: fp,
@@ -577,10 +588,10 @@ class PreloadRequester {
577
588
  startPreload(routePath) {
578
589
  this.preloadOnDemand$.next(routePath);
579
590
  }
580
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadRequester, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
581
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadRequester, providedIn: 'root' });
591
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
592
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, providedIn: 'root' });
582
593
  }
583
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadRequester, decorators: [{
594
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadRequester, decorators: [{
584
595
  type: Injectable,
585
596
  args: [{ providedIn: 'root' }]
586
597
  }] });
@@ -620,10 +631,10 @@ class PreloadStrategy {
620
631
  const predicate = createRoutePredicate(fp);
621
632
  return this.req.preloadRequested$.pipe(filter$1((path) => path === fp || predicate(path)), take(1), switchMap(() => load()), finalize(() => this.loading.delete(fp)));
622
633
  }
623
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
624
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadStrategy, providedIn: 'root' });
634
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
635
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, providedIn: 'root' });
625
636
  }
626
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: PreloadStrategy, decorators: [{
637
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PreloadStrategy, decorators: [{
627
638
  type: Injectable,
628
639
  args: [{
629
640
  providedIn: 'root',
@@ -650,6 +661,38 @@ function treeToSerializedUrl(router, urlTree) {
650
661
  return null;
651
662
  return router.serializeUrl(urlTree);
652
663
  }
664
+ /**
665
+ * Returns an imperative function that triggers preloading for an arbitrary link, using
666
+ * the same path resolution and {@link PreloadStrategy} pipeline as the {@link Link}
667
+ * (`mmLink`) directive.
668
+ *
669
+ * Use this when the `Link` directive isn't a fit — for example, preloading a route from
670
+ * an effect when a user opens a menu, hovers a non-link element, or reacts to a signal
671
+ * change — and you don't want to render an `<a [mmLink]>` just to request the preload.
672
+ *
673
+ * Requires {@link PreloadStrategy} to be wired up via `provideRouter(routes, withPreloading(PreloadStrategy))`,
674
+ * just like the directive.
675
+ *
676
+ * @returns A function accepting the same link descriptor shape as `mmLink` (`string`,
677
+ * commands array, `UrlTree`, or `null`). Passing `null` or an unresolvable link is a no-op.
678
+ *
679
+ * @example
680
+ * ```typescript
681
+ * @Component({ ... })
682
+ * export class CommandPaletteComponent {
683
+ * private readonly triggerPreload = injectTriggerPreload();
684
+ *
685
+ * protected readonly highlighted = signal<string | null>(null);
686
+ *
687
+ * constructor() {
688
+ * effect(() => {
689
+ * const target = this.highlighted();
690
+ * if (target) this.triggerPreload(target);
691
+ * });
692
+ * }
693
+ * }
694
+ * ```
695
+ */
653
696
  function injectTriggerPreload() {
654
697
  const req = inject(PreloadRequester);
655
698
  const router = inject(Router);
@@ -688,26 +731,26 @@ class Link {
688
731
  }) ?? inject(RouterLinkWithHref, { self: true, optional: true });
689
732
  req = inject(PreloadRequester);
690
733
  router = inject(Router);
691
- target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : []));
692
- queryParams = input(...(ngDevMode ? [undefined, { debugName: "queryParams" }] : []));
693
- fragment = input(...(ngDevMode ? [undefined, { debugName: "fragment" }] : []));
694
- queryParamsHandling = input(...(ngDevMode ? [undefined, { debugName: "queryParamsHandling" }] : []));
695
- state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : []));
696
- info = input(...(ngDevMode ? [undefined, { debugName: "info" }] : []));
697
- relativeTo = input(...(ngDevMode ? [undefined, { debugName: "relativeTo" }] : []));
698
- skipLocationChange = input(false, { ...(ngDevMode ? { debugName: "skipLocationChange" } : {}), transform: booleanAttribute });
699
- replaceUrl = input(false, { ...(ngDevMode ? { debugName: "replaceUrl" } : {}), transform: booleanAttribute });
700
- mmLink = input(null, ...(ngDevMode ? [{ debugName: "mmLink" }] : []));
701
- preloadOn = input(injectConfig().preloadOn, ...(ngDevMode ? [{ debugName: "preloadOn" }] : []));
702
- useMouseDown = input(injectConfig().useMouseDown, { ...(ngDevMode ? { debugName: "useMouseDown" } : {}), transform: booleanAttribute });
703
- beforeNavigate = input(...(ngDevMode ? [undefined, { debugName: "beforeNavigate" }] : []));
734
+ target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
735
+ queryParams = input(...(ngDevMode ? [undefined, { debugName: "queryParams" }] : /* istanbul ignore next */ []));
736
+ fragment = input(...(ngDevMode ? [undefined, { debugName: "fragment" }] : /* istanbul ignore next */ []));
737
+ queryParamsHandling = input(...(ngDevMode ? [undefined, { debugName: "queryParamsHandling" }] : /* istanbul ignore next */ []));
738
+ state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : /* istanbul ignore next */ []));
739
+ info = input(...(ngDevMode ? [undefined, { debugName: "info" }] : /* istanbul ignore next */ []));
740
+ relativeTo = input(...(ngDevMode ? [undefined, { debugName: "relativeTo" }] : /* istanbul ignore next */ []));
741
+ skipLocationChange = input(false, { ...(ngDevMode ? { debugName: "skipLocationChange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
742
+ replaceUrl = input(false, { ...(ngDevMode ? { debugName: "replaceUrl" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
743
+ mmLink = input(null, ...(ngDevMode ? [{ debugName: "mmLink" }] : /* istanbul ignore next */ []));
744
+ preloadOn = input(injectConfig().preloadOn, ...(ngDevMode ? [{ debugName: "preloadOn" }] : /* istanbul ignore next */ []));
745
+ useMouseDown = input(injectConfig().useMouseDown, { ...(ngDevMode ? { debugName: "useMouseDown" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
746
+ beforeNavigate = input(...(ngDevMode ? [undefined, { debugName: "beforeNavigate" }] : /* istanbul ignore next */ []));
704
747
  preloading = output();
705
748
  urlTree = computed(() => {
706
749
  return inputToUrlTree(this.router, this.mmLink(), this.relativeTo(), this.queryParams(), this.fragment(), this.queryParamsHandling(), this.routerLink?.urlTree);
707
- }, ...(ngDevMode ? [{ debugName: "urlTree" }] : []));
750
+ }, ...(ngDevMode ? [{ debugName: "urlTree" }] : /* istanbul ignore next */ []));
708
751
  fullPath = computed(() => {
709
752
  return treeToSerializedUrl(this.router, this.urlTree());
710
- }, ...(ngDevMode ? [{ debugName: "fullPath" }] : []));
753
+ }, ...(ngDevMode ? [{ debugName: "fullPath" }] : /* istanbul ignore next */ []));
711
754
  onHover() {
712
755
  if (untracked(this.preloadOn) !== 'hover')
713
756
  return;
@@ -743,10 +786,10 @@ class Link {
743
786
  untracked(this.beforeNavigate)?.();
744
787
  return this.routerLink?.onClick(button, ctrlKey, shiftKey, altKey, metaKey);
745
788
  }
746
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: Link, deps: [], target: i0.ɵɵFactoryTarget.Directive });
747
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.8", type: Link, isStandalone: true, selector: "[mmLink]", inputs: { target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, queryParamsHandling: { classPropertyName: "queryParamsHandling", publicName: "queryParamsHandling", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, info: { classPropertyName: "info", publicName: "info", isSignal: true, isRequired: false, transformFunction: null }, relativeTo: { classPropertyName: "relativeTo", publicName: "relativeTo", isSignal: true, isRequired: false, transformFunction: null }, skipLocationChange: { classPropertyName: "skipLocationChange", publicName: "skipLocationChange", isSignal: true, isRequired: false, transformFunction: null }, replaceUrl: { classPropertyName: "replaceUrl", publicName: "replaceUrl", isSignal: true, isRequired: false, transformFunction: null }, mmLink: { classPropertyName: "mmLink", publicName: "mmLink", isSignal: true, isRequired: false, transformFunction: null }, preloadOn: { classPropertyName: "preloadOn", publicName: "preloadOn", isSignal: true, isRequired: false, transformFunction: null }, useMouseDown: { classPropertyName: "useMouseDown", publicName: "useMouseDown", isSignal: true, isRequired: false, transformFunction: null }, beforeNavigate: { classPropertyName: "beforeNavigate", publicName: "beforeNavigate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { preloading: "preloading" }, host: { listeners: { "mouseenter": "onHover()", "mousedown": "onMouseDown($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)", "click": "onClick($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)" } }, exportAs: ["mmLink"], hostDirectives: [{ directive: i1.RouterLink, inputs: ["routerLink", "mmLink", "target", "target", "queryParams", "queryParams", "fragment", "fragment", "queryParamsHandling", "queryParamsHandling", "state", "state", "relativeTo", "relativeTo", "skipLocationChange", "skipLocationChange", "replaceUrl", "replaceUrl"] }], ngImport: i0 });
789
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Link, deps: [], target: i0.ɵɵFactoryTarget.Directive });
790
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.12", type: Link, isStandalone: true, selector: "[mmLink]", inputs: { target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, queryParamsHandling: { classPropertyName: "queryParamsHandling", publicName: "queryParamsHandling", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, info: { classPropertyName: "info", publicName: "info", isSignal: true, isRequired: false, transformFunction: null }, relativeTo: { classPropertyName: "relativeTo", publicName: "relativeTo", isSignal: true, isRequired: false, transformFunction: null }, skipLocationChange: { classPropertyName: "skipLocationChange", publicName: "skipLocationChange", isSignal: true, isRequired: false, transformFunction: null }, replaceUrl: { classPropertyName: "replaceUrl", publicName: "replaceUrl", isSignal: true, isRequired: false, transformFunction: null }, mmLink: { classPropertyName: "mmLink", publicName: "mmLink", isSignal: true, isRequired: false, transformFunction: null }, preloadOn: { classPropertyName: "preloadOn", publicName: "preloadOn", isSignal: true, isRequired: false, transformFunction: null }, useMouseDown: { classPropertyName: "useMouseDown", publicName: "useMouseDown", isSignal: true, isRequired: false, transformFunction: null }, beforeNavigate: { classPropertyName: "beforeNavigate", publicName: "beforeNavigate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { preloading: "preloading" }, host: { listeners: { "mouseenter": "onHover()", "mousedown": "onMouseDown($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)", "click": "onClick($event.button,$event.ctrlKey,$event.shiftKey,$event.altKey,$event.metaKey)" } }, exportAs: ["mmLink"], hostDirectives: [{ directive: i1.RouterLink, inputs: ["routerLink", "mmLink", "target", "target", "queryParams", "queryParams", "fragment", "fragment", "queryParamsHandling", "queryParamsHandling", "state", "state", "relativeTo", "relativeTo", "skipLocationChange", "skipLocationChange", "replaceUrl", "replaceUrl"] }], ngImport: i0 });
748
791
  }
749
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: Link, decorators: [{
792
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: Link, decorators: [{
750
793
  type: Directive,
751
794
  args: [{
752
795
  selector: '[mmLink]',
@@ -791,6 +834,229 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
791
834
  ]]
792
835
  }] } });
793
836
 
837
+ /** @internal */
838
+ const NEVER_TRUE = computed(() => false, ...(ngDevMode ? [{ debugName: "NEVER_TRUE" }] : /* istanbul ignore next */ []));
839
+ function wrap(value, fallback) {
840
+ if (value === undefined)
841
+ return fallback ? fallback : computed(() => undefined);
842
+ if (typeof value === 'function')
843
+ return isSignal(value) ? value : computed(value);
844
+ return computed(() => value);
845
+ }
846
+ function isAbsoluteCommandArray(commands) {
847
+ return (commands.length > 0 &&
848
+ typeof commands[0] === 'string' &&
849
+ commands[0].startsWith('/'));
850
+ }
851
+ function resolveLinkTree(input, router, relativeTo) {
852
+ const raw = typeof input === 'function' ? input() : input;
853
+ if (raw === undefined || raw === null)
854
+ return null;
855
+ if (raw instanceof UrlTree)
856
+ return raw;
857
+ if (typeof raw === 'string') {
858
+ if (raw.startsWith('/'))
859
+ return router.parseUrl(raw);
860
+ const parsed = router.parseUrl('/' + raw);
861
+ const primary = parsed.root.children['primary'];
862
+ const segments = primary ? primary.segments.map((s) => s.path) : [];
863
+ return createUrlTreeFromSnapshot(relativeTo, segments, parsed.queryParams, parsed.fragment);
864
+ }
865
+ if (isAbsoluteCommandArray(raw))
866
+ return router.createUrlTree(raw);
867
+ return createUrlTreeFromSnapshot(relativeTo, raw);
868
+ }
869
+ function resolveMeta(input) {
870
+ if (input === undefined)
871
+ return {};
872
+ return typeof input === 'function' ? input() : input;
873
+ }
874
+ /**
875
+ * @internal
876
+ * Recursively builds an {@link InternalNavItem} tree from {@link CreateNavItem} input.
877
+ * Cascades parent `disabled`/`hidden` to descendants and computes `active` against the
878
+ * current router URL using `Router.isActive`.
879
+ */
880
+ function createInternalNavItem(input, router, relativeTo, configActiveMatch, parentDisabled, parentHidden, fallbackId) {
881
+ const label = wrap(input.label);
882
+ const ariaLabel = input.ariaLabel ? wrap(input.ariaLabel) : label;
883
+ const linkTree = computed(() => resolveLinkTree(input.link, router, relativeTo), ...(ngDevMode ? [{ debugName: "linkTree" }] : /* istanbul ignore next */ []));
884
+ const link = computed(() => {
885
+ const tree = linkTree();
886
+ return tree ? router.serializeUrl(tree) : null;
887
+ }, ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
888
+ const ownDisabled = wrap(input.disabled, NEVER_TRUE);
889
+ const ownHidden = wrap(input.hidden, NEVER_TRUE);
890
+ const disabled = computed(() => parentDisabled() || ownDisabled(), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
891
+ const hidden = computed(() => parentHidden() || ownHidden(), ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
892
+ const metaInput = input.meta;
893
+ const meta = computed(() => resolveMeta(metaInput), ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
894
+ const id = input.id !== undefined
895
+ ? wrap(input.id)
896
+ : computed(() => link() ?? fallbackId);
897
+ const childItems = (input.children ?? []).map((childInput, i) => createInternalNavItem(childInput, router, relativeTo, configActiveMatch, disabled, hidden, `${fallbackId}.${i}`));
898
+ const children = computed(() => childItems.filter((c) => !c.hidden()), ...(ngDevMode ? [{ debugName: "children" }] : /* istanbul ignore next */ []));
899
+ const mergedActiveMatch = {
900
+ ...configActiveMatch,
901
+ ...input.activeMatch,
902
+ };
903
+ const trackNavigation = url(router);
904
+ const finalOptions = {
905
+ paths: 'subset',
906
+ fragment: 'ignored',
907
+ matrixParams: 'ignored',
908
+ queryParams: 'subset',
909
+ ...mergedActiveMatch,
910
+ };
911
+ const ownActive = computed(() => {
912
+ trackNavigation();
913
+ const tree = linkTree();
914
+ return tree ? router.isActive(tree, finalOptions) : false;
915
+ }, ...(ngDevMode ? [{ debugName: "ownActive" }] : /* istanbul ignore next */ []));
916
+ const orWithChildren = input.matchesWhenChildActive ?? input.activeMatch === undefined;
917
+ const active = computed(() => ownActive() ||
918
+ (orWithChildren &&
919
+ !!childItems.length &&
920
+ childItems.some((c) => c.active())), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
921
+ return {
922
+ id,
923
+ label,
924
+ ariaLabel,
925
+ link,
926
+ active,
927
+ disabled,
928
+ hidden,
929
+ meta,
930
+ children,
931
+ };
932
+ }
933
+
934
+ /** @internal */
935
+ const token$1 = new InjectionToken('@mmstack/router-core:nav-config');
936
+ /**
937
+ * Provides global configuration for the nav system.
938
+ *
939
+ * @example
940
+ * ```typescript
941
+ * provideNavConfig({
942
+ * activeMatch: { queryParams: 'ignored' },
943
+ * }),
944
+ * ```
945
+ */
946
+ function provideNavConfig(config) {
947
+ return {
948
+ provide: token$1,
949
+ useValue: { ...config },
950
+ };
951
+ }
952
+ /** @internal */
953
+ function injectNavConfig() {
954
+ return inject(token$1, { optional: true }) ?? {};
955
+ }
956
+
957
+ /** @internal */
958
+ const DEFAULT_NAV_SCOPE = Symbol('mmstack.nav.default');
959
+ class NavStore {
960
+ map = mutable(new Map());
961
+ leafRoutes = injectLeafRoutes();
962
+ /** @internal */
963
+ register(scope, routePath, items) {
964
+ this.map.inline((m) => {
965
+ let scopeMap = m.get(scope);
966
+ if (!scopeMap) {
967
+ scopeMap = new Map();
968
+ m.set(scope, scopeMap);
969
+ }
970
+ scopeMap.set(routePath, items);
971
+ });
972
+ }
973
+ /** @internal */
974
+ scope(name) {
975
+ return computed(() => {
976
+ const scopeMap = this.map().get(name);
977
+ if (!scopeMap)
978
+ return [];
979
+ const leaves = this.leafRoutes();
980
+ for (let i = leaves.length - 1; i >= 0; i--) {
981
+ const items = scopeMap.get(leaves[i].path);
982
+ if (items) {
983
+ return items.filter((it) => !it.hidden());
984
+ }
985
+ }
986
+ return [];
987
+ });
988
+ }
989
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
990
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, providedIn: 'root' });
991
+ }
992
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NavStore, decorators: [{
993
+ type: Injectable,
994
+ args: [{ providedIn: 'root' }]
995
+ }] });
996
+ /**
997
+ * Returns a reactive list of nav items for the requested scope.
998
+ *
999
+ * The returned signal reflects the nearest active ancestor route that registered items
1000
+ * for `name` via `createNavItems`. Hidden items are filtered out.
1001
+ *
1002
+ * @typeParam TMeta The shape of `NavItem.meta` for the consuming code. Untyped at the
1003
+ * registration site — this is a consumer-side assertion.
1004
+ *
1005
+ * @example
1006
+ * ```typescript
1007
+ * @Component({
1008
+ * template: `
1009
+ * @for (item of items(); track item) {
1010
+ * <a [href]="item.link()" [class.active]="item.active()">{{ item.label() }}</a>
1011
+ * }
1012
+ * `,
1013
+ * })
1014
+ * export class TopBar {
1015
+ * protected readonly items = injectNavItems();
1016
+ * }
1017
+ * ```
1018
+ */
1019
+ function injectNavItems(name) {
1020
+ const store = inject(NavStore);
1021
+ return store.scope(name ?? DEFAULT_NAV_SCOPE);
1022
+ }
1023
+
1024
+ /**
1025
+ * Registers a set of nav items for the activating route under the given scope.
1026
+ * Mirrors `createBreadcrumb` / `createTitle` — designed to be used in a route's
1027
+ * `resolve` map.
1028
+ *
1029
+ * Multiple scopes can be registered on a single route by giving each its own `name`
1030
+ * (and a unique key in the `resolve` map):
1031
+ *
1032
+ * ```typescript
1033
+ * resolve: {
1034
+ * mainNav: createNavItems([...], { name: 'main' }),
1035
+ * sideNav: createNavItems([...], { name: 'side' }),
1036
+ * }
1037
+ * ```
1038
+ *
1039
+ * Scope override semantics: when multiple routes in the active chain register items
1040
+ * under the same scope, the deepest active registration wins. Navigating away restores
1041
+ * the shallower registration.
1042
+ */
1043
+ function createNavItems(itemsOrFactory, options) {
1044
+ const factory = typeof itemsOrFactory === 'function'
1045
+ ? itemsOrFactory
1046
+ : () => itemsOrFactory;
1047
+ return async (route) => {
1048
+ const router = inject(Router);
1049
+ const store = inject(NavStore);
1050
+ const resolveRoutePath = injectSnapshotPathResolver();
1051
+ const config = injectNavConfig();
1052
+ const routePath = resolveRoutePath(route);
1053
+ const scope = options?.name ?? DEFAULT_NAV_SCOPE;
1054
+ const items = factory().map((input, i) => createInternalNavItem(input, router, route, config.activeMatch, NEVER_TRUE, NEVER_TRUE, `${routePath}#${i}`));
1055
+ store.register(scope, routePath, items);
1056
+ return Promise.resolve();
1057
+ };
1058
+ }
1059
+
794
1060
  /**
795
1061
  * Creates a WritableSignal that synchronizes with a specific URL query parameter,
796
1062
  * enabling two-way binding between the signal's state and the URL.
@@ -822,7 +1088,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
822
1088
  *
823
1089
  * @Component({
824
1090
  * selector: 'app-product-list',
825
- * standalone: true,
826
1091
  * // imports: [FormsModule], // If using ngModel
827
1092
  * template: `
828
1093
  * <div>
@@ -880,7 +1145,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
880
1145
  const queryParams = toSignal(route.queryParams, {
881
1146
  initialValue: route.snapshot.queryParams,
882
1147
  });
883
- const queryParam = computed(() => queryParamMap().get(keySignal()), ...(ngDevMode ? [{ debugName: "queryParam" }] : []));
1148
+ const queryParam = computed(() => queryParamMap().get(keySignal()), ...(ngDevMode ? [{ debugName: "queryParam" }] : /* istanbul ignore next */ []));
884
1149
  const set = (newValue) => {
885
1150
  const next = {
886
1151
  ...untracked(queryParams),
@@ -901,7 +1166,7 @@ function queryParam(key, route = inject(ActivatedRoute)) {
901
1166
  return toWritable(queryParam, set);
902
1167
  }
903
1168
 
904
- const token = new InjectionToken('MMSTACK_TITLE_CONFIG');
1169
+ const token = new InjectionToken('@mmstack/router-core:title-config');
905
1170
  /**
906
1171
  * used to provide the title configuration, will not be applied unless a `createTitle` resolver is used
907
1172
  */
@@ -913,29 +1178,41 @@ function provideTitleConfig(config) {
913
1178
  return {
914
1179
  provide: token,
915
1180
  useValue: {
1181
+ initialTitle: config?.initialTitle ?? '',
916
1182
  parser: prefixFn,
917
1183
  keepLastKnown: config?.keepLastKnownTitle ?? true,
918
1184
  },
919
1185
  };
920
1186
  }
1187
+ function identity(str) {
1188
+ return str;
1189
+ }
921
1190
  function injectTitleConfig() {
922
- return inject(token);
1191
+ return (inject(token, {
1192
+ optional: true,
1193
+ }) ?? {
1194
+ initialTitle: '',
1195
+ parser: identity,
1196
+ keepLastKnown: true,
1197
+ });
923
1198
  }
924
1199
 
925
1200
  class TitleStore {
926
- title = inject(Title);
927
1201
  map = mutable(new Map());
928
- leafRoutes = injectLeafRoutes();
929
1202
  constructor() {
930
- const reverseLeaves = computed(() => this.leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : []));
1203
+ const { keepLastKnown, initialTitle } = injectTitleConfig();
1204
+ const leafRoutes = injectLeafRoutes();
1205
+ const title = inject(Title);
1206
+ const fallbackTitle = initialTitle || untracked(() => title.getTitle());
1207
+ const reverseLeaves = computed(() => leafRoutes().toReversed(), ...(ngDevMode ? [{ debugName: "reverseLeaves" }] : /* istanbul ignore next */ []));
931
1208
  const currentResolvedTitles = computed(() => {
932
1209
  const map = this.map();
933
1210
  return reverseLeaves()
934
- .map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title)
935
- .filter((v) => !!v);
936
- }, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : []));
937
- const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : []));
938
- const heldTitle = injectTitleConfig().keepLastKnown
1211
+ .map((leaf) => map.get(leaf.path)?.() ?? leaf.route.title ?? null)
1212
+ .filter((v) => v !== null);
1213
+ }, ...(ngDevMode ? [{ debugName: "currentResolvedTitles" }] : /* istanbul ignore next */ []));
1214
+ const currentTitle = computed(() => currentResolvedTitles().at(0) ?? '', ...(ngDevMode ? [{ debugName: "currentTitle" }] : /* istanbul ignore next */ []));
1215
+ const heldTitle = keepLastKnown
939
1216
  ? linkedSignal({
940
1217
  source: () => currentTitle(),
941
1218
  computation: (value, prev) => {
@@ -946,16 +1223,16 @@ class TitleStore {
946
1223
  })
947
1224
  : currentTitle;
948
1225
  effect(() => {
949
- this.title.setTitle(heldTitle());
1226
+ title.setTitle(heldTitle() || fallbackTitle);
950
1227
  });
951
1228
  }
952
1229
  register(id, titleFn) {
953
1230
  this.map.inline((m) => m.set(id, titleFn));
954
1231
  }
955
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TitleStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
956
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TitleStore, providedIn: 'root' });
1232
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1233
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, providedIn: 'root' });
957
1234
  }
958
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TitleStore, decorators: [{
1235
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: TitleStore, decorators: [{
959
1236
  type: Injectable,
960
1237
  args: [{
961
1238
  providedIn: 'root',
@@ -965,23 +1242,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
965
1242
  *
966
1243
  * Creates a title resolver function that can be used in Angular's router.
967
1244
  *
968
- * @param fn
969
- * A function that returns a string or a Signal<string> representing the title.
1245
+ * @param factoryOrValue
1246
+ * A function that returns a string or a Signal<string> representing the title or just the string directly.
970
1247
  * @param awaitValue
971
1248
  * If `true`, the resolver will wait until the title signal has a value before resolving.
972
1249
  * Defaults to `false`.
973
1250
  */
974
- function createTitle(fn, awaitValue = false) {
1251
+ function createTitle(factoryOrValue, awaitValue = false) {
1252
+ const factory = typeof factoryOrValue === 'string' ? () => factoryOrValue : factoryOrValue;
975
1253
  return async (route) => {
976
1254
  const store = inject(TitleStore);
977
1255
  const resolver = injectSnapshotPathResolver();
978
1256
  const fp = resolver(route);
979
1257
  const { parser } = injectTitleConfig();
980
- const resolved = fn();
1258
+ const resolved = factory();
981
1259
  const titleSignal = typeof resolved === 'string'
982
1260
  ? computed(() => resolved)
983
1261
  : computed(resolved);
984
- const parsedTitleSignal = computed(() => parser(titleSignal()), ...(ngDevMode ? [{ debugName: "parsedTitleSignal" }] : []));
1262
+ const parsedTitleSignal = computed(() => parser(titleSignal()), ...(ngDevMode ? [{ debugName: "parsedTitleSignal" }] : /* istanbul ignore next */ []));
985
1263
  store.register(fp, parsedTitleSignal);
986
1264
  if (awaitValue)
987
1265
  await until(parsedTitleSignal, (v) => !!v);
@@ -993,5 +1271,5 @@ function createTitle(fn, awaitValue = false) {
993
1271
  * Generated bundle index. Do not edit.
994
1272
  */
995
1273
 
996
- export { Link, PreloadStrategy, createBreadcrumb, createTitle, injectBreadcrumbs, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideTitleConfig, queryParam, url };
1274
+ export { Link, PreloadStrategy, createBreadcrumb, createNavItems, createTitle, injectBreadcrumbs, injectNavItems, injectTriggerPreload, provideBreadcrumbConfig, provideMMLinkDefaultConfig, provideNavConfig, provideTitleConfig, queryParam, url };
997
1275
  //# sourceMappingURL=mmstack-router-core.mjs.map