@reforgium/presentia 1.4.4 → 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-lang-keys.mjs +1 -1
- package/bin/presentia-gen-namespaces.mjs +1248 -0
- package/fesm2022/reforgium-presentia.mjs +750 -197
- package/package.json +27 -7
- package/types/reforgium-presentia.d.ts +292 -89
|
@@ -1,14 +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
|
-
import { fromEvent, debounceTime, firstValueFrom, tap, filter as filter$1 } from 'rxjs';
|
|
8
7
|
import { HttpClient } from '@angular/common/http';
|
|
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
|
-
import { filter } from 'rxjs/operators';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Default breakpoints for device type detection.
|
|
@@ -74,10 +73,10 @@ const DEVICE_BREAKPOINTS = new InjectionToken('RE_DEVICE_BREAKPOINTS', {
|
|
|
74
73
|
*/
|
|
75
74
|
class AdaptiveService {
|
|
76
75
|
/** @internal Signal of the current device type. */
|
|
77
|
-
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : []));
|
|
76
|
+
#device = signal('desktop', ...(ngDevMode ? [{ debugName: "#device" }] : /* istanbul ignore next */ []));
|
|
78
77
|
/** @internal Signals of the current window width and height. */
|
|
79
|
-
#width = signal(0, ...(ngDevMode ? [{ debugName: "#width" }] : []));
|
|
80
|
-
#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 */ []));
|
|
81
80
|
/**
|
|
82
81
|
* Current device type (reactive signal).
|
|
83
82
|
* Possible values: `'desktop' | 'tablet' | 'mobile'`.
|
|
@@ -100,17 +99,21 @@ class AdaptiveService {
|
|
|
100
99
|
* Computed signal indicating whether the current device is a desktop.
|
|
101
100
|
* Used for conditional rendering or layout configuration.
|
|
102
101
|
*/
|
|
103
|
-
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 */ []));
|
|
104
106
|
/**
|
|
105
107
|
* Computed signal determining whether the screen is in portrait orientation.
|
|
106
108
|
* Returns `true` if window height is greater than width.
|
|
107
109
|
*/
|
|
108
|
-
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : []));
|
|
110
|
+
isPortrait = computed(() => this.#height() > this.#width(), ...(ngDevMode ? [{ debugName: "isPortrait" }] : /* istanbul ignore next */ []));
|
|
109
111
|
deviceBreakpoints = inject(DEVICE_BREAKPOINTS);
|
|
110
112
|
devicePriority = Object.keys(this.deviceBreakpoints);
|
|
111
113
|
destroyRef = inject(DestroyRef);
|
|
112
114
|
breakpointObserver = inject(BreakpointObserver);
|
|
113
115
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
116
|
+
resizeDebounceId = null;
|
|
114
117
|
constructor() {
|
|
115
118
|
if (!this.isBrowser) {
|
|
116
119
|
this.#device.set('desktop');
|
|
@@ -129,20 +132,47 @@ class AdaptiveService {
|
|
|
129
132
|
this.#device.set(device);
|
|
130
133
|
}
|
|
131
134
|
});
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
const onResize = () => {
|
|
136
|
+
this.resizeDebounceId !== null && clearTimeout(this.resizeDebounceId);
|
|
137
|
+
this.resizeDebounceId = setTimeout(() => {
|
|
138
|
+
this.resizeDebounceId = null;
|
|
139
|
+
const width = window.innerWidth;
|
|
140
|
+
const height = window.innerHeight;
|
|
141
|
+
this.#width() !== width && this.#width.set(width);
|
|
142
|
+
this.#height() !== height && this.#height.set(height);
|
|
143
|
+
}, 100);
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener('resize', onResize, { passive: true });
|
|
146
|
+
this.destroyRef.onDestroy(() => {
|
|
147
|
+
window.removeEventListener('resize', onResize);
|
|
148
|
+
if (this.resizeDebounceId !== null) {
|
|
149
|
+
clearTimeout(this.resizeDebounceId);
|
|
150
|
+
this.resizeDebounceId = null;
|
|
151
|
+
}
|
|
140
152
|
});
|
|
141
153
|
}
|
|
142
|
-
|
|
143
|
-
|
|
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' });
|
|
144
174
|
}
|
|
145
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AdaptiveService, decorators: [{
|
|
146
176
|
type: Injectable,
|
|
147
177
|
args: [{
|
|
148
178
|
providedIn: 'root',
|
|
@@ -170,8 +200,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
170
200
|
* the template is automatically added or removed from the DOM.
|
|
171
201
|
*/
|
|
172
202
|
class IfDeviceDirective {
|
|
173
|
-
deviceInput = signal(undefined, ...(ngDevMode ? [{ debugName: "deviceInput" }] : []));
|
|
174
|
-
|
|
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 */ []));
|
|
175
207
|
tpl = inject(TemplateRef);
|
|
176
208
|
vcr = inject(ViewContainerRef);
|
|
177
209
|
adaptive = inject(AdaptiveService);
|
|
@@ -181,20 +213,37 @@ class IfDeviceDirective {
|
|
|
181
213
|
constructor() {
|
|
182
214
|
effect(() => {
|
|
183
215
|
const device = this.deviceInput();
|
|
216
|
+
const atLeast = this.atLeastInput();
|
|
217
|
+
const between = this.betweenInput();
|
|
184
218
|
if (device) {
|
|
185
219
|
this.allowedDevices = Array.isArray(device) ? device : [device];
|
|
186
220
|
}
|
|
221
|
+
else if (!atLeast && !between) {
|
|
222
|
+
this.allowedDevices = [];
|
|
223
|
+
}
|
|
187
224
|
this.updateView();
|
|
188
225
|
});
|
|
189
226
|
}
|
|
190
227
|
set reIfDevice(value) {
|
|
191
228
|
this.deviceInput.set(value);
|
|
192
229
|
}
|
|
230
|
+
set reIfDeviceAtLeast(value) {
|
|
231
|
+
this.atLeastInput.set(value);
|
|
232
|
+
}
|
|
233
|
+
set reIfDeviceBetween(value) {
|
|
234
|
+
this.betweenInput.set(value);
|
|
235
|
+
}
|
|
193
236
|
set inverse(value) {
|
|
194
237
|
this.inverseInput.set(!!value);
|
|
195
238
|
}
|
|
196
239
|
updateView() {
|
|
197
|
-
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());
|
|
198
247
|
const shouldShow = this.inverseInput() ? !isAllowed : isAllowed;
|
|
199
248
|
if (shouldShow && !this.hasView) {
|
|
200
249
|
this.vcr.createEmbeddedView(this.tpl);
|
|
@@ -205,18 +254,24 @@ class IfDeviceDirective {
|
|
|
205
254
|
this.hasView = false;
|
|
206
255
|
}
|
|
207
256
|
}
|
|
208
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
209
|
-
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 });
|
|
210
259
|
}
|
|
211
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
260
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IfDeviceDirective, decorators: [{
|
|
212
261
|
type: Directive,
|
|
213
262
|
args: [{
|
|
214
|
-
selector: '[reIfDevice]',
|
|
263
|
+
selector: '[reIfDevice],[reIfDeviceAtLeast],[reIfDeviceBetween]',
|
|
215
264
|
standalone: true,
|
|
216
265
|
}]
|
|
217
266
|
}], ctorParameters: () => [], propDecorators: { reIfDevice: [{
|
|
218
267
|
type: Input,
|
|
219
268
|
args: ['reIfDevice']
|
|
269
|
+
}], reIfDeviceAtLeast: [{
|
|
270
|
+
type: Input,
|
|
271
|
+
args: ['reIfDeviceAtLeast']
|
|
272
|
+
}], reIfDeviceBetween: [{
|
|
273
|
+
type: Input,
|
|
274
|
+
args: ['reIfDeviceBetween']
|
|
220
275
|
}], inverse: [{
|
|
221
276
|
type: Input
|
|
222
277
|
}] } });
|
|
@@ -225,6 +280,19 @@ const innerLangVal = Symbol('reInnerLangVal');
|
|
|
225
280
|
|
|
226
281
|
const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
|
|
227
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
|
+
|
|
228
296
|
/**
|
|
229
297
|
* Injection token for providing locale configuration to the language module.
|
|
230
298
|
*
|
|
@@ -245,6 +313,67 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
|
|
|
245
313
|
*/
|
|
246
314
|
const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
|
|
247
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
|
+
|
|
248
377
|
/**
|
|
249
378
|
* LangService provides functionality for managing and tracking language settings
|
|
250
379
|
* and translations in the application. It is designed to handle localization needs,
|
|
@@ -258,12 +387,13 @@ class LangService {
|
|
|
258
387
|
http = inject(HttpClient);
|
|
259
388
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
260
389
|
missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
|
|
390
|
+
routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
|
|
261
391
|
supportedLangSet = new Set([
|
|
262
392
|
...LangService.BUILTIN_LANGS,
|
|
263
393
|
...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
|
|
264
394
|
]);
|
|
265
|
-
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
|
|
266
|
-
#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 */ []));
|
|
267
397
|
#loadedNamespaces = new Set();
|
|
268
398
|
#pendingLoads = new Map();
|
|
269
399
|
#pendingBatchLoads = new Map();
|
|
@@ -280,7 +410,7 @@ class LangService {
|
|
|
280
410
|
currentLang = computed(() => {
|
|
281
411
|
const lang = this.#lang();
|
|
282
412
|
return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
|
|
283
|
-
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
|
|
413
|
+
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
|
|
284
414
|
/**
|
|
285
415
|
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
286
416
|
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
@@ -325,6 +455,9 @@ class LangService {
|
|
|
325
455
|
}
|
|
326
456
|
return baseValue;
|
|
327
457
|
}
|
|
458
|
+
has(query) {
|
|
459
|
+
return this.getChainedValue(query) !== undefined;
|
|
460
|
+
}
|
|
328
461
|
observe(query, params) {
|
|
329
462
|
const [ns] = query.split('.');
|
|
330
463
|
if (!this.isNamespaceLoaded(ns)) {
|
|
@@ -345,6 +478,7 @@ class LangService {
|
|
|
345
478
|
if (this.isNamespaceValid(key)) {
|
|
346
479
|
return;
|
|
347
480
|
}
|
|
481
|
+
this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
|
|
348
482
|
this.tryExpireNamespace(key);
|
|
349
483
|
if (this.#pendingLoads.has(key)) {
|
|
350
484
|
return this.#pendingLoads.get(key);
|
|
@@ -399,14 +533,27 @@ class LangService {
|
|
|
399
533
|
await Promise.all(toLoad.map((ns) => this.loadNamespace(ns)));
|
|
400
534
|
return;
|
|
401
535
|
}
|
|
402
|
-
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);
|
|
403
550
|
if (this.#pendingBatchLoads.has(batchKey)) {
|
|
404
551
|
return this.#pendingBatchLoads.get(batchKey);
|
|
405
552
|
}
|
|
406
553
|
const promise = (async () => {
|
|
407
554
|
try {
|
|
408
555
|
const context = {
|
|
409
|
-
namespaces
|
|
556
|
+
namespaces,
|
|
410
557
|
lang: requestedLang,
|
|
411
558
|
isFromAssets: this.config.isFromAssets,
|
|
412
559
|
baseUrl: this.config.url,
|
|
@@ -418,7 +565,7 @@ class LangService {
|
|
|
418
565
|
}
|
|
419
566
|
const payloads = this.config.batchResponseAdapter(response, this.toBatchResponseContext(context));
|
|
420
567
|
const merged = {};
|
|
421
|
-
for (const ns of
|
|
568
|
+
for (const ns of namespaces) {
|
|
422
569
|
const nsPayload = payloads[ns];
|
|
423
570
|
if (!nsPayload) {
|
|
424
571
|
continue;
|
|
@@ -438,6 +585,19 @@ class LangService {
|
|
|
438
585
|
this.#pendingBatchLoads.set(batchKey, promise);
|
|
439
586
|
return promise;
|
|
440
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
|
+
}
|
|
441
601
|
evictNamespace(ns) {
|
|
442
602
|
const key = this.makeNamespaceKey(ns);
|
|
443
603
|
if (!this.#loadedNamespaces.has(key)) {
|
|
@@ -652,10 +812,10 @@ class LangService {
|
|
|
652
812
|
.map((lang) => lang.trim().toLowerCase())
|
|
653
813
|
.filter((lang) => !!lang && LangService.LANG_CODE_RE.test(lang));
|
|
654
814
|
}
|
|
655
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
656
|
-
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' });
|
|
657
817
|
}
|
|
658
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
818
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, decorators: [{
|
|
659
819
|
type: Injectable,
|
|
660
820
|
args: [{ providedIn: 'root' }]
|
|
661
821
|
}], ctorParameters: () => [] });
|
|
@@ -693,19 +853,19 @@ class LangDirective {
|
|
|
693
853
|
* Localization mode: defines which parts of the element will be translated.
|
|
694
854
|
* @default 'all'
|
|
695
855
|
*/
|
|
696
|
-
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
|
|
856
|
+
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
|
|
697
857
|
/**
|
|
698
858
|
* Explicit key for text content translation.
|
|
699
859
|
*/
|
|
700
|
-
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
|
|
860
|
+
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
|
|
701
861
|
/**
|
|
702
862
|
* Explicit attribute-to-key map for translation.
|
|
703
863
|
*/
|
|
704
|
-
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
|
|
864
|
+
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
|
|
705
865
|
/**
|
|
706
866
|
* Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
|
|
707
867
|
*/
|
|
708
|
-
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
|
|
868
|
+
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
|
|
709
869
|
el = inject(ElementRef);
|
|
710
870
|
renderer = inject(Renderer2);
|
|
711
871
|
service = inject(LangService);
|
|
@@ -901,10 +1061,10 @@ class LangDirective {
|
|
|
901
1061
|
}
|
|
902
1062
|
return this.service.get(key);
|
|
903
1063
|
}
|
|
904
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
905
|
-
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 });
|
|
906
1066
|
}
|
|
907
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1067
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
|
|
908
1068
|
type: Directive,
|
|
909
1069
|
args: [{ selector: '[reLang]', standalone: true }]
|
|
910
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: [{
|
|
@@ -931,6 +1091,7 @@ const LANG_PIPE_CONFIG = new InjectionToken('RE_LANG_PIPE_CONFIG');
|
|
|
931
1091
|
*/
|
|
932
1092
|
class LangPipe {
|
|
933
1093
|
cache = new Map();
|
|
1094
|
+
warnedUnresolved = new Set();
|
|
934
1095
|
lang = inject(LangService);
|
|
935
1096
|
injector = inject(Injector);
|
|
936
1097
|
config;
|
|
@@ -977,6 +1138,9 @@ class LangPipe {
|
|
|
977
1138
|
return placeholder;
|
|
978
1139
|
}
|
|
979
1140
|
}
|
|
1141
|
+
if (ns && this.lang.isNamespaceLoaded(ns) && !this.lang.has(query)) {
|
|
1142
|
+
this.warnUnresolvedKey(query);
|
|
1143
|
+
}
|
|
980
1144
|
return value;
|
|
981
1145
|
}
|
|
982
1146
|
makeKey(query, params) {
|
|
@@ -997,14 +1161,299 @@ class LangPipe {
|
|
|
997
1161
|
this.cache.delete(firstKey);
|
|
998
1162
|
}
|
|
999
1163
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1164
|
+
warnUnresolvedKey(query) {
|
|
1165
|
+
if (typeof ngDevMode === 'undefined') {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (this.warnedUnresolved.has(query)) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
this.warnedUnresolved.add(query);
|
|
1172
|
+
// eslint-disable-next-line no-console
|
|
1173
|
+
console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
|
|
1174
|
+
}
|
|
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 });
|
|
1002
1177
|
}
|
|
1003
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1178
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
|
|
1004
1179
|
type: Pipe,
|
|
1005
1180
|
args: [{ name: 'lang', standalone: true, pure: false }]
|
|
1006
1181
|
}], ctorParameters: () => [] });
|
|
1007
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
|
+
|
|
1008
1457
|
/**
|
|
1009
1458
|
* Type-safe mapping of available theme names.
|
|
1010
1459
|
*
|
|
@@ -1026,27 +1475,12 @@ const themes = {
|
|
|
1026
1475
|
dark: 'dark',
|
|
1027
1476
|
};
|
|
1028
1477
|
/**
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
* This constant defines the prefix applied to HTML elements when the dark theme is active.
|
|
1032
|
-
* It is typically added to the root element or specific components to enable dark theme styles.
|
|
1033
|
-
*
|
|
1034
|
-
* @example
|
|
1035
|
-
* ```typescript
|
|
1036
|
-
* document.body.classList.add(darkThemePrefix); // Applies 're-dark' class
|
|
1037
|
-
* ```
|
|
1478
|
+
* @deprecated Prefer `theme.dom.darkClassName` via `providePresentia(...)`.
|
|
1038
1479
|
*/
|
|
1039
1480
|
const darkThemePrefix = 're-dark';
|
|
1040
1481
|
|
|
1041
1482
|
/**
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
* Defines the initial theme settings for the application.
|
|
1045
|
-
* By default, sets the light theme as the active theme.
|
|
1046
|
-
*
|
|
1047
|
-
* This configuration can be overridden when providing `THEME_CONFIG` token
|
|
1048
|
-
* at the module or application level.
|
|
1049
|
-
* ```
|
|
1483
|
+
* @deprecated Prefer configuring themes through `providePresentia({ theme: ... })`.
|
|
1050
1484
|
*/
|
|
1051
1485
|
const defaultThemeConfig = {
|
|
1052
1486
|
defaultTheme: themes.light,
|
|
@@ -1070,6 +1504,28 @@ const defaultThemeConfig = {
|
|
|
1070
1504
|
*/
|
|
1071
1505
|
const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
1072
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
|
+
|
|
1073
1529
|
/**
|
|
1074
1530
|
* Service for managing application theme.
|
|
1075
1531
|
*
|
|
@@ -1085,11 +1541,18 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
|
1085
1541
|
*/
|
|
1086
1542
|
class ThemeService {
|
|
1087
1543
|
config = inject(THEME_CONFIG);
|
|
1088
|
-
|
|
1089
|
-
|
|
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);
|
|
1090
1553
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1091
1554
|
document = inject(DOCUMENT);
|
|
1092
|
-
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
|
|
1555
|
+
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
|
|
1093
1556
|
/**
|
|
1094
1557
|
* Current active theme (`light` or `dark`).
|
|
1095
1558
|
*
|
|
@@ -1099,26 +1562,22 @@ class ThemeService {
|
|
|
1099
1562
|
* <div [class]="themeService.theme()"></div>
|
|
1100
1563
|
* ```
|
|
1101
1564
|
*/
|
|
1102
|
-
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
1565
|
+
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
1103
1566
|
/**
|
|
1104
1567
|
* Convenient flag returning `true` if the light theme is active.
|
|
1105
1568
|
* Suitable for conditional style application or resource selection.
|
|
1106
1569
|
*/
|
|
1107
|
-
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 */ []));
|
|
1108
1572
|
constructor() {
|
|
1109
1573
|
effect(() => {
|
|
1110
1574
|
if (!this.isBrowser) {
|
|
1111
1575
|
this.#theme.set(this.themeDefault);
|
|
1112
1576
|
return;
|
|
1113
1577
|
}
|
|
1114
|
-
const theme =
|
|
1578
|
+
const theme = this.resolveTheme(this.getStoredTheme());
|
|
1115
1579
|
this.switch(theme);
|
|
1116
1580
|
});
|
|
1117
|
-
effect(() => {
|
|
1118
|
-
if (this.isBrowser) {
|
|
1119
|
-
localStorage.setItem('theme', this.#theme());
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
1581
|
}
|
|
1123
1582
|
/**
|
|
1124
1583
|
* Switches theme.
|
|
@@ -1129,41 +1588,212 @@ class ThemeService {
|
|
|
1129
1588
|
* @param theme — explicit theme value (`'light'` or `'dark'`).
|
|
1130
1589
|
*/
|
|
1131
1590
|
switch(theme) {
|
|
1132
|
-
const
|
|
1591
|
+
const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
|
|
1592
|
+
const newTheme = requestedTheme ?? this.nextTheme();
|
|
1133
1593
|
if (this.isBrowser) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
newTheme === themes.light && html.classList.remove(this.darkPrefix);
|
|
1594
|
+
this.applyThemeToDom(newTheme);
|
|
1595
|
+
this.persistTheme(newTheme);
|
|
1137
1596
|
}
|
|
1138
1597
|
this.#theme.set(newTheme);
|
|
1139
1598
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1599
|
+
is(theme) {
|
|
1600
|
+
const allowed = Array.isArray(theme) ? theme : [theme];
|
|
1601
|
+
return allowed.includes(this.#theme());
|
|
1602
|
+
}
|
|
1603
|
+
resolveTheme(theme) {
|
|
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
|
+
}
|
|
1675
|
+
}
|
|
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' });
|
|
1142
1678
|
}
|
|
1143
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1679
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
|
|
1144
1680
|
type: Injectable,
|
|
1145
1681
|
args: [{
|
|
1146
1682
|
providedIn: 'root',
|
|
1147
1683
|
}]
|
|
1148
1684
|
}], ctorParameters: () => [] });
|
|
1149
1685
|
|
|
1686
|
+
let warnedLegacyProvider = false;
|
|
1687
|
+
/**
|
|
1688
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1689
|
+
*/
|
|
1150
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) {
|
|
1151
1702
|
return makeEnvironmentProviders([
|
|
1152
1703
|
{ provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
|
|
1153
1704
|
{ provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
|
|
1154
|
-
{ provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => ls.setLang },
|
|
1705
|
+
{ provide: CHANGE_LANG, deps: [LangService], useFactory: (ls) => (lang) => ls.setLang(lang) },
|
|
1155
1706
|
{ provide: SELECTED_THEME, deps: [ThemeService], useFactory: (ls) => ls.theme },
|
|
1156
|
-
{
|
|
1707
|
+
{
|
|
1708
|
+
provide: CHANGE_THEME,
|
|
1709
|
+
deps: [ThemeService],
|
|
1710
|
+
useFactory: (ls) => (theme) => ls.switch(theme),
|
|
1711
|
+
},
|
|
1157
1712
|
{ provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
|
|
1158
1713
|
{ provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
|
|
1159
1714
|
{ provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
|
|
1715
|
+
{ provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
|
|
1160
1716
|
{ provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
|
|
1161
1717
|
{ provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
|
|
1162
1718
|
{ provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
|
|
1163
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,
|
|
1164
1790
|
]);
|
|
1165
1791
|
}
|
|
1166
1792
|
|
|
1793
|
+
/**
|
|
1794
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1795
|
+
*/
|
|
1796
|
+
|
|
1167
1797
|
/**
|
|
1168
1798
|
* Service for managing page SEO metadata.
|
|
1169
1799
|
*
|
|
@@ -1291,11 +1921,12 @@ class SeoService {
|
|
|
1291
1921
|
link.href = href;
|
|
1292
1922
|
}
|
|
1293
1923
|
}
|
|
1294
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1295
|
-
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' });
|
|
1296
1926
|
}
|
|
1297
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1298
|
-
type: Injectable
|
|
1927
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, decorators: [{
|
|
1928
|
+
type: Injectable,
|
|
1929
|
+
args: [{ providedIn: 'root' }]
|
|
1299
1930
|
}] });
|
|
1300
1931
|
|
|
1301
1932
|
/**
|
|
@@ -1319,8 +1950,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
1319
1950
|
class SeoRouteListener {
|
|
1320
1951
|
router = inject(Router);
|
|
1321
1952
|
seo = inject(SeoService);
|
|
1322
|
-
ar = inject(ActivatedRoute);
|
|
1323
1953
|
destroyRef = inject(DestroyRef);
|
|
1954
|
+
initialized = false;
|
|
1955
|
+
baseUrl = '';
|
|
1324
1956
|
/**
|
|
1325
1957
|
* Initializes the route listener to monitor navigation events and update SEO metadata.
|
|
1326
1958
|
* Subscribes to router NavigationEnd events and automatically unsubscribes on component destruction.
|
|
@@ -1329,10 +1961,11 @@ class SeoRouteListener {
|
|
|
1329
1961
|
* Trailing slashes will be removed automatically.
|
|
1330
1962
|
*/
|
|
1331
1963
|
init(baseUrl) {
|
|
1964
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
1332
1965
|
const applyRouteSeo = () => {
|
|
1333
|
-
const
|
|
1334
|
-
const data =
|
|
1335
|
-
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));
|
|
1336
1969
|
data.title && this.seo.setTitle(data.title);
|
|
1337
1970
|
data.description && this.seo.setDescription(data.description);
|
|
1338
1971
|
data.twitter && this.seo.setTwitter(data.twitter);
|
|
@@ -1347,124 +1980,44 @@ class SeoRouteListener {
|
|
|
1347
1980
|
});
|
|
1348
1981
|
};
|
|
1349
1982
|
applyRouteSeo();
|
|
1350
|
-
this.
|
|
1351
|
-
|
|
1352
|
-
.subscribe(() => applyRouteSeo());
|
|
1353
|
-
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Recursively finds the deepest (most nested) activated route in the route tree.
|
|
1356
|
-
* This is used to extract route data from the currently active leaf route.
|
|
1357
|
-
*
|
|
1358
|
-
* @param r - The root activated route to start traversing from.
|
|
1359
|
-
* @returns The deepest child route in the hierarchy.
|
|
1360
|
-
*/
|
|
1361
|
-
deepest(r) {
|
|
1362
|
-
let cur = r;
|
|
1363
|
-
while (cur.firstChild) {
|
|
1364
|
-
cur = cur.firstChild;
|
|
1983
|
+
if (this.initialized) {
|
|
1984
|
+
return;
|
|
1365
1985
|
}
|
|
1366
|
-
|
|
1986
|
+
this.initialized = true;
|
|
1987
|
+
const subscription = this.router.events.subscribe((event) => {
|
|
1988
|
+
if (event instanceof NavigationEnd) {
|
|
1989
|
+
applyRouteSeo();
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1367
1993
|
}
|
|
1368
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1369
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
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' });
|
|
1370
1996
|
}
|
|
1371
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1997
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, decorators: [{
|
|
1372
1998
|
type: Injectable,
|
|
1373
1999
|
args: [{ providedIn: 'root' }]
|
|
1374
2000
|
}] });
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
* Updates on every `NavigationEnd` event.
|
|
1379
|
-
*
|
|
1380
|
-
* Provides:
|
|
1381
|
-
* - `params` / `query` — strings (as in Angular Router)
|
|
1382
|
-
* - `data` — arbitrary data (from route configuration/resolvers)
|
|
1383
|
-
* - `url` — string assembled from `UrlSegment[]`
|
|
1384
|
-
* - `fragment` — hash (#section)
|
|
1385
|
-
* - `selectData(key)` — type-safe selector for `data`
|
|
1386
|
-
* - `state` — combined computed object (convenient for single subscriber)
|
|
1387
|
-
*/
|
|
1388
|
-
class RouteWatcher {
|
|
1389
|
-
router = inject(Router);
|
|
1390
|
-
destroyRef = inject(DestroyRef);
|
|
1391
|
-
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : []));
|
|
1392
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
1393
|
-
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : []));
|
|
1394
|
-
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : []));
|
|
1395
|
-
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : []));
|
|
1396
|
-
/** Signal for tracking and retrieving URL parameters */
|
|
1397
|
-
params = this.#params.asReadonly();
|
|
1398
|
-
/** Signal for tracking and retrieving query parameters */
|
|
1399
|
-
query = this.#query.asReadonly();
|
|
1400
|
-
/** Signal for tracking and retrieving route data */
|
|
1401
|
-
data = this.#data.asReadonly();
|
|
1402
|
-
/** Signal for tracking and retrieving URL */
|
|
1403
|
-
url = this.#url.asReadonly();
|
|
1404
|
-
/** Signal for tracking and retrieving URL fragment */
|
|
1405
|
-
fragment = this.#fragment.asReadonly();
|
|
1406
|
-
/** Combined computed snapshot (to avoid multiple effects) */
|
|
1407
|
-
state = computed(() => ({
|
|
1408
|
-
params: this.#params(),
|
|
1409
|
-
query: this.#query(),
|
|
1410
|
-
data: this.#data(),
|
|
1411
|
-
url: this.#url(),
|
|
1412
|
-
fragment: this.#fragment(),
|
|
1413
|
-
}), ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
1414
|
-
constructor() {
|
|
1415
|
-
const read = () => {
|
|
1416
|
-
const snap = this.deepestSnapshot();
|
|
1417
|
-
const url = snapshotFullPath(snap);
|
|
1418
|
-
const params = snapshotMergedParams(snap);
|
|
1419
|
-
!deepEqual(params, this.#params()) && this.#params.set(params);
|
|
1420
|
-
!deepEqual(snap.queryParams, this.#query()) && this.#query.set(snap.queryParams);
|
|
1421
|
-
!deepEqual(snap.data, this.#data()) && this.#data.set(snap.data);
|
|
1422
|
-
this.#url() !== url && this.#url.set(url);
|
|
1423
|
-
this.#fragment() !== snap.fragment && this.#fragment.set(snap.fragment ?? null);
|
|
1424
|
-
};
|
|
1425
|
-
read();
|
|
1426
|
-
this.router.events
|
|
1427
|
-
.pipe(filter$1((e) => e instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef))
|
|
1428
|
-
.subscribe(() => read());
|
|
2001
|
+
function resolveCanonicalUrl(canonical, baseUrl, path) {
|
|
2002
|
+
if (!canonical) {
|
|
2003
|
+
return joinBaseUrl(baseUrl, path);
|
|
1429
2004
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
return computed(() => this.#data()[key]);
|
|
2005
|
+
if (/^https?:\/\//i.test(canonical)) {
|
|
2006
|
+
return canonical;
|
|
1433
2007
|
}
|
|
1434
|
-
|
|
1435
|
-
// work with snapshot — we need a "frozen" point at NavigationEnd moment
|
|
1436
|
-
let snap = this.router.routerState.snapshot.root;
|
|
1437
|
-
while (snap.firstChild) {
|
|
1438
|
-
snap = snap.firstChild;
|
|
1439
|
-
}
|
|
1440
|
-
return snap;
|
|
1441
|
-
}
|
|
1442
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1443
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
|
|
1444
|
-
}
|
|
1445
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: RouteWatcher, decorators: [{
|
|
1446
|
-
type: Injectable,
|
|
1447
|
-
args: [{ providedIn: 'root' }]
|
|
1448
|
-
}], ctorParameters: () => [] });
|
|
1449
|
-
/** Joins `UrlSegment[]` into a path string */
|
|
1450
|
-
function joinUrl(segments) {
|
|
1451
|
-
return segments.length ? segments.map((s) => s.path).join('/') : '';
|
|
2008
|
+
return joinBaseUrl(baseUrl, canonical);
|
|
1452
2009
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1460
|
-
/** Merges params from root to the deepest snapshot (child keys override parent). */
|
|
1461
|
-
function snapshotMergedParams(snap) {
|
|
1462
|
-
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;
|
|
1463
2016
|
}
|
|
1464
2017
|
|
|
1465
2018
|
/**
|
|
1466
2019
|
* Generated bundle index. Do not edit.
|
|
1467
2020
|
*/
|
|
1468
2021
|
|
|
1469
|
-
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 };
|
|
1470
2023
|
//# sourceMappingURL=reforgium-presentia.mjs.map
|