@reforgium/presentia 1.5.0 → 2.0.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/CHANGELOG.md +144 -0
- package/LICENSE +21 -0
- package/README.md +346 -35
- package/bin/presentia-gen-namespaces.mjs +1248 -0
- package/fesm2022/reforgium-presentia.mjs +690 -186
- package/package.json +23 -5
- package/types/reforgium-presentia.d.ts +284 -89
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, makeEnvironmentProviders, LOCALE_ID } from '@angular/core';
|
|
3
|
-
import { getChainedValue, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEME, CHANGE_THEME, CURRENT_DEVICE
|
|
2
|
+
import { InjectionToken, signal, computed, inject, DestroyRef, PLATFORM_ID, afterRenderEffect, Injectable, TemplateRef, ViewContainerRef, effect, Input, Directive, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, makeEnvironmentProviders, provideAppInitializer, LOCALE_ID } from '@angular/core';
|
|
3
|
+
import { getChainedValue, deepEqual, compareRoutes, TRANSLATION, SELECTED_LANG, CHANGE_LANG, SELECTED_THEME, CHANGE_THEME, CURRENT_DEVICE } from '@reforgium/internal';
|
|
4
4
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
5
5
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
6
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
7
7
|
import { HttpClient } from '@angular/common/http';
|
|
8
8
|
import { firstValueFrom, tap } from 'rxjs';
|
|
9
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
9
10
|
import { Title, Meta } from '@angular/platform-browser';
|
|
10
|
-
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Default breakpoints for device type detection.
|
|
@@ -73,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
|
|
|
73
73
|
*/
|
|
74
74
|
class AdaptiveService {
|
|
75
75
|
/** @internal Signal of the current device type. */
|
|
76
|
-
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
|
|
76
|
+
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : /* istanbul ignore next */ []));
|
|
77
77
|
/** @internal Signals of the current window width and height. */
|
|
78
|
-
#width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
|
|
79
|
-
#height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : []));
|
|
78
|
+
#width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : /* istanbul ignore next */ []));
|
|
79
|
+
#height = signal(0, ...(ngDevMode ? [{ debugName: "#height" }] : /* istanbul ignore next */ []));
|
|
80
80
|
/**
|
|
81
81
|
* Current device type (reactive signal).
|
|
82
82
|
* Possible values: `'desktop' | 'tablet' | 'mobile'`.
|
|
@@ -99,12 +99,15 @@ class AdaptiveService {
|
|
|
99
99
|
* Computed signal indicating whether the current device is a desktop.
|
|
100
100
|
* Used for conditional rendering or layout configuration.
|
|
101
101
|
*/
|
|
102
|
-
isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : []));
|
|
102
|
+
isDesktop = computed(() => this.#device() === 'desktop', ...(ngDevMode ? [{ debugName: "isDesktop" }] : /* istanbul ignore next */ []));
|
|
103
|
+
isMobile = computed(() => this.#device() === 'mobile', ...(ngDevMode ? [{ debugName: "isMobile" }] : /* istanbul ignore next */ []));
|
|
104
|
+
isTablet = computed(() => this.#device() === 'tablet', ...(ngDevMode ? [{ debugName: "isTablet" }] : /* istanbul ignore next */ []));
|
|
105
|
+
isDesktopSmall = computed(() => this.#device() === 'desktop-s', ...(ngDevMode ? [{ debugName: "isDesktopSmall" }] : /* istanbul ignore next */ []));
|
|
103
106
|
/**
|
|
104
107
|
* Computed signal determining whether the screen is in portrait orientation.
|
|
105
108
|
* Returns `true` if window height is greater than width.
|
|
106
109
|
*/
|
|
107
|
-
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
|
|
110
|
+
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : /* istanbul ignore next */ []));
|
|
108
111
|
deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
|
|
109
112
|
devicePriority = Object.keys(this.deviceBreakpoints);
|
|
110
113
|
destroyRef = inject(DestroyRef);
|
|
@@ -148,10 +151,28 @@ class AdaptiveService {
|
|
|
148
151
|
}
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
breakpoint() {
|
|
155
|
+
return this.#device();
|
|
156
|
+
}
|
|
157
|
+
is(device) {
|
|
158
|
+
const allowed = Array.isArray(device) ? device : [device];
|
|
159
|
+
return allowed.includes(this.#device());
|
|
160
|
+
}
|
|
161
|
+
isAtLeast(device) {
|
|
162
|
+
return this.deviceRank(this.#device()) >= this.deviceRank(device);
|
|
163
|
+
}
|
|
164
|
+
isBetween(min, max) {
|
|
165
|
+
const current = this.deviceRank(this.#device());
|
|
166
|
+
return current >= this.deviceRank(min) && current <= this.deviceRank(max);
|
|
167
|
+
}
|
|
168
|
+
deviceRank(device) {
|
|
169
|
+
const index = this.devicePriority.indexOf(device);
|
|
170
|
+
return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
|
|
171
|
+
}
|
|
172
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
173
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, providedIn: 'root' });
|
|
153
174
|
}
|
|
154
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, decorators: [{
|
|
155
176
|
type: Injectable,
|
|
156
177
|
args: [{
|
|
157
178
|
providedIn: 'root',
|
|
@@ -179,8 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
179
200
|
* the template is automatically added or removed from the DOM.
|
|
180
201
|
*/
|
|
181
202
|
class IfDeviceDirective {
|
|
182
|
-
deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
|
|
183
|
-
|
|
203
|
+
deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : /* istanbul ignore next */ []));
|
|
204
|
+
atLeastInput = signal(undefined, ...(ngDevMode ? [{ debugName: "atLeastInput" }] : /* istanbul ignore next */ []));
|
|
205
|
+
betweenInput = signal(undefined, ...(ngDevMode ? [{ debugName: "betweenInput" }] : /* istanbul ignore next */ []));
|
|
206
|
+
inverseInput = signal(false, ...(ngDevMode ? [{ debugName: "inverseInput" }] : /* istanbul ignore next */ []));
|
|
184
207
|
tpl = inject(TemplateRef);
|
|
185
208
|
vcr = inject(ViewContainerRef);
|
|
186
209
|
adaptive = inject(AdaptiveService);
|
|
@@ -190,20 +213,37 @@ class IfDeviceDirective {
|
|
|
190
213
|
constructor() {
|
|
191
214
|
effect(() => {
|
|
192
215
|
const device = this.deviceInput();
|
|
216
|
+
const atLeast = this.atLeastInput();
|
|
217
|
+
const between = this.betweenInput();
|
|
193
218
|
if (device) {
|
|
194
219
|
this.allowedDevices = Array.isArray(device) ? device : [device];
|
|
195
220
|
}
|
|
221
|
+
else if (!atLeast && !between) {
|
|
222
|
+
this.allowedDevices = [];
|
|
223
|
+
}
|
|
196
224
|
this.updateView();
|
|
197
225
|
});
|
|
198
226
|
}
|
|
199
227
|
set reIfDevice(value) {
|
|
200
228
|
this.deviceInput.set(value);
|
|
201
229
|
}
|
|
230
|
+
set reIfDeviceAtLeast(value) {
|
|
231
|
+
this.atLeastInput.set(value);
|
|
232
|
+
}
|
|
233
|
+
set reIfDeviceBetween(value) {
|
|
234
|
+
this.betweenInput.set(value);
|
|
235
|
+
}
|
|
202
236
|
set inverse(value) {
|
|
203
237
|
this.inverseInput.set(!!value);
|
|
204
238
|
}
|
|
205
239
|
updateView() {
|
|
206
|
-
const
|
|
240
|
+
const between = this.betweenInput();
|
|
241
|
+
const atLeast = this.atLeastInput();
|
|
242
|
+
const isAllowed = between
|
|
243
|
+
? this.adaptive.isBetween(between[0], between[1])
|
|
244
|
+
: atLeast
|
|
245
|
+
? this.adaptive.isAtLeast(atLeast)
|
|
246
|
+
: this.allowedDevices.includes(this.currentDevice());
|
|
207
247
|
const shouldShow = this.inverseInput() ? !isAllowed : isAllowed;
|
|
208
248
|
if (shouldShow && !this.hasView) {
|
|
209
249
|
this.vcr.createEmbeddedView(this.tpl);
|
|
@@ -214,18 +254,24 @@ class IfDeviceDirective {
|
|
|
214
254
|
this.hasView = false;
|
|
215
255
|
}
|
|
216
256
|
}
|
|
217
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
218
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.
|
|
257
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IfDeviceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
258
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.4", type: IfDeviceDirective, isStandalone: true, selector: "[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]", inputs: { reIfDevice: "reIfDevice", reIfDeviceAtLeast: "reIfDeviceAtLeast", reIfDeviceBetween: "reIfDeviceBetween", inverse: "inverse" }, ngImport: i0 });
|
|
219
259
|
}
|
|
220
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IfDeviceDirective, decorators: [{
|
|
221
261
|
type: Directive,
|
|
222
262
|
args: [{
|
|
223
|
-
selector: '[reIfDevice]',
|
|
263
|
+
selector: '[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]',
|
|
224
264
|
standalone: true,
|
|
225
265
|
}]
|
|
226
266
|
}], ctorParameters: () => [], propDecorators: { reIfDevice: [{
|
|
227
267
|
type: Input,
|
|
228
268
|
args: ['reIfDevice']
|
|
269
|
+
}], reIfDeviceAtLeast: [{
|
|
270
|
+
type: Input,
|
|
271
|
+
args: ['reIfDeviceAtLeast']
|
|
272
|
+
}], reIfDeviceBetween: [{
|
|
273
|
+
type: Input,
|
|
274
|
+
args: ['reIfDeviceBetween']
|
|
229
275
|
}], inverse: [{
|
|
230
276
|
type: Input
|
|
231
277
|
}] } });
|
|
@@ -234,6 +280,19 @@ const innerLangVal = Symbol('reInnerLangVal');
|
|
|
234
280
|
|
|
235
281
|
const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
|
|
236
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Optional DI token for a custom persistence adapter used by `LangService`
|
|
285
|
+
* to store and retrieve the selected language (default key: `'lang'`).
|
|
286
|
+
*
|
|
287
|
+
* When not provided the service falls back to `localStorage` directly.
|
|
288
|
+
*
|
|
289
|
+
* Example:
|
|
290
|
+
* ```ts
|
|
291
|
+
* { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER');
|
|
295
|
+
|
|
237
296
|
/**
|
|
238
297
|
* Injection token for providing locale configuration to the language module.
|
|
239
298
|
*
|
|
@@ -254,6 +313,67 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
|
|
|
254
313
|
*/
|
|
255
314
|
const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
|
|
256
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @deprecated Diagnostics are usually enabled through route preload config, not by consuming this service directly.
|
|
318
|
+
*/
|
|
319
|
+
class RouteNamespaceDiagnosticsService {
|
|
320
|
+
router = inject(Router, { optional: true });
|
|
321
|
+
enabled = !!inject(LANG_CONFIG).routeNamespacePreload?.diagnostics;
|
|
322
|
+
warned = new Set();
|
|
323
|
+
currentUrl = '';
|
|
324
|
+
currentNamespaces = new Set();
|
|
325
|
+
navigationSettled = false;
|
|
326
|
+
constructor() {
|
|
327
|
+
if (!this.enabled || !this.router) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.router.events.subscribe((event) => {
|
|
331
|
+
if (event instanceof NavigationEnd) {
|
|
332
|
+
const url = normalizeUrlPath$1(event.urlAfterRedirects || event.url);
|
|
333
|
+
if (url === this.currentUrl) {
|
|
334
|
+
this.navigationSettled = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
registerRouteNamespaces(url, namespaces) {
|
|
340
|
+
if (!this.enabled) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.currentUrl = normalizeUrlPath$1(url);
|
|
344
|
+
this.currentNamespaces = new Set(namespaces.map((ns) => ns.trim()).filter(Boolean));
|
|
345
|
+
this.navigationSettled = false;
|
|
346
|
+
}
|
|
347
|
+
warnLateNamespaceLoad(namespace) {
|
|
348
|
+
if (!this.enabled || !this.navigationSettled || typeof ngDevMode === 'undefined') {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const normalizedNamespace = namespace.trim();
|
|
352
|
+
if (!normalizedNamespace || this.currentNamespaces.has(normalizedNamespace)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const key = `${this.currentUrl}|${normalizedNamespace}`;
|
|
356
|
+
if (this.warned.has(key)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.warned.add(key);
|
|
360
|
+
// eslint-disable-next-line no-console
|
|
361
|
+
console.warn(`[presentia] Namespace "${normalizedNamespace}" was loaded after route activation for "${this.currentUrl}". ` +
|
|
362
|
+
'Add it to routeNamespacePreload manifest or route data to avoid raw i18n keys on first paint.');
|
|
363
|
+
}
|
|
364
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
365
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, providedIn: 'root' });
|
|
366
|
+
}
|
|
367
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, decorators: [{
|
|
368
|
+
type: Injectable,
|
|
369
|
+
args: [{ providedIn: 'root' }]
|
|
370
|
+
}], ctorParameters: () => [] });
|
|
371
|
+
function normalizeUrlPath$1(url) {
|
|
372
|
+
const [path] = url.split(/[?#]/, 1);
|
|
373
|
+
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
374
|
+
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
375
|
+
}
|
|
376
|
+
|
|
257
377
|
/**
|
|
258
378
|
* LangService provides functionality for managing and tracking language settings
|
|
259
379
|
* and translations in the application. It is designed to handle localization needs,
|
|
@@ -267,12 +387,13 @@ class LangService {
|
|
|
267
387
|
http = inject(HttpClient);
|
|
268
388
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
269
389
|
missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
|
|
390
|
+
routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
|
|
270
391
|
supportedLangSet = new Set([
|
|
271
392
|
...LangService.BUILTIN_LANGS,
|
|
272
393
|
...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
|
|
273
394
|
]);
|
|
274
|
-
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
|
|
275
|
-
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
|
|
395
|
+
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
|
|
396
|
+
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
|
|
276
397
|
#loadedNamespaces = new Set();
|
|
277
398
|
#pendingLoads = new Map();
|
|
278
399
|
#pendingBatchLoads = new Map();
|
|
@@ -289,7 +410,7 @@ class LangService {
|
|
|
289
410
|
currentLang = computed(() => {
|
|
290
411
|
const lang = this.#lang();
|
|
291
412
|
return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
|
|
292
|
-
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
|
|
413
|
+
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
|
|
293
414
|
/**
|
|
294
415
|
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
295
416
|
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
@@ -357,6 +478,7 @@ class LangService {
|
|
|
357
478
|
if (this.isNamespaceValid(key)) {
|
|
358
479
|
return;
|
|
359
480
|
}
|
|
481
|
+
this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
|
|
360
482
|
this.tryExpireNamespace(key);
|
|
361
483
|
if (this.#pendingLoads.has(key)) {
|
|
362
484
|
return this.#pendingLoads.get(key);
|
|
@@ -411,14 +533,27 @@ class LangService {
|
|
|
411
533
|
await Promise.all(toLoad.map((ns) => this.loadNamespace(ns)));
|
|
412
534
|
return;
|
|
413
535
|
}
|
|
414
|
-
const
|
|
536
|
+
const maxBatchSize = this.normalizeMaxBatchSize(this.config.maxBatchSize);
|
|
537
|
+
const chunks = maxBatchSize && toLoad.length > maxBatchSize ? this.chunkNamespaces(toLoad, maxBatchSize) : [toLoad];
|
|
538
|
+
await Promise.all(chunks.map((chunk) => {
|
|
539
|
+
if (chunk.length < 2) {
|
|
540
|
+
return this.loadNamespace(chunk[0]);
|
|
541
|
+
}
|
|
542
|
+
return this.loadNamespaceBatch(chunk, requestedLang);
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
async loadNamespaceBatch(namespaces, requestedLang) {
|
|
546
|
+
for (const ns of namespaces) {
|
|
547
|
+
this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
|
|
548
|
+
}
|
|
549
|
+
const batchKey = this.makeBatchKey(requestedLang, namespaces);
|
|
415
550
|
if (this.#pendingBatchLoads.has(batchKey)) {
|
|
416
551
|
return this.#pendingBatchLoads.get(batchKey);
|
|
417
552
|
}
|
|
418
553
|
const promise = (async () => {
|
|
419
554
|
try {
|
|
420
555
|
const context = {
|
|
421
|
-
namespaces
|
|
556
|
+
namespaces,
|
|
422
557
|
lang: requestedLang,
|
|
423
558
|
isFromAssets: this.config.isFromAssets,
|
|
424
559
|
baseUrl: this.config.url,
|
|
@@ -430,7 +565,7 @@ class LangService {
|
|
|
430
565
|
}
|
|
431
566
|
const payloads = this.config.batchResponseAdapter(response, this.toBatchResponseContext(context));
|
|
432
567
|
const merged = {};
|
|
433
|
-
for (const ns of
|
|
568
|
+
for (const ns of namespaces) {
|
|
434
569
|
const nsPayload = payloads[ns];
|
|
435
570
|
if (!nsPayload) {
|
|
436
571
|
continue;
|
|
@@ -450,6 +585,19 @@ class LangService {
|
|
|
450
585
|
this.#pendingBatchLoads.set(batchKey, promise);
|
|
451
586
|
return promise;
|
|
452
587
|
}
|
|
588
|
+
normalizeMaxBatchSize(value) {
|
|
589
|
+
if (!value || value < 1) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return Math.floor(value);
|
|
593
|
+
}
|
|
594
|
+
chunkNamespaces(namespaces, size) {
|
|
595
|
+
const chunks = [];
|
|
596
|
+
for (let i = 0; i < namespaces.length; i += size) {
|
|
597
|
+
chunks.push(namespaces.slice(i, i + size));
|
|
598
|
+
}
|
|
599
|
+
return chunks;
|
|
600
|
+
}
|
|
453
601
|
evictNamespace(ns) {
|
|
454
602
|
const key = this.makeNamespaceKey(ns);
|
|
455
603
|
if (!this.#loadedNamespaces.has(key)) {
|
|
@@ -664,10 +812,10 @@ class LangService {
|
|
|
664
812
|
.map((lang) => lang.trim().toLowerCase())
|
|
665
813
|
.filter((lang) => !!lang && LangService.LANG_CODE_RE.test(lang));
|
|
666
814
|
}
|
|
667
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
668
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
815
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
816
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, providedIn: 'root' });
|
|
669
817
|
}
|
|
670
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
818
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, decorators: [{
|
|
671
819
|
type: Injectable,
|
|
672
820
|
args: [{ providedIn: 'root' }]
|
|
673
821
|
}], ctorParameters: () => [] });
|
|
@@ -705,19 +853,19 @@ class LangDirective {
|
|
|
705
853
|
* Localization mode: defines which parts of the element will be translated.
|
|
706
854
|
* @default 'all'
|
|
707
855
|
*/
|
|
708
|
-
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
|
|
856
|
+
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
|
|
709
857
|
/**
|
|
710
858
|
* Explicit key for text content translation.
|
|
711
859
|
*/
|
|
712
|
-
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
|
|
860
|
+
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
|
|
713
861
|
/**
|
|
714
862
|
* Explicit attribute-to-key map for translation.
|
|
715
863
|
*/
|
|
716
|
-
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
|
|
864
|
+
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
|
|
717
865
|
/**
|
|
718
866
|
* Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
|
|
719
867
|
*/
|
|
720
|
-
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
|
|
868
|
+
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
|
|
721
869
|
el = inject(ElementRef);
|
|
722
870
|
renderer = inject(Renderer2);
|
|
723
871
|
service = inject(LangService);
|
|
@@ -913,10 +1061,10 @@ class LangDirective {
|
|
|
913
1061
|
}
|
|
914
1062
|
return this.service.get(key);
|
|
915
1063
|
}
|
|
916
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
917
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
1064
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1065
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.4", type: LangDirective, isStandalone: true, selector: "[reLang]", inputs: { lang: { classPropertyName: "lang", publicName: "reLang", isSignal: true, isRequired: false, transformFunction: null }, langForAttr: { classPropertyName: "langForAttr", publicName: "langForAttr", isSignal: true, isRequired: false, transformFunction: null }, reLangKey: { classPropertyName: "reLangKey", publicName: "reLangKey", isSignal: false, isRequired: false, transformFunction: null }, reLangAttrs: { classPropertyName: "reLangAttrs", publicName: "reLangAttrs", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
918
1066
|
}
|
|
919
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1067
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
|
|
920
1068
|
type: Directive,
|
|
921
1069
|
args: [{ selector: '[reLang]', standalone: true }]
|
|
922
1070
|
}], ctorParameters: () => [], propDecorators: { lang: [{ type: i0.Input, args: [{ isSignal: true, alias: "reLang", required: false }] }], langForAttr: [{ type: i0.Input, args: [{ isSignal: true, alias: "langForAttr", required: false }] }], reLangKey: [{
|
|
@@ -1024,14 +1172,288 @@ class LangPipe {
|
|
|
1024
1172
|
// eslint-disable-next-line no-console
|
|
1025
1173
|
console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
|
|
1026
1174
|
}
|
|
1027
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1028
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.
|
|
1175
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1176
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
|
|
1029
1177
|
}
|
|
1030
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1178
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
|
|
1031
1179
|
type: Pipe,
|
|
1032
1180
|
args: [{ name: 'lang', standalone: true, pure: false }]
|
|
1033
1181
|
}], ctorParameters: () => [] });
|
|
1034
1182
|
|
|
1183
|
+
function deepestActivatedRoute(route) {
|
|
1184
|
+
let current = route;
|
|
1185
|
+
while (current.firstChild) {
|
|
1186
|
+
current = current.firstChild;
|
|
1187
|
+
}
|
|
1188
|
+
return current;
|
|
1189
|
+
}
|
|
1190
|
+
function deepestRouteSnapshot(snapshot) {
|
|
1191
|
+
let current = snapshot;
|
|
1192
|
+
while (current.firstChild) {
|
|
1193
|
+
current = current.firstChild;
|
|
1194
|
+
}
|
|
1195
|
+
return current;
|
|
1196
|
+
}
|
|
1197
|
+
function joinUrl(segments) {
|
|
1198
|
+
return segments.length ? segments.map((segment) => segment.path).join('/') : '';
|
|
1199
|
+
}
|
|
1200
|
+
function snapshotFullPath(snapshot) {
|
|
1201
|
+
return snapshot.pathFromRoot
|
|
1202
|
+
.map((route) => joinUrl(route.url))
|
|
1203
|
+
.filter(Boolean)
|
|
1204
|
+
.join('/');
|
|
1205
|
+
}
|
|
1206
|
+
function snapshotRoutePattern(snapshot) {
|
|
1207
|
+
return snapshot.pathFromRoot
|
|
1208
|
+
.map((route) => route.routeConfig?.path ?? '')
|
|
1209
|
+
.filter(Boolean)
|
|
1210
|
+
.join('/');
|
|
1211
|
+
}
|
|
1212
|
+
function snapshotMergedParams(snapshot) {
|
|
1213
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
|
|
1214
|
+
}
|
|
1215
|
+
function snapshotDeepestParams(snapshot) {
|
|
1216
|
+
const mergedParams = snapshotMergedParams(snapshot);
|
|
1217
|
+
const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
|
|
1218
|
+
if (!keys.length) {
|
|
1219
|
+
return {};
|
|
1220
|
+
}
|
|
1221
|
+
return keys.reduce((acc, key) => {
|
|
1222
|
+
const value = mergedParams[key];
|
|
1223
|
+
if (value !== undefined) {
|
|
1224
|
+
acc[key] = value;
|
|
1225
|
+
}
|
|
1226
|
+
return acc;
|
|
1227
|
+
}, {});
|
|
1228
|
+
}
|
|
1229
|
+
function snapshotMergedData(snapshot) {
|
|
1230
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
|
|
1231
|
+
}
|
|
1232
|
+
function extractRouteParamKeys(path) {
|
|
1233
|
+
return path
|
|
1234
|
+
.split('/')
|
|
1235
|
+
.filter((segment) => segment.startsWith(':'))
|
|
1236
|
+
.map((segment) => segment.slice(1))
|
|
1237
|
+
.filter(Boolean);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Reactive snapshot of the current route (the deepest active route).
|
|
1242
|
+
* Updates on every `NavigationEnd` event.
|
|
1243
|
+
*/
|
|
1244
|
+
class RouteWatcher {
|
|
1245
|
+
router = inject(Router);
|
|
1246
|
+
destroyRef = inject(DestroyRef);
|
|
1247
|
+
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
|
|
1248
|
+
#deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
|
|
1249
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
1250
|
+
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
|
|
1251
|
+
#mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
|
|
1252
|
+
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
|
|
1253
|
+
#routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
|
|
1254
|
+
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
|
|
1255
|
+
/** Params merged from root to deepest route. */
|
|
1256
|
+
params = this.#params.asReadonly();
|
|
1257
|
+
/** Params declared on the deepest route only. */
|
|
1258
|
+
deepestParams = this.#deepestParams.asReadonly();
|
|
1259
|
+
/** Query params from the current navigation. */
|
|
1260
|
+
query = this.#query.asReadonly();
|
|
1261
|
+
/** Deepest route data only. */
|
|
1262
|
+
data = this.#data.asReadonly();
|
|
1263
|
+
/** Route data merged from root to deepest route. */
|
|
1264
|
+
mergedData = this.#mergedData.asReadonly();
|
|
1265
|
+
/** Full current url path assembled from root to deepest route. */
|
|
1266
|
+
url = this.#url.asReadonly();
|
|
1267
|
+
/** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
|
|
1268
|
+
routePattern = this.#routePattern.asReadonly();
|
|
1269
|
+
/** Current url fragment without `#`. */
|
|
1270
|
+
fragment = this.#fragment.asReadonly();
|
|
1271
|
+
/** Combined computed snapshot. */
|
|
1272
|
+
state = computed(() => ({
|
|
1273
|
+
params: this.#params(),
|
|
1274
|
+
deepestParams: this.#deepestParams(),
|
|
1275
|
+
query: this.#query(),
|
|
1276
|
+
data: this.#data(),
|
|
1277
|
+
mergedData: this.#mergedData(),
|
|
1278
|
+
url: this.#url(),
|
|
1279
|
+
routePattern: this.#routePattern(),
|
|
1280
|
+
fragment: this.#fragment(),
|
|
1281
|
+
}), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
1282
|
+
constructor() {
|
|
1283
|
+
const read = () => {
|
|
1284
|
+
const snapshot = this.deepestSnapshot();
|
|
1285
|
+
const nextUrl = snapshotFullPath(snapshot);
|
|
1286
|
+
const nextRoutePattern = snapshotRoutePattern(snapshot);
|
|
1287
|
+
const nextParams = snapshotMergedParams(snapshot);
|
|
1288
|
+
const nextDeepestParams = snapshotDeepestParams(snapshot);
|
|
1289
|
+
const nextMergedData = snapshotMergedData(snapshot);
|
|
1290
|
+
const nextQuery = snapshot.queryParams;
|
|
1291
|
+
const nextData = snapshot.data;
|
|
1292
|
+
const nextFragment = snapshot.fragment ?? null;
|
|
1293
|
+
!deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
|
|
1294
|
+
!deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
|
|
1295
|
+
!deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
|
|
1296
|
+
!deepEqual(nextData, this.#data()) && this.#data.set(nextData);
|
|
1297
|
+
!deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
|
|
1298
|
+
this.#url() !== nextUrl && this.#url.set(nextUrl);
|
|
1299
|
+
this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
|
|
1300
|
+
this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
|
|
1301
|
+
};
|
|
1302
|
+
read();
|
|
1303
|
+
const subscription = this.router.events.subscribe((event) => {
|
|
1304
|
+
if (event instanceof NavigationEnd) {
|
|
1305
|
+
read();
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1309
|
+
}
|
|
1310
|
+
selectData(key, strategy = 'deepest') {
|
|
1311
|
+
return computed(() => {
|
|
1312
|
+
const source = strategy === 'merged' ? this.#mergedData() : this.#data();
|
|
1313
|
+
return source[key];
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
selectParam(key, strategy = 'merged') {
|
|
1317
|
+
return computed(() => {
|
|
1318
|
+
const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
|
|
1319
|
+
return source[key];
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
matchesPath(path) {
|
|
1323
|
+
const current = this.#url();
|
|
1324
|
+
return typeof path === 'string' ? current === path : path.test(current);
|
|
1325
|
+
}
|
|
1326
|
+
deepestSnapshot() {
|
|
1327
|
+
return deepestRouteSnapshot(this.router.routerState.snapshot.root);
|
|
1328
|
+
}
|
|
1329
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1330
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
|
|
1331
|
+
}
|
|
1332
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, decorators: [{
|
|
1333
|
+
type: Injectable,
|
|
1334
|
+
args: [{ providedIn: 'root' }]
|
|
1335
|
+
}], ctorParameters: () => [] });
|
|
1336
|
+
|
|
1337
|
+
const PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY = '__rePresentiaRouteNamespacePreload';
|
|
1338
|
+
const PRESENTIA_ROUTE_NAMESPACES_DATA_KEY = 'presentiaNamespaces';
|
|
1339
|
+
function providePresentiaRouteNamespacePreload() {
|
|
1340
|
+
return makeEnvironmentProviders([
|
|
1341
|
+
provideAppInitializer(() => {
|
|
1342
|
+
const router = inject(Router, { optional: true });
|
|
1343
|
+
const langConfig = inject(LANG_CONFIG);
|
|
1344
|
+
const normalized = normalizeRouteNamespacePreloadConfig(langConfig.routeNamespacePreload);
|
|
1345
|
+
if (!router || !normalized) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (typeof ngDevMode !== 'undefined' && normalized.manifest && Object.keys(normalized.manifest).length === 0) {
|
|
1349
|
+
// eslint-disable-next-line no-console
|
|
1350
|
+
console.warn('[presentia] routeNamespacePreload.manifest is empty. Route-data preload still works, but manifest mode is effectively disabled.');
|
|
1351
|
+
}
|
|
1352
|
+
router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
|
|
1353
|
+
}),
|
|
1354
|
+
]);
|
|
1355
|
+
}
|
|
1356
|
+
function normalizeRouteNamespacePreloadConfig(config) {
|
|
1357
|
+
if (!config) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return {
|
|
1361
|
+
mode: config.mode ?? 'blocking',
|
|
1362
|
+
dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
|
|
1363
|
+
manifest: config.manifest,
|
|
1364
|
+
mergeStrategy: config.mergeStrategy ?? 'append',
|
|
1365
|
+
onError: config.onError ?? 'continue',
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function patchRoutesWithNamespacePreload(routes, config) {
|
|
1369
|
+
return routes.map((route) => patchRouteWithNamespacePreload(route, config));
|
|
1370
|
+
}
|
|
1371
|
+
function patchRouteWithNamespacePreload(route, config) {
|
|
1372
|
+
const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
|
|
1373
|
+
if (route.redirectTo) {
|
|
1374
|
+
return nextChildren === route.children ? route : { ...route, children: nextChildren };
|
|
1375
|
+
}
|
|
1376
|
+
const nextResolve = {
|
|
1377
|
+
...(route.resolve ?? {}),
|
|
1378
|
+
[PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
|
|
1379
|
+
};
|
|
1380
|
+
return {
|
|
1381
|
+
...route,
|
|
1382
|
+
...(nextChildren ? { children: nextChildren } : {}),
|
|
1383
|
+
resolve: nextResolve,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function makeRouteNamespacePreloadResolver(config) {
|
|
1387
|
+
return async (route, state) => {
|
|
1388
|
+
const lang = inject(LangService);
|
|
1389
|
+
const diagnostics = inject(RouteNamespaceDiagnosticsService);
|
|
1390
|
+
const namespaces = resolveRouteNamespaces(route, state, config);
|
|
1391
|
+
diagnostics.registerRouteNamespaces(state.url, namespaces);
|
|
1392
|
+
if (!namespaces.length) {
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
if (config.mode === 'lazy') {
|
|
1396
|
+
queueMicrotask(() => {
|
|
1397
|
+
void lang.loadNamespaces(namespaces);
|
|
1398
|
+
});
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
try {
|
|
1402
|
+
await lang.loadNamespaces(namespaces);
|
|
1403
|
+
}
|
|
1404
|
+
catch (error) {
|
|
1405
|
+
if (config.onError === 'throw') {
|
|
1406
|
+
throw error;
|
|
1407
|
+
}
|
|
1408
|
+
// Keep navigation alive; runtime lazy loading remains a fallback.
|
|
1409
|
+
}
|
|
1410
|
+
return true;
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function resolveRouteNamespaces(route, state, config) {
|
|
1414
|
+
const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
|
|
1415
|
+
const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
|
|
1416
|
+
if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
|
|
1417
|
+
return dataNamespaces;
|
|
1418
|
+
}
|
|
1419
|
+
return uniqueNamespaces([...manifestNamespaces, ...dataNamespaces]);
|
|
1420
|
+
}
|
|
1421
|
+
function readNamespacesFromRouteData(route, dataKey) {
|
|
1422
|
+
return uniqueNamespaces(route.pathFromRoot.flatMap((snapshot) => {
|
|
1423
|
+
const value = snapshot.data?.[dataKey];
|
|
1424
|
+
return Array.isArray(value) ? value : [];
|
|
1425
|
+
}));
|
|
1426
|
+
}
|
|
1427
|
+
function readNamespacesFromManifest(route, state, manifest) {
|
|
1428
|
+
if (!manifest) {
|
|
1429
|
+
return [];
|
|
1430
|
+
}
|
|
1431
|
+
const actualUrl = normalizeUrlPath(state.url);
|
|
1432
|
+
const routePath = snapshotRouteConfigPath(route);
|
|
1433
|
+
return uniqueNamespaces(Object.entries(manifest)
|
|
1434
|
+
.filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
|
|
1435
|
+
.flatMap(([, namespaces]) => namespaces));
|
|
1436
|
+
}
|
|
1437
|
+
function matchesManifestKey(actualUrl, routePath, manifestKey) {
|
|
1438
|
+
const normalizedKey = normalizeUrlPath(manifestKey);
|
|
1439
|
+
return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
|
|
1440
|
+
}
|
|
1441
|
+
function snapshotRouteConfigPath(route) {
|
|
1442
|
+
const templatePath = route.pathFromRoot
|
|
1443
|
+
.map((item) => item.routeConfig?.path ?? '')
|
|
1444
|
+
.filter(Boolean)
|
|
1445
|
+
.join('/');
|
|
1446
|
+
return normalizeUrlPath(templatePath);
|
|
1447
|
+
}
|
|
1448
|
+
function normalizeUrlPath(url) {
|
|
1449
|
+
const [path] = url.split(/[?#]/, 1);
|
|
1450
|
+
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
1451
|
+
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
1452
|
+
}
|
|
1453
|
+
function uniqueNamespaces(namespaces) {
|
|
1454
|
+
return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1035
1457
|
/**
|
|
1036
1458
|
* Type-safe mapping of available theme names.
|
|
1037
1459
|
*
|
|
@@ -1053,27 +1475,12 @@ const themes = {
|
|
|
1053
1475
|
dark: 'dark',
|
|
1054
1476
|
};
|
|
1055
1477
|
/**
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
* This constant defines the prefix applied to HTML elements when the dark theme is active.
|
|
1059
|
-
* It is typically added to the root element or specific components to enable dark theme styles.
|
|
1060
|
-
*
|
|
1061
|
-
* @example
|
|
1062
|
-
* ```typescript
|
|
1063
|
-
* document.body.classList.add(darkThemePrefix); // Applies 're-dark' class
|
|
1064
|
-
* ```
|
|
1478
|
+
* @deprecated Prefer `theme.dom.darkClassName` via `providePresentia(...)`.
|
|
1065
1479
|
*/
|
|
1066
1480
|
const darkThemePrefix = 're-dark';
|
|
1067
1481
|
|
|
1068
1482
|
/**
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* Defines the initial theme settings for the application.
|
|
1072
|
-
* By default, sets the light theme as the active theme.
|
|
1073
|
-
*
|
|
1074
|
-
* This configuration can be overridden when providing `THEME_CONFIG` token
|
|
1075
|
-
* at the module or application level.
|
|
1076
|
-
* ```
|
|
1483
|
+
* @deprecated Prefer configuring themes through `providePresentia({ theme: ... })`.
|
|
1077
1484
|
*/
|
|
1078
1485
|
const defaultThemeConfig = {
|
|
1079
1486
|
defaultTheme: themes.light,
|
|
@@ -1097,6 +1504,28 @@ const defaultThemeConfig = {
|
|
|
1097
1504
|
*/
|
|
1098
1505
|
const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
1099
1506
|
|
|
1507
|
+
/**
|
|
1508
|
+
* @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
|
|
1509
|
+
*/
|
|
1510
|
+
const defaultThemePersistenceAdapter = {
|
|
1511
|
+
getItem: (key) => localStorage.getItem(key),
|
|
1512
|
+
setItem: (key, value) => localStorage.setItem(key, value),
|
|
1513
|
+
};
|
|
1514
|
+
/**
|
|
1515
|
+
* DI token for the persistence adapter used by `ThemeService`
|
|
1516
|
+
* to store and retrieve the selected theme (default key: `'theme'`).
|
|
1517
|
+
*
|
|
1518
|
+
* By default `presentia` provides a `localStorage`-backed adapter.
|
|
1519
|
+
*
|
|
1520
|
+
* Example:
|
|
1521
|
+
* ```ts
|
|
1522
|
+
* { provide: THEME_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
1523
|
+
* ```
|
|
1524
|
+
*/
|
|
1525
|
+
const THEME_PERSISTENCE_ADAPTER = new InjectionToken('RE_THEME_PERSISTENCE_ADAPTER', {
|
|
1526
|
+
factory: () => defaultThemePersistenceAdapter,
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1100
1529
|
/**
|
|
1101
1530
|
* Service for managing application theme.
|
|
1102
1531
|
*
|
|
@@ -1112,11 +1541,18 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
|
1112
1541
|
*/
|
|
1113
1542
|
class ThemeService {
|
|
1114
1543
|
config = inject(THEME_CONFIG);
|
|
1115
|
-
|
|
1116
|
-
|
|
1544
|
+
registry = this.resolveRegistry(this.config?.registry);
|
|
1545
|
+
themeDefault = this.resolveInitialTheme(this.config?.defaultTheme);
|
|
1546
|
+
domStrategy = this.config?.dom?.strategy ?? 'root-class';
|
|
1547
|
+
darkPrefix = this.config?.dom?.darkThemePrefix || this.config?.darkThemePrefix || darkThemePrefix;
|
|
1548
|
+
attributeName = this.config?.dom?.attributeName || 'data-theme';
|
|
1549
|
+
themeClassPrefix = this.config?.dom?.themeClassPrefix;
|
|
1550
|
+
classNameBuilder = this.config?.dom?.classNameBuilder;
|
|
1551
|
+
rootSelector = this.config?.dom?.rootSelector;
|
|
1552
|
+
persistence = inject(THEME_PERSISTENCE_ADAPTER);
|
|
1117
1553
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1118
1554
|
document = inject(DOCUMENT);
|
|
1119
|
-
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
|
|
1555
|
+
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
|
|
1120
1556
|
/**
|
|
1121
1557
|
* Current active theme (`light` or `dark`).
|
|
1122
1558
|
*
|
|
@@ -1126,26 +1562,22 @@ class ThemeService {
|
|
|
1126
1562
|
* <div [class]="themeService.theme()"></div>
|
|
1127
1563
|
* ```
|
|
1128
1564
|
*/
|
|
1129
|
-
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
1565
|
+
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
1130
1566
|
/**
|
|
1131
1567
|
* Convenient flag returning `true` if the light theme is active.
|
|
1132
1568
|
* Suitable for conditional style application or resource selection.
|
|
1133
1569
|
*/
|
|
1134
|
-
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
|
|
1570
|
+
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
|
|
1571
|
+
isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
1135
1572
|
constructor() {
|
|
1136
1573
|
effect(() => {
|
|
1137
1574
|
if (!this.isBrowser) {
|
|
1138
1575
|
this.#theme.set(this.themeDefault);
|
|
1139
1576
|
return;
|
|
1140
1577
|
}
|
|
1141
|
-
const theme = this.resolveTheme(
|
|
1578
|
+
const theme = this.resolveTheme(this.getStoredTheme());
|
|
1142
1579
|
this.switch(theme);
|
|
1143
1580
|
});
|
|
1144
|
-
effect(() => {
|
|
1145
|
-
if (this.isBrowser) {
|
|
1146
|
-
localStorage.setItem('theme', this.#theme());
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
1581
|
}
|
|
1150
1582
|
/**
|
|
1151
1583
|
* Switches theme.
|
|
@@ -1157,28 +1589,116 @@ class ThemeService {
|
|
|
1157
1589
|
*/
|
|
1158
1590
|
switch(theme) {
|
|
1159
1591
|
const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
|
|
1160
|
-
const newTheme = requestedTheme ??
|
|
1592
|
+
const newTheme = requestedTheme ?? this.nextTheme();
|
|
1161
1593
|
if (this.isBrowser) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
newTheme === themes.light && html.classList.remove(this.darkPrefix);
|
|
1594
|
+
this.applyThemeToDom(newTheme);
|
|
1595
|
+
this.persistTheme(newTheme);
|
|
1165
1596
|
}
|
|
1166
1597
|
this.#theme.set(newTheme);
|
|
1167
1598
|
}
|
|
1599
|
+
is(theme) {
|
|
1600
|
+
const allowed = Array.isArray(theme) ? theme : [theme];
|
|
1601
|
+
return allowed.includes(this.#theme());
|
|
1602
|
+
}
|
|
1168
1603
|
resolveTheme(theme) {
|
|
1169
|
-
|
|
1604
|
+
if (theme && this.registry.includes(theme)) {
|
|
1605
|
+
return theme;
|
|
1606
|
+
}
|
|
1607
|
+
return this.themeDefault;
|
|
1608
|
+
}
|
|
1609
|
+
resolveRegistry(registry) {
|
|
1610
|
+
return registry?.length ? registry : [themes.light, themes.dark];
|
|
1611
|
+
}
|
|
1612
|
+
resolveInitialTheme(theme) {
|
|
1613
|
+
const next = theme ?? themes.light;
|
|
1614
|
+
return this.registry.includes(next) ? next : (this.registry[0] ?? themes.light);
|
|
1615
|
+
}
|
|
1616
|
+
getStoredTheme() {
|
|
1617
|
+
return this.persistence.getItem('theme');
|
|
1618
|
+
}
|
|
1619
|
+
persistTheme(theme) {
|
|
1620
|
+
this.persistence.setItem('theme', theme);
|
|
1621
|
+
}
|
|
1622
|
+
applyThemeToDom(theme) {
|
|
1623
|
+
const root = this.resolveRootElement();
|
|
1624
|
+
if (!root) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (this.domStrategy === 'data-attribute') {
|
|
1628
|
+
root.setAttribute(this.attributeName, theme);
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
this.clearThemeClasses(root);
|
|
1632
|
+
if (theme === themes.dark) {
|
|
1633
|
+
root.classList.add(this.darkPrefix);
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
root.classList.remove(this.darkPrefix);
|
|
1637
|
+
}
|
|
1638
|
+
const className = this.resolveThemeClassName(theme);
|
|
1639
|
+
if (className) {
|
|
1640
|
+
root.classList.add(className);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
nextTheme() {
|
|
1644
|
+
const currentIndex = this.registry.indexOf(this.#theme());
|
|
1645
|
+
if (currentIndex < 0) {
|
|
1646
|
+
return this.themeDefault;
|
|
1647
|
+
}
|
|
1648
|
+
return this.registry[(currentIndex + 1) % this.registry.length] ?? this.themeDefault;
|
|
1649
|
+
}
|
|
1650
|
+
resolveRootElement() {
|
|
1651
|
+
if (!this.rootSelector) {
|
|
1652
|
+
return this.document.documentElement;
|
|
1653
|
+
}
|
|
1654
|
+
return this.document.querySelector(this.rootSelector);
|
|
1655
|
+
}
|
|
1656
|
+
resolveThemeClassName(theme) {
|
|
1657
|
+
if (this.classNameBuilder) {
|
|
1658
|
+
return this.classNameBuilder(theme);
|
|
1659
|
+
}
|
|
1660
|
+
if (this.themeClassPrefix) {
|
|
1661
|
+
return `${this.themeClassPrefix}${theme}`;
|
|
1662
|
+
}
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
clearThemeClasses(root) {
|
|
1666
|
+
if (!this.classNameBuilder && !this.themeClassPrefix) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
for (const registeredTheme of this.registry) {
|
|
1670
|
+
const className = this.resolveThemeClassName(registeredTheme);
|
|
1671
|
+
if (className) {
|
|
1672
|
+
root.classList.remove(className);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1170
1675
|
}
|
|
1171
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1172
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
1676
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1677
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
1173
1678
|
}
|
|
1174
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1679
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
|
|
1175
1680
|
type: Injectable,
|
|
1176
1681
|
args: [{
|
|
1177
1682
|
providedIn: 'root',
|
|
1178
1683
|
}]
|
|
1179
1684
|
}], ctorParameters: () => [] });
|
|
1180
1685
|
|
|
1686
|
+
let warnedLegacyProvider = false;
|
|
1687
|
+
/**
|
|
1688
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1689
|
+
*/
|
|
1181
1690
|
function provideReInit(config) {
|
|
1691
|
+
if (typeof ngDevMode !== 'undefined' && !warnedLegacyProvider) {
|
|
1692
|
+
warnedLegacyProvider = true;
|
|
1693
|
+
// eslint-disable-next-line no-console
|
|
1694
|
+
console.warn('[presentia] provideReInit(...) is the legacy 1.x provider. Prefer providePresentia(...) for the grouped v2 config API.');
|
|
1695
|
+
}
|
|
1696
|
+
return buildPresentiaProviders(config);
|
|
1697
|
+
}
|
|
1698
|
+
function __resetPresentiaProviderWarningsForTests() {
|
|
1699
|
+
warnedLegacyProvider = false;
|
|
1700
|
+
}
|
|
1701
|
+
function buildPresentiaProviders(config) {
|
|
1182
1702
|
return makeEnvironmentProviders([
|
|
1183
1703
|
{ provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
|
|
1184
1704
|
{ provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
|
|
@@ -1192,13 +1712,88 @@ function provideReInit(config) {
|
|
|
1192
1712
|
{ provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
|
|
1193
1713
|
{ provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
|
|
1194
1714
|
{ provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
|
|
1715
|
+
{ provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
|
|
1195
1716
|
{ provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
|
|
1196
1717
|
{ provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
|
|
1197
1718
|
{ provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
|
|
1198
1719
|
{ provide: LOCALE_ID, useValue: config.locale.defaultLang ?? 'ru' },
|
|
1720
|
+
providePresentiaRouteNamespacePreload(),
|
|
1721
|
+
]);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function providePresentia(config) {
|
|
1725
|
+
const routesConfig = config.lang.preload?.routes;
|
|
1726
|
+
const diagnosticsEnabled = config.lang.diagnostics?.lateNamespaceLoads;
|
|
1727
|
+
const routeNamespacePreload = routesConfig || diagnosticsEnabled
|
|
1728
|
+
? { ...routesConfig, diagnostics: diagnosticsEnabled ?? routesConfig?.diagnostics }
|
|
1729
|
+
: undefined;
|
|
1730
|
+
const extraProviders = [];
|
|
1731
|
+
if (config.theme?.persistence === 'none') {
|
|
1732
|
+
extraProviders.push({
|
|
1733
|
+
provide: THEME_PERSISTENCE_ADAPTER,
|
|
1734
|
+
useValue: {
|
|
1735
|
+
getItem: () => null,
|
|
1736
|
+
setItem: () => undefined,
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
else if (config.theme?.persistenceAdapter) {
|
|
1741
|
+
extraProviders.push({
|
|
1742
|
+
provide: THEME_PERSISTENCE_ADAPTER,
|
|
1743
|
+
useValue: config.theme.persistenceAdapter,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
return makeEnvironmentProviders([
|
|
1747
|
+
buildPresentiaProviders({
|
|
1748
|
+
locale: {
|
|
1749
|
+
url: config.lang.source.url,
|
|
1750
|
+
isFromAssets: config.lang.source.fromAssets,
|
|
1751
|
+
defaultLang: config.lang.source.defaultLang,
|
|
1752
|
+
fallbackLang: config.lang.source.fallbackLang,
|
|
1753
|
+
supportedLangs: config.lang.source.supportedLangs,
|
|
1754
|
+
kgValue: config.lang.source.kgValue,
|
|
1755
|
+
defaultValue: config.lang.rendering?.missingValue,
|
|
1756
|
+
preloadNamespaces: config.lang.preload?.global,
|
|
1757
|
+
requestBuilder: config.lang.transport?.requestBuilder,
|
|
1758
|
+
requestOptionsFactory: config.lang.transport?.requestOptionsFactory,
|
|
1759
|
+
responseAdapter: config.lang.transport?.responseAdapter,
|
|
1760
|
+
batchRequestBuilder: config.lang.transport?.batchRequestBuilder,
|
|
1761
|
+
batchResponseAdapter: config.lang.transport?.batchResponseAdapter,
|
|
1762
|
+
namespaceCache: config.lang.cache,
|
|
1763
|
+
routeNamespacePreload,
|
|
1764
|
+
maxBatchSize: config.lang.transport?.maxBatchSize,
|
|
1765
|
+
},
|
|
1766
|
+
theme: config.theme
|
|
1767
|
+
? {
|
|
1768
|
+
registry: config.theme.registry,
|
|
1769
|
+
defaultTheme: config.theme.defaultTheme,
|
|
1770
|
+
darkThemePrefix: config.theme.dom?.darkClassName,
|
|
1771
|
+
dom: config.theme.dom
|
|
1772
|
+
? {
|
|
1773
|
+
strategy: config.theme.dom.strategy,
|
|
1774
|
+
rootSelector: config.theme.dom.rootSelector,
|
|
1775
|
+
darkThemePrefix: config.theme.dom.darkClassName,
|
|
1776
|
+
attributeName: config.theme.dom.attributeName,
|
|
1777
|
+
themeClassPrefix: config.theme.dom.classPrefix,
|
|
1778
|
+
classNameBuilder: config.theme.dom.classNameBuilder,
|
|
1779
|
+
}
|
|
1780
|
+
: undefined,
|
|
1781
|
+
}
|
|
1782
|
+
: undefined,
|
|
1783
|
+
breakpoints: config.adaptive?.breakpoints,
|
|
1784
|
+
langPipe: {
|
|
1785
|
+
placeholder: config.lang.rendering?.placeholder,
|
|
1786
|
+
},
|
|
1787
|
+
langMissingKeyHandler: config.lang.missingKeyHandler,
|
|
1788
|
+
}),
|
|
1789
|
+
...extraProviders,
|
|
1199
1790
|
]);
|
|
1200
1791
|
}
|
|
1201
1792
|
|
|
1793
|
+
/**
|
|
1794
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1795
|
+
*/
|
|
1796
|
+
|
|
1202
1797
|
/**
|
|
1203
1798
|
* Service for managing page SEO metadata.
|
|
1204
1799
|
*
|
|
@@ -1326,10 +1921,10 @@ class SeoService {
|
|
|
1326
1921
|
link.href = href;
|
|
1327
1922
|
}
|
|
1328
1923
|
}
|
|
1329
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1330
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
1924
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1925
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, providedIn: 'root' });
|
|
1331
1926
|
}
|
|
1332
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1927
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, decorators: [{
|
|
1333
1928
|
type: Injectable,
|
|
1334
1929
|
args: [{ providedIn: 'root' }]
|
|
1335
1930
|
}] });
|
|
@@ -1355,7 +1950,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
1355
1950
|
class SeoRouteListener {
|
|
1356
1951
|
router = inject(Router);
|
|
1357
1952
|
seo = inject(SeoService);
|
|
1358
|
-
ar = inject(ActivatedRoute);
|
|
1359
1953
|
destroyRef = inject(DestroyRef);
|
|
1360
1954
|
initialized = false;
|
|
1361
1955
|
baseUrl = '';
|
|
@@ -1369,9 +1963,9 @@ class SeoRouteListener {
|
|
|
1369
1963
|
init(baseUrl) {
|
|
1370
1964
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
1371
1965
|
const applyRouteSeo = () => {
|
|
1372
|
-
const
|
|
1373
|
-
const data =
|
|
1374
|
-
const url = data.canonical
|
|
1966
|
+
const snapshot = deepestRouteSnapshot(this.router.routerState.snapshot.root);
|
|
1967
|
+
const data = snapshotMergedData(snapshot);
|
|
1968
|
+
const url = resolveCanonicalUrl(data.canonical, this.baseUrl, snapshotFullPath(snapshot));
|
|
1375
1969
|
data.title && this.seo.setTitle(data.title);
|
|
1376
1970
|
data.description && this.seo.setDescription(data.description);
|
|
1377
1971
|
data.twitter && this.seo.setTwitter(data.twitter);
|
|
@@ -1397,123 +1991,33 @@ class SeoRouteListener {
|
|
|
1397
1991
|
});
|
|
1398
1992
|
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1399
1993
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
* This is used to extract route data from the currently active leaf route.
|
|
1403
|
-
*
|
|
1404
|
-
* @param r - The root activated route to start traversing from.
|
|
1405
|
-
* @returns The deepest child route in the hierarchy.
|
|
1406
|
-
*/
|
|
1407
|
-
deepest(r) {
|
|
1408
|
-
let cur = r;
|
|
1409
|
-
while (cur.firstChild) {
|
|
1410
|
-
cur = cur.firstChild;
|
|
1411
|
-
}
|
|
1412
|
-
return cur;
|
|
1413
|
-
}
|
|
1414
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1415
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
|
|
1994
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1995
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
|
|
1416
1996
|
}
|
|
1417
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1997
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, decorators: [{
|
|
1418
1998
|
type: Injectable,
|
|
1419
1999
|
args: [{ providedIn: 'root' }]
|
|
1420
2000
|
}] });
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
* Updates on every `NavigationEnd` event.
|
|
1425
|
-
*
|
|
1426
|
-
* Provides:
|
|
1427
|
-
* - `params` / `query` — strings (as in Angular Router)
|
|
1428
|
-
* - `data` — arbitrary data (from route configuration/resolvers)
|
|
1429
|
-
* - `url` — string assembled from `UrlSegment[]`
|
|
1430
|
-
* - `fragment` — hash (#section)
|
|
1431
|
-
* - `selectData(key)` — type-safe selector for `data`
|
|
1432
|
-
* - `state` — combined computed object (convenient for single subscriber)
|
|
1433
|
-
*/
|
|
1434
|
-
class RouteWatcher {
|
|
1435
|
-
router = inject(Router);
|
|
1436
|
-
destroyRef = inject(DestroyRef);
|
|
1437
|
-
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : []));
|
|
1438
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
1439
|
-
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
|
|
1440
|
-
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
|
|
1441
|
-
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
|
|
1442
|
-
/** Signal for tracking and retrieving URL parameters */
|
|
1443
|
-
params = this.#params.asReadonly();
|
|
1444
|
-
/** Signal for tracking and retrieving query parameters */
|
|
1445
|
-
query = this.#query.asReadonly();
|
|
1446
|
-
/** Signal for tracking and retrieving route data */
|
|
1447
|
-
data = this.#data.asReadonly();
|
|
1448
|
-
/** Signal for tracking and retrieving URL */
|
|
1449
|
-
url = this.#url.asReadonly();
|
|
1450
|
-
/** Signal for tracking and retrieving URL fragment */
|
|
1451
|
-
fragment = this.#fragment.asReadonly();
|
|
1452
|
-
/** Combined computed snapshot (to avoid multiple effects) */
|
|
1453
|
-
state = computed(() => ({
|
|
1454
|
-
params: this.#params(),
|
|
1455
|
-
query: this.#query(),
|
|
1456
|
-
data: this.#data(),
|
|
1457
|
-
url: this.#url(),
|
|
1458
|
-
fragment: this.#fragment(),
|
|
1459
|
-
}), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
1460
|
-
constructor() {
|
|
1461
|
-
const read = () => {
|
|
1462
|
-
const snap = this.deepestSnapshot();
|
|
1463
|
-
const url = snapshotFullPath(snap);
|
|
1464
|
-
const params = snapshotMergedParams(snap);
|
|
1465
|
-
!deepEqual(params, this.#params()) && this.#params.set(params);
|
|
1466
|
-
!deepEqual(snap.queryParams, this.#query()) && this.#query.set(snap.queryParams);
|
|
1467
|
-
!deepEqual(snap.data, this.#data()) && this.#data.set(snap.data);
|
|
1468
|
-
this.#url() !== url && this.#url.set(url);
|
|
1469
|
-
this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
|
|
1470
|
-
};
|
|
1471
|
-
read();
|
|
1472
|
-
const subscription = this.router.events.subscribe((event) => {
|
|
1473
|
-
if (event instanceof NavigationEnd) {
|
|
1474
|
-
read();
|
|
1475
|
-
}
|
|
1476
|
-
});
|
|
1477
|
-
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
2001
|
+
function resolveCanonicalUrl(canonical, baseUrl, path) {
|
|
2002
|
+
if (!canonical) {
|
|
2003
|
+
return joinBaseUrl(baseUrl, path);
|
|
1478
2004
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
return computed(() => this.#data()[key]);
|
|
2005
|
+
if (/^https?:\/\//i.test(canonical)) {
|
|
2006
|
+
return canonical;
|
|
1482
2007
|
}
|
|
1483
|
-
|
|
1484
|
-
// work with snapshot — we need a "frozen" point at NavigationEnd moment
|
|
1485
|
-
let snap = this.router.routerState.snapshot.root;
|
|
1486
|
-
while (snap.firstChild) {
|
|
1487
|
-
snap = snap.firstChild;
|
|
1488
|
-
}
|
|
1489
|
-
return snap;
|
|
1490
|
-
}
|
|
1491
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1492
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
|
|
1493
|
-
}
|
|
1494
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, decorators: [{
|
|
1495
|
-
type: Injectable,
|
|
1496
|
-
args: [{ providedIn: 'root' }]
|
|
1497
|
-
}], ctorParameters: () => [] });
|
|
1498
|
-
/** Joins `UrlSegment[]` into a path string */
|
|
1499
|
-
function joinUrl(segments) {
|
|
1500
|
-
return segments.length ? segments.map((s) => s.path).join('/') : '';
|
|
2008
|
+
return joinBaseUrl(baseUrl, canonical);
|
|
1501
2009
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
}
|
|
1509
|
-
/** Merges params from root to the deepest snapshot (child keys override parent). */
|
|
1510
|
-
function snapshotMergedParams(snap) {
|
|
1511
|
-
return snap.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
|
|
2010
|
+
function joinBaseUrl(baseUrl, path) {
|
|
2011
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
2012
|
+
if (!baseUrl) {
|
|
2013
|
+
return normalizedPath ? `/${normalizedPath}` : '/';
|
|
2014
|
+
}
|
|
2015
|
+
return normalizedPath ? `${baseUrl}/${normalizedPath}` : baseUrl;
|
|
1512
2016
|
}
|
|
1513
2017
|
|
|
1514
2018
|
/**
|
|
1515
2019
|
* Generated bundle index. Do not edit.
|
|
1516
2020
|
*/
|
|
1517
2021
|
|
|
1518
|
-
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, innerLangVal, provideReInit, themes };
|
|
2022
|
+
export { AdaptiveService, DEVICE_BREAKPOINTS, IfDeviceDirective, LANG_CONFIG, LANG_MISSING_KEY_HANDLER, LANG_PERSISTENCE_ADAPTER, LANG_PIPE_CONFIG, LangDirective, LangPipe, LangService, PRESENTIA_ROUTE_NAMESPACES_DATA_KEY, RouteNamespaceDiagnosticsService, RouteWatcher, SeoRouteListener, SeoService, THEME_CONFIG, THEME_PERSISTENCE_ADAPTER, ThemeService, darkThemePrefix, defaultBreakpoints, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
1519
2023
|
//# sourceMappingURL=reforgium-presentia.mjs.map
|