@reforgium/presentia 1.5.0 → 2.1.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 +163 -0
- package/LICENSE +21 -0
- package/README.md +346 -35
- package/bin/presentia-gen-namespaces.mjs +1248 -0
- package/fesm2022/reforgium-presentia.mjs +738 -188
- package/package.json +24 -6
- package/types/reforgium-presentia.d.ts +292 -88
|
@@ -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,
|
|
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, makeEnvironmentProviders, provideAppInitializer, input, ElementRef, Renderer2, Injector, afterNextRender, runInInjectionContext, Pipe, LOCALE_ID } from '@angular/core';
|
|
3
|
+
import { LruCache, MemoryStorage, deepEqual, compareRoutes, getChainedValue, 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,77 @@ const innerLangVal = Symbol('reInnerLangVal');
|
|
|
234
280
|
|
|
235
281
|
const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER');
|
|
236
282
|
|
|
283
|
+
function createPresentiaStorage(strategy, storage) {
|
|
284
|
+
if (storage) {
|
|
285
|
+
return storage;
|
|
286
|
+
}
|
|
287
|
+
switch (strategy ?? 'persist') {
|
|
288
|
+
case 'none':
|
|
289
|
+
return createNoopStorage();
|
|
290
|
+
case 'memory':
|
|
291
|
+
return new MemoryStorage();
|
|
292
|
+
case 'session':
|
|
293
|
+
return createBrowserStringStorage('session');
|
|
294
|
+
case 'lru':
|
|
295
|
+
return new LruCache(1);
|
|
296
|
+
case 'persist':
|
|
297
|
+
default:
|
|
298
|
+
return createBrowserStringStorage('local');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function createBrowserStringStorage(kind) {
|
|
302
|
+
const getStorage = () => {
|
|
303
|
+
if (typeof globalThis === 'undefined') {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return kind === 'local' ? (globalThis.localStorage ?? null) : (globalThis.sessionStorage ?? null);
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
get length() {
|
|
310
|
+
return getStorage()?.length ?? 0;
|
|
311
|
+
},
|
|
312
|
+
get(key) {
|
|
313
|
+
return getStorage()?.getItem(key) ?? null;
|
|
314
|
+
},
|
|
315
|
+
set(key, value) {
|
|
316
|
+
getStorage()?.setItem(key, value);
|
|
317
|
+
},
|
|
318
|
+
remove(key) {
|
|
319
|
+
getStorage()?.removeItem(key);
|
|
320
|
+
},
|
|
321
|
+
clear() {
|
|
322
|
+
getStorage()?.clear();
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createNoopStorage() {
|
|
327
|
+
return {
|
|
328
|
+
get length() {
|
|
329
|
+
return 0;
|
|
330
|
+
},
|
|
331
|
+
get: () => null,
|
|
332
|
+
set: () => undefined,
|
|
333
|
+
remove: () => undefined,
|
|
334
|
+
clear: () => undefined,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Optional DI token for a custom persistence adapter used by `LangService`
|
|
340
|
+
* to store and retrieve the selected language (default key: `'lang'`).
|
|
341
|
+
*
|
|
342
|
+
* When not provided the service falls back to `localStorage` directly.
|
|
343
|
+
*
|
|
344
|
+
* Example:
|
|
345
|
+
* ```ts
|
|
346
|
+
* { provide: LANG_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
347
|
+
* ```
|
|
348
|
+
*/
|
|
349
|
+
const defaultLangPersistenceAdapter = createPresentiaStorage('persist');
|
|
350
|
+
const LANG_PERSISTENCE_ADAPTER = new InjectionToken('RE_LANG_PERSISTENCE_ADAPTER', {
|
|
351
|
+
factory: () => defaultLangPersistenceAdapter,
|
|
352
|
+
});
|
|
353
|
+
|
|
237
354
|
/**
|
|
238
355
|
* Injection token for providing locale configuration to the language module.
|
|
239
356
|
*
|
|
@@ -254,6 +371,341 @@ const LANG_MISSING_KEY_HANDLER = new InjectionToken('RE_LANG_MISSING_KEY_HANDLER
|
|
|
254
371
|
*/
|
|
255
372
|
const LANG_CONFIG = new InjectionToken('RE_LANG_CONFIG');
|
|
256
373
|
|
|
374
|
+
function deepestActivatedRoute(route) {
|
|
375
|
+
let current = route;
|
|
376
|
+
while (current.firstChild) {
|
|
377
|
+
current = current.firstChild;
|
|
378
|
+
}
|
|
379
|
+
return current;
|
|
380
|
+
}
|
|
381
|
+
function deepestRouteSnapshot(snapshot) {
|
|
382
|
+
let current = snapshot;
|
|
383
|
+
while (current.firstChild) {
|
|
384
|
+
current = current.firstChild;
|
|
385
|
+
}
|
|
386
|
+
return current;
|
|
387
|
+
}
|
|
388
|
+
function joinUrl(segments) {
|
|
389
|
+
return segments.length ? segments.map((segment) => segment.path).join('/') : '';
|
|
390
|
+
}
|
|
391
|
+
function snapshotFullPath(snapshot) {
|
|
392
|
+
return snapshot.pathFromRoot
|
|
393
|
+
.map((route) => joinUrl(route.url))
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join('/');
|
|
396
|
+
}
|
|
397
|
+
function snapshotRoutePattern(snapshot) {
|
|
398
|
+
return snapshot.pathFromRoot
|
|
399
|
+
.map((route) => route.routeConfig?.path ?? '')
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.join('/');
|
|
402
|
+
}
|
|
403
|
+
function snapshotMergedParams(snapshot) {
|
|
404
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.params }), {});
|
|
405
|
+
}
|
|
406
|
+
function snapshotDeepestParams(snapshot) {
|
|
407
|
+
const mergedParams = snapshotMergedParams(snapshot);
|
|
408
|
+
const keys = extractRouteParamKeys(snapshot.routeConfig?.path ?? '');
|
|
409
|
+
if (!keys.length) {
|
|
410
|
+
return {};
|
|
411
|
+
}
|
|
412
|
+
return keys.reduce((acc, key) => {
|
|
413
|
+
const value = mergedParams[key];
|
|
414
|
+
if (value !== undefined) {
|
|
415
|
+
acc[key] = value;
|
|
416
|
+
}
|
|
417
|
+
return acc;
|
|
418
|
+
}, {});
|
|
419
|
+
}
|
|
420
|
+
function snapshotMergedData(snapshot) {
|
|
421
|
+
return snapshot.pathFromRoot.reduce((acc, route) => ({ ...acc, ...route.data }), {});
|
|
422
|
+
}
|
|
423
|
+
function extractRouteParamKeys(path) {
|
|
424
|
+
return path
|
|
425
|
+
.split('/')
|
|
426
|
+
.filter((segment) => segment.startsWith(':'))
|
|
427
|
+
.map((segment) => segment.slice(1))
|
|
428
|
+
.filter(Boolean);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Reactive snapshot of the current route (the deepest active route).
|
|
433
|
+
* Updates on every `NavigationEnd` event.
|
|
434
|
+
*/
|
|
435
|
+
class RouteWatcher {
|
|
436
|
+
router = inject(Router);
|
|
437
|
+
destroyRef = inject(DestroyRef);
|
|
438
|
+
#params = signal({}, ...(ngDevMode ? [{ debugName: "#params" }] : /* istanbul ignore next */ []));
|
|
439
|
+
#deepestParams = signal({}, ...(ngDevMode ? [{ debugName: "#deepestParams" }] : /* istanbul ignore next */ []));
|
|
440
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
441
|
+
#data = signal({}, ...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
|
|
442
|
+
#mergedData = signal({}, ...(ngDevMode ? [{ debugName: "#mergedData" }] : /* istanbul ignore next */ []));
|
|
443
|
+
#url = signal('', ...(ngDevMode ? [{ debugName: "#url" }] : /* istanbul ignore next */ []));
|
|
444
|
+
#routePattern = signal('', ...(ngDevMode ? [{ debugName: "#routePattern" }] : /* istanbul ignore next */ []));
|
|
445
|
+
#fragment = signal(null, ...(ngDevMode ? [{ debugName: "#fragment" }] : /* istanbul ignore next */ []));
|
|
446
|
+
/** Params merged from root to deepest route. */
|
|
447
|
+
params = this.#params.asReadonly();
|
|
448
|
+
/** Params declared on the deepest route only. */
|
|
449
|
+
deepestParams = this.#deepestParams.asReadonly();
|
|
450
|
+
/** Query params from the current navigation. */
|
|
451
|
+
query = this.#query.asReadonly();
|
|
452
|
+
/** Deepest route data only. */
|
|
453
|
+
data = this.#data.asReadonly();
|
|
454
|
+
/** Route data merged from root to deepest route. */
|
|
455
|
+
mergedData = this.#mergedData.asReadonly();
|
|
456
|
+
/** Full current url path assembled from root to deepest route. */
|
|
457
|
+
url = this.#url.asReadonly();
|
|
458
|
+
/** Current route config pattern, e.g. `orgs/:orgId/users/:id`. */
|
|
459
|
+
routePattern = this.#routePattern.asReadonly();
|
|
460
|
+
/** Current url fragment without `#`. */
|
|
461
|
+
fragment = this.#fragment.asReadonly();
|
|
462
|
+
/** Combined computed snapshot. */
|
|
463
|
+
state = computed(() => ({
|
|
464
|
+
params: this.#params(),
|
|
465
|
+
deepestParams: this.#deepestParams(),
|
|
466
|
+
query: this.#query(),
|
|
467
|
+
data: this.#data(),
|
|
468
|
+
mergedData: this.#mergedData(),
|
|
469
|
+
url: this.#url(),
|
|
470
|
+
routePattern: this.#routePattern(),
|
|
471
|
+
fragment: this.#fragment(),
|
|
472
|
+
}), ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
473
|
+
constructor() {
|
|
474
|
+
const read = () => {
|
|
475
|
+
const snapshot = this.deepestSnapshot();
|
|
476
|
+
const nextUrl = snapshotFullPath(snapshot);
|
|
477
|
+
const nextRoutePattern = snapshotRoutePattern(snapshot);
|
|
478
|
+
const nextParams = snapshotMergedParams(snapshot);
|
|
479
|
+
const nextDeepestParams = snapshotDeepestParams(snapshot);
|
|
480
|
+
const nextMergedData = snapshotMergedData(snapshot);
|
|
481
|
+
const nextQuery = snapshot.queryParams;
|
|
482
|
+
const nextData = snapshot.data;
|
|
483
|
+
const nextFragment = snapshot.fragment ?? null;
|
|
484
|
+
!deepEqual(nextParams, this.#params()) && this.#params.set(nextParams);
|
|
485
|
+
!deepEqual(nextDeepestParams, this.#deepestParams()) && this.#deepestParams.set(nextDeepestParams);
|
|
486
|
+
!deepEqual(nextQuery, this.#query()) && this.#query.set(nextQuery);
|
|
487
|
+
!deepEqual(nextData, this.#data()) && this.#data.set(nextData);
|
|
488
|
+
!deepEqual(nextMergedData, this.#mergedData()) && this.#mergedData.set(nextMergedData);
|
|
489
|
+
this.#url() !== nextUrl && this.#url.set(nextUrl);
|
|
490
|
+
this.#routePattern() !== nextRoutePattern && this.#routePattern.set(nextRoutePattern);
|
|
491
|
+
this.#fragment() !== nextFragment && this.#fragment.set(nextFragment);
|
|
492
|
+
};
|
|
493
|
+
read();
|
|
494
|
+
const subscription = this.router.events.subscribe((event) => {
|
|
495
|
+
if (event instanceof NavigationEnd) {
|
|
496
|
+
read();
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
500
|
+
}
|
|
501
|
+
selectData(key, strategy = 'deepest') {
|
|
502
|
+
return computed(() => {
|
|
503
|
+
const source = strategy === 'merged' ? this.#mergedData() : this.#data();
|
|
504
|
+
return source[key];
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
selectParam(key, strategy = 'merged') {
|
|
508
|
+
return computed(() => {
|
|
509
|
+
const source = strategy === 'deepest' ? this.#deepestParams() : this.#params();
|
|
510
|
+
return source[key];
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
matchesPath(path) {
|
|
514
|
+
const current = this.#url();
|
|
515
|
+
return typeof path === 'string' ? current === path : path.test(current);
|
|
516
|
+
}
|
|
517
|
+
deepestSnapshot() {
|
|
518
|
+
return deepestRouteSnapshot(this.router.routerState.snapshot.root);
|
|
519
|
+
}
|
|
520
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
521
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, providedIn: 'root' });
|
|
522
|
+
}
|
|
523
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteWatcher, decorators: [{
|
|
524
|
+
type: Injectable,
|
|
525
|
+
args: [{ providedIn: 'root' }]
|
|
526
|
+
}], ctorParameters: () => [] });
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* @deprecated Diagnostics are usually enabled through route preload config, not by consuming this service directly.
|
|
530
|
+
*/
|
|
531
|
+
class RouteNamespaceDiagnosticsService {
|
|
532
|
+
router = inject(Router, { optional: true });
|
|
533
|
+
enabled = !!inject(LANG_CONFIG).routeNamespacePreload?.diagnostics;
|
|
534
|
+
warned = new Set();
|
|
535
|
+
currentUrl = '';
|
|
536
|
+
currentNamespaces = new Set();
|
|
537
|
+
navigationSettled = false;
|
|
538
|
+
constructor() {
|
|
539
|
+
if (!this.enabled || !this.router) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.router.events.subscribe((event) => {
|
|
543
|
+
if (event instanceof NavigationEnd) {
|
|
544
|
+
const url = normalizeUrlPath$1(event.urlAfterRedirects || event.url);
|
|
545
|
+
if (url === this.currentUrl) {
|
|
546
|
+
this.navigationSettled = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
registerRouteNamespaces(url, namespaces) {
|
|
552
|
+
if (!this.enabled) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.currentUrl = normalizeUrlPath$1(url);
|
|
556
|
+
this.currentNamespaces = new Set(namespaces.map((ns) => ns.trim()).filter(Boolean));
|
|
557
|
+
this.navigationSettled = false;
|
|
558
|
+
}
|
|
559
|
+
warnLateNamespaceLoad(namespace) {
|
|
560
|
+
if (!this.enabled || !this.navigationSettled || typeof ngDevMode === 'undefined') {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const normalizedNamespace = namespace.trim();
|
|
564
|
+
if (!normalizedNamespace || this.currentNamespaces.has(normalizedNamespace)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const key = `${this.currentUrl}|${normalizedNamespace}`;
|
|
568
|
+
if (this.warned.has(key)) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.warned.add(key);
|
|
572
|
+
// eslint-disable-next-line no-console
|
|
573
|
+
console.warn(`[presentia] Namespace "${normalizedNamespace}" was loaded after route activation for "${this.currentUrl}". ` +
|
|
574
|
+
'Add it to routeNamespacePreload manifest or route data to avoid raw i18n keys on first paint.');
|
|
575
|
+
}
|
|
576
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
577
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, providedIn: 'root' });
|
|
578
|
+
}
|
|
579
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RouteNamespaceDiagnosticsService, decorators: [{
|
|
580
|
+
type: Injectable,
|
|
581
|
+
args: [{ providedIn: 'root' }]
|
|
582
|
+
}], ctorParameters: () => [] });
|
|
583
|
+
function normalizeUrlPath$1(url) {
|
|
584
|
+
const [path] = url.split(/[?#]/, 1);
|
|
585
|
+
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
586
|
+
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY = '__rePresentiaRouteNamespacePreload';
|
|
590
|
+
const PRESENTIA_ROUTE_NAMESPACES_DATA_KEY = 'presentiaNamespaces';
|
|
591
|
+
function providePresentiaRouteNamespacePreload() {
|
|
592
|
+
return makeEnvironmentProviders([
|
|
593
|
+
provideAppInitializer(() => {
|
|
594
|
+
const router = inject(Router, { optional: true });
|
|
595
|
+
const langConfig = inject(LANG_CONFIG);
|
|
596
|
+
const normalized = normalizeRouteNamespacePreloadConfig(langConfig.routeNamespacePreload);
|
|
597
|
+
if (!router || !normalized) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (typeof ngDevMode !== 'undefined' && normalized.manifest && Object.keys(normalized.manifest).length === 0) {
|
|
601
|
+
// eslint-disable-next-line no-console
|
|
602
|
+
console.warn('[presentia] routeNamespacePreload.manifest is empty. Route-data preload still works, but manifest mode is effectively disabled.');
|
|
603
|
+
}
|
|
604
|
+
router.resetConfig(patchRoutesWithNamespacePreload(router.config, normalized));
|
|
605
|
+
}),
|
|
606
|
+
]);
|
|
607
|
+
}
|
|
608
|
+
function normalizeRouteNamespacePreloadConfig(config) {
|
|
609
|
+
if (!config) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
mode: config.mode ?? 'blocking',
|
|
614
|
+
dataKey: config.dataKey?.trim() || PRESENTIA_ROUTE_NAMESPACES_DATA_KEY,
|
|
615
|
+
manifest: config.manifest,
|
|
616
|
+
mergeStrategy: config.mergeStrategy ?? 'append',
|
|
617
|
+
onError: config.onError ?? 'continue',
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function patchRoutesWithNamespacePreload(routes, config) {
|
|
621
|
+
return routes.map((route) => patchRouteWithNamespacePreload(route, config));
|
|
622
|
+
}
|
|
623
|
+
function patchRouteWithNamespacePreload(route, config) {
|
|
624
|
+
const nextChildren = route.children ? patchRoutesWithNamespacePreload(route.children, config) : route.children;
|
|
625
|
+
if (route.redirectTo) {
|
|
626
|
+
return nextChildren === route.children ? route : { ...route, children: nextChildren };
|
|
627
|
+
}
|
|
628
|
+
const nextResolve = {
|
|
629
|
+
...(route.resolve ?? {}),
|
|
630
|
+
[PRESENTIA_ROUTE_NAMESPACE_PRELOAD_RESOLVE_KEY]: makeRouteNamespacePreloadResolver(config),
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
...route,
|
|
634
|
+
...(nextChildren ? { children: nextChildren } : {}),
|
|
635
|
+
resolve: nextResolve,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function makeRouteNamespacePreloadResolver(config) {
|
|
639
|
+
return async (route, state) => {
|
|
640
|
+
const lang = inject(LangService);
|
|
641
|
+
const diagnostics = inject(RouteNamespaceDiagnosticsService);
|
|
642
|
+
const namespaces = resolveRouteNamespaces(route, state, config);
|
|
643
|
+
diagnostics.registerRouteNamespaces(state.url, namespaces);
|
|
644
|
+
if (!namespaces.length) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (config.mode === 'lazy') {
|
|
648
|
+
queueMicrotask(() => {
|
|
649
|
+
void lang.loadNamespaces(namespaces);
|
|
650
|
+
});
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await lang.loadNamespaces(namespaces);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (config.onError === 'throw') {
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
// Keep navigation alive; runtime lazy loading remains a fallback.
|
|
661
|
+
}
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function resolveRouteNamespaces(route, state, config) {
|
|
666
|
+
const dataNamespaces = readNamespacesFromRouteData(route, config.dataKey);
|
|
667
|
+
const manifestNamespaces = readNamespacesFromManifest(route, state, config.manifest);
|
|
668
|
+
if (config.mergeStrategy === 'replace' && dataNamespaces.length) {
|
|
669
|
+
return dataNamespaces;
|
|
670
|
+
}
|
|
671
|
+
return uniqueNamespaces([...manifestNamespaces, ...dataNamespaces]);
|
|
672
|
+
}
|
|
673
|
+
function readNamespacesFromRouteData(route, dataKey) {
|
|
674
|
+
return uniqueNamespaces(route.pathFromRoot.flatMap((snapshot) => {
|
|
675
|
+
const value = snapshot.data?.[dataKey];
|
|
676
|
+
return Array.isArray(value) ? value : [];
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
function readNamespacesFromManifest(route, state, manifest) {
|
|
680
|
+
if (!manifest) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
const actualUrl = normalizeUrlPath(state.url);
|
|
684
|
+
const routePath = snapshotRouteConfigPath(route);
|
|
685
|
+
return uniqueNamespaces(Object.entries(manifest)
|
|
686
|
+
.filter(([key]) => matchesManifestKey(actualUrl, routePath, key))
|
|
687
|
+
.flatMap(([, namespaces]) => namespaces));
|
|
688
|
+
}
|
|
689
|
+
function matchesManifestKey(actualUrl, routePath, manifestKey) {
|
|
690
|
+
const normalizedKey = normalizeUrlPath(manifestKey);
|
|
691
|
+
return compareRoutes(actualUrl, normalizedKey) || (!!routePath && routePath === normalizedKey);
|
|
692
|
+
}
|
|
693
|
+
function snapshotRouteConfigPath(route) {
|
|
694
|
+
const templatePath = route.pathFromRoot
|
|
695
|
+
.map((item) => item.routeConfig?.path ?? '')
|
|
696
|
+
.filter(Boolean)
|
|
697
|
+
.join('/');
|
|
698
|
+
return normalizeUrlPath(templatePath);
|
|
699
|
+
}
|
|
700
|
+
function normalizeUrlPath(url) {
|
|
701
|
+
const [path] = url.split(/[?#]/, 1);
|
|
702
|
+
const normalized = `/${(path ?? '').replace(/^\/+|\/+$/g, '')}`;
|
|
703
|
+
return normalized === '/' ? normalized : normalized.replace(/\/{2,}/g, '/');
|
|
704
|
+
}
|
|
705
|
+
function uniqueNamespaces(namespaces) {
|
|
706
|
+
return Array.from(new Set(namespaces.map((ns) => ns.trim()).filter(Boolean)));
|
|
707
|
+
}
|
|
708
|
+
|
|
257
709
|
/**
|
|
258
710
|
* LangService provides functionality for managing and tracking language settings
|
|
259
711
|
* and translations in the application. It is designed to handle localization needs,
|
|
@@ -266,13 +718,15 @@ class LangService {
|
|
|
266
718
|
config = inject(LANG_CONFIG);
|
|
267
719
|
http = inject(HttpClient);
|
|
268
720
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
721
|
+
persistence = inject(LANG_PERSISTENCE_ADAPTER);
|
|
269
722
|
missingKeyHandler = inject(LANG_MISSING_KEY_HANDLER, { optional: true });
|
|
723
|
+
routeNamespaceDiagnostics = inject(RouteNamespaceDiagnosticsService, { optional: true });
|
|
270
724
|
supportedLangSet = new Set([
|
|
271
725
|
...LangService.BUILTIN_LANGS,
|
|
272
726
|
...this.normalizeSupportedLangs(this.config.supportedLangs ?? []),
|
|
273
727
|
]);
|
|
274
|
-
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : []));
|
|
275
|
-
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : []));
|
|
728
|
+
#lang = signal(this.getStoredLang(), ...(ngDevMode ? [{ debugName: "#lang" }] : /* istanbul ignore next */ []));
|
|
729
|
+
#cache = signal({}, ...(ngDevMode ? [{ debugName: "#cache" }] : /* istanbul ignore next */ []));
|
|
276
730
|
#loadedNamespaces = new Set();
|
|
277
731
|
#pendingLoads = new Map();
|
|
278
732
|
#pendingBatchLoads = new Map();
|
|
@@ -289,7 +743,7 @@ class LangService {
|
|
|
289
743
|
currentLang = computed(() => {
|
|
290
744
|
const lang = this.#lang();
|
|
291
745
|
return lang === 'kg' ? (this.config?.kgValue ?? 'kg') : lang;
|
|
292
|
-
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : []));
|
|
746
|
+
}, ...(ngDevMode ? [{ debugName: "currentLang" }] : /* istanbul ignore next */ []));
|
|
293
747
|
/**
|
|
294
748
|
* Extracts readonly value from private property `#lang` and assigns it to `innerLangVal`.
|
|
295
749
|
* Expected that property `#lang` has `asReadonly` method that returns immutable representation.
|
|
@@ -318,7 +772,7 @@ class LangService {
|
|
|
318
772
|
if (langVal !== this.#lang()) {
|
|
319
773
|
this.#lang.set(langVal);
|
|
320
774
|
if (this.isBrowser) {
|
|
321
|
-
|
|
775
|
+
this.persistence.set('lang', langVal);
|
|
322
776
|
}
|
|
323
777
|
const namespaces = Array.from(this.#loadedNamespaces.values()).map((key) => this.namespaceFromKey(key));
|
|
324
778
|
this.#loadedNamespaces.clear();
|
|
@@ -357,6 +811,7 @@ class LangService {
|
|
|
357
811
|
if (this.isNamespaceValid(key)) {
|
|
358
812
|
return;
|
|
359
813
|
}
|
|
814
|
+
this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
|
|
360
815
|
this.tryExpireNamespace(key);
|
|
361
816
|
if (this.#pendingLoads.has(key)) {
|
|
362
817
|
return this.#pendingLoads.get(key);
|
|
@@ -411,14 +866,27 @@ class LangService {
|
|
|
411
866
|
await Promise.all(toLoad.map((ns) => this.loadNamespace(ns)));
|
|
412
867
|
return;
|
|
413
868
|
}
|
|
414
|
-
const
|
|
869
|
+
const maxBatchSize = this.normalizeMaxBatchSize(this.config.maxBatchSize);
|
|
870
|
+
const chunks = maxBatchSize && toLoad.length > maxBatchSize ? this.chunkNamespaces(toLoad, maxBatchSize) : [toLoad];
|
|
871
|
+
await Promise.all(chunks.map((chunk) => {
|
|
872
|
+
if (chunk.length < 2) {
|
|
873
|
+
return this.loadNamespace(chunk[0]);
|
|
874
|
+
}
|
|
875
|
+
return this.loadNamespaceBatch(chunk, requestedLang);
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
async loadNamespaceBatch(namespaces, requestedLang) {
|
|
879
|
+
for (const ns of namespaces) {
|
|
880
|
+
this.routeNamespaceDiagnostics?.warnLateNamespaceLoad(ns);
|
|
881
|
+
}
|
|
882
|
+
const batchKey = this.makeBatchKey(requestedLang, namespaces);
|
|
415
883
|
if (this.#pendingBatchLoads.has(batchKey)) {
|
|
416
884
|
return this.#pendingBatchLoads.get(batchKey);
|
|
417
885
|
}
|
|
418
886
|
const promise = (async () => {
|
|
419
887
|
try {
|
|
420
888
|
const context = {
|
|
421
|
-
namespaces
|
|
889
|
+
namespaces,
|
|
422
890
|
lang: requestedLang,
|
|
423
891
|
isFromAssets: this.config.isFromAssets,
|
|
424
892
|
baseUrl: this.config.url,
|
|
@@ -430,7 +898,7 @@ class LangService {
|
|
|
430
898
|
}
|
|
431
899
|
const payloads = this.config.batchResponseAdapter(response, this.toBatchResponseContext(context));
|
|
432
900
|
const merged = {};
|
|
433
|
-
for (const ns of
|
|
901
|
+
for (const ns of namespaces) {
|
|
434
902
|
const nsPayload = payloads[ns];
|
|
435
903
|
if (!nsPayload) {
|
|
436
904
|
continue;
|
|
@@ -450,6 +918,19 @@ class LangService {
|
|
|
450
918
|
this.#pendingBatchLoads.set(batchKey, promise);
|
|
451
919
|
return promise;
|
|
452
920
|
}
|
|
921
|
+
normalizeMaxBatchSize(value) {
|
|
922
|
+
if (!value || value < 1) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return Math.floor(value);
|
|
926
|
+
}
|
|
927
|
+
chunkNamespaces(namespaces, size) {
|
|
928
|
+
const chunks = [];
|
|
929
|
+
for (let i = 0; i < namespaces.length; i += size) {
|
|
930
|
+
chunks.push(namespaces.slice(i, i + size));
|
|
931
|
+
}
|
|
932
|
+
return chunks;
|
|
933
|
+
}
|
|
453
934
|
evictNamespace(ns) {
|
|
454
935
|
const key = this.makeNamespaceKey(ns);
|
|
455
936
|
if (!this.#loadedNamespaces.has(key)) {
|
|
@@ -512,7 +993,7 @@ class LangService {
|
|
|
512
993
|
if (!this.isBrowser) {
|
|
513
994
|
return defaultLang;
|
|
514
995
|
}
|
|
515
|
-
return normalize(
|
|
996
|
+
return normalize(this.persistence.get('lang')) ?? defaultLang;
|
|
516
997
|
}
|
|
517
998
|
makeUrl(ns, lang) {
|
|
518
999
|
if (this.config.requestBuilder) {
|
|
@@ -664,10 +1145,10 @@ class LangService {
|
|
|
664
1145
|
.map((lang) => lang.trim().toLowerCase())
|
|
665
1146
|
.filter((lang) => !!lang && LangService.LANG_CODE_RE.test(lang));
|
|
666
1147
|
}
|
|
667
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
668
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
1148
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1149
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, providedIn: 'root' });
|
|
669
1150
|
}
|
|
670
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1151
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangService, decorators: [{
|
|
671
1152
|
type: Injectable,
|
|
672
1153
|
args: [{ providedIn: 'root' }]
|
|
673
1154
|
}], ctorParameters: () => [] });
|
|
@@ -705,19 +1186,19 @@ class LangDirective {
|
|
|
705
1186
|
* Localization mode: defines which parts of the element will be translated.
|
|
706
1187
|
* @default 'all'
|
|
707
1188
|
*/
|
|
708
|
-
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : {}), alias: 'reLang' });
|
|
1189
|
+
lang = input('all', { ...(ngDevMode ? { debugName: "lang" } : /* istanbul ignore next */ {}), alias: 'reLang' });
|
|
709
1190
|
/**
|
|
710
1191
|
* Explicit key for text content translation.
|
|
711
1192
|
*/
|
|
712
|
-
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : []));
|
|
1193
|
+
reLangKeySig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangKeySig" }] : /* istanbul ignore next */ []));
|
|
713
1194
|
/**
|
|
714
1195
|
* Explicit attribute-to-key map for translation.
|
|
715
1196
|
*/
|
|
716
|
-
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : []));
|
|
1197
|
+
reLangAttrsSig = signal(undefined, ...(ngDevMode ? [{ debugName: "reLangAttrsSig" }] : /* istanbul ignore next */ []));
|
|
717
1198
|
/**
|
|
718
1199
|
* Name of an additional attribute to localize (besides standard `title`, `label`, `placeholder`).
|
|
719
1200
|
*/
|
|
720
|
-
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : []));
|
|
1201
|
+
langForAttr = input(...(ngDevMode ? [undefined, { debugName: "langForAttr" }] : /* istanbul ignore next */ []));
|
|
721
1202
|
el = inject(ElementRef);
|
|
722
1203
|
renderer = inject(Renderer2);
|
|
723
1204
|
service = inject(LangService);
|
|
@@ -913,10 +1394,10 @@ class LangDirective {
|
|
|
913
1394
|
}
|
|
914
1395
|
return this.service.get(key);
|
|
915
1396
|
}
|
|
916
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
917
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.
|
|
1397
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1398
|
+
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
1399
|
}
|
|
919
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangDirective, decorators: [{
|
|
920
1401
|
type: Directive,
|
|
921
1402
|
args: [{ selector: '[reLang]', standalone: true }]
|
|
922
1403
|
}], 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,10 +1505,10 @@ class LangPipe {
|
|
|
1024
1505
|
// eslint-disable-next-line no-console
|
|
1025
1506
|
console.warn(`LangPipe: namespace loaded but key "${query}" is unresolved`);
|
|
1026
1507
|
}
|
|
1027
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1028
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.
|
|
1508
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1509
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, isStandalone: true, name: "lang", pure: false });
|
|
1029
1510
|
}
|
|
1030
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1511
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LangPipe, decorators: [{
|
|
1031
1512
|
type: Pipe,
|
|
1032
1513
|
args: [{ name: 'lang', standalone: true, pure: false }]
|
|
1033
1514
|
}], ctorParameters: () => [] });
|
|
@@ -1053,27 +1534,12 @@ const themes = {
|
|
|
1053
1534
|
dark: 'dark',
|
|
1054
1535
|
};
|
|
1055
1536
|
/**
|
|
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
|
-
* ```
|
|
1537
|
+
* @deprecated Prefer `theme.dom.darkClassName` via `providePresentia(...)`.
|
|
1065
1538
|
*/
|
|
1066
1539
|
const darkThemePrefix = 're-dark';
|
|
1067
1540
|
|
|
1068
1541
|
/**
|
|
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
|
-
* ```
|
|
1542
|
+
* @deprecated Prefer configuring themes through `providePresentia({ theme: ... })`.
|
|
1077
1543
|
*/
|
|
1078
1544
|
const defaultThemeConfig = {
|
|
1079
1545
|
defaultTheme: themes.light,
|
|
@@ -1097,6 +1563,25 @@ const defaultThemeConfig = {
|
|
|
1097
1563
|
*/
|
|
1098
1564
|
const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
1099
1565
|
|
|
1566
|
+
/**
|
|
1567
|
+
* @deprecated Prefer configuring theme persistence through `providePresentia({ theme: { persistence... } })`.
|
|
1568
|
+
*/
|
|
1569
|
+
const defaultThemePersistenceAdapter = createPresentiaStorage('persist');
|
|
1570
|
+
/**
|
|
1571
|
+
* DI token for the persistence adapter used by `ThemeService`
|
|
1572
|
+
* to store and retrieve the selected theme (default key: `'theme'`).
|
|
1573
|
+
*
|
|
1574
|
+
* By default `presentia` provides a `localStorage`-backed adapter.
|
|
1575
|
+
*
|
|
1576
|
+
* Example:
|
|
1577
|
+
* ```ts
|
|
1578
|
+
* { provide: THEME_PERSISTENCE_ADAPTER, useValue: sessionStorageAdapter }
|
|
1579
|
+
* ```
|
|
1580
|
+
*/
|
|
1581
|
+
const THEME_PERSISTENCE_ADAPTER = new InjectionToken('RE_THEME_PERSISTENCE_ADAPTER', {
|
|
1582
|
+
factory: () => defaultThemePersistenceAdapter,
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1100
1585
|
/**
|
|
1101
1586
|
* Service for managing application theme.
|
|
1102
1587
|
*
|
|
@@ -1112,11 +1597,18 @@ const THEME_CONFIG = new InjectionToken('RE_THEME_CONFIG');
|
|
|
1112
1597
|
*/
|
|
1113
1598
|
class ThemeService {
|
|
1114
1599
|
config = inject(THEME_CONFIG);
|
|
1115
|
-
|
|
1116
|
-
|
|
1600
|
+
registry = this.resolveRegistry(this.config?.registry);
|
|
1601
|
+
themeDefault = this.resolveInitialTheme(this.config?.defaultTheme);
|
|
1602
|
+
domStrategy = this.config?.dom?.strategy ?? 'root-class';
|
|
1603
|
+
darkPrefix = this.config?.dom?.darkThemePrefix || this.config?.darkThemePrefix || darkThemePrefix;
|
|
1604
|
+
attributeName = this.config?.dom?.attributeName || 'data-theme';
|
|
1605
|
+
themeClassPrefix = this.config?.dom?.themeClassPrefix;
|
|
1606
|
+
classNameBuilder = this.config?.dom?.classNameBuilder;
|
|
1607
|
+
rootSelector = this.config?.dom?.rootSelector;
|
|
1608
|
+
persistence = inject(THEME_PERSISTENCE_ADAPTER);
|
|
1117
1609
|
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
1118
1610
|
document = inject(DOCUMENT);
|
|
1119
|
-
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : []));
|
|
1611
|
+
#theme = signal(this.themeDefault, ...(ngDevMode ? [{ debugName: "#theme" }] : /* istanbul ignore next */ []));
|
|
1120
1612
|
/**
|
|
1121
1613
|
* Current active theme (`light` or `dark`).
|
|
1122
1614
|
*
|
|
@@ -1126,26 +1618,22 @@ class ThemeService {
|
|
|
1126
1618
|
* <div [class]="themeService.theme()"></div>
|
|
1127
1619
|
* ```
|
|
1128
1620
|
*/
|
|
1129
|
-
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
1621
|
+
theme = computed(() => this.#theme(), ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
1130
1622
|
/**
|
|
1131
1623
|
* Convenient flag returning `true` if the light theme is active.
|
|
1132
1624
|
* Suitable for conditional style application or resource selection.
|
|
1133
1625
|
*/
|
|
1134
|
-
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : []));
|
|
1626
|
+
isLight = computed(() => this.#theme() === themes.light, ...(ngDevMode ? [{ debugName: "isLight" }] : /* istanbul ignore next */ []));
|
|
1627
|
+
isDark = computed(() => this.#theme() === themes.dark, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
1135
1628
|
constructor() {
|
|
1136
1629
|
effect(() => {
|
|
1137
1630
|
if (!this.isBrowser) {
|
|
1138
1631
|
this.#theme.set(this.themeDefault);
|
|
1139
1632
|
return;
|
|
1140
1633
|
}
|
|
1141
|
-
const theme = this.resolveTheme(
|
|
1634
|
+
const theme = this.resolveTheme(this.getStoredTheme());
|
|
1142
1635
|
this.switch(theme);
|
|
1143
1636
|
});
|
|
1144
|
-
effect(() => {
|
|
1145
|
-
if (this.isBrowser) {
|
|
1146
|
-
localStorage.setItem('theme', this.#theme());
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
1637
|
}
|
|
1150
1638
|
/**
|
|
1151
1639
|
* Switches theme.
|
|
@@ -1157,28 +1645,116 @@ class ThemeService {
|
|
|
1157
1645
|
*/
|
|
1158
1646
|
switch(theme) {
|
|
1159
1647
|
const requestedTheme = theme ? this.resolveTheme(theme) : undefined;
|
|
1160
|
-
const newTheme = requestedTheme ??
|
|
1648
|
+
const newTheme = requestedTheme ?? this.nextTheme();
|
|
1161
1649
|
if (this.isBrowser) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
newTheme === themes.light && html.classList.remove(this.darkPrefix);
|
|
1650
|
+
this.applyThemeToDom(newTheme);
|
|
1651
|
+
this.persistTheme(newTheme);
|
|
1165
1652
|
}
|
|
1166
1653
|
this.#theme.set(newTheme);
|
|
1167
1654
|
}
|
|
1655
|
+
is(theme) {
|
|
1656
|
+
const allowed = Array.isArray(theme) ? theme : [theme];
|
|
1657
|
+
return allowed.includes(this.#theme());
|
|
1658
|
+
}
|
|
1168
1659
|
resolveTheme(theme) {
|
|
1169
|
-
|
|
1660
|
+
if (theme && this.registry.includes(theme)) {
|
|
1661
|
+
return theme;
|
|
1662
|
+
}
|
|
1663
|
+
return this.themeDefault;
|
|
1664
|
+
}
|
|
1665
|
+
resolveRegistry(registry) {
|
|
1666
|
+
return registry?.length ? registry : [themes.light, themes.dark];
|
|
1667
|
+
}
|
|
1668
|
+
resolveInitialTheme(theme) {
|
|
1669
|
+
const next = theme ?? themes.light;
|
|
1670
|
+
return this.registry.includes(next) ? next : (this.registry[0] ?? themes.light);
|
|
1671
|
+
}
|
|
1672
|
+
getStoredTheme() {
|
|
1673
|
+
return this.persistence.get('theme');
|
|
1674
|
+
}
|
|
1675
|
+
persistTheme(theme) {
|
|
1676
|
+
this.persistence.set('theme', theme);
|
|
1677
|
+
}
|
|
1678
|
+
applyThemeToDom(theme) {
|
|
1679
|
+
const root = this.resolveRootElement();
|
|
1680
|
+
if (!root) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (this.domStrategy === 'data-attribute') {
|
|
1684
|
+
root.setAttribute(this.attributeName, theme);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
this.clearThemeClasses(root);
|
|
1688
|
+
if (theme === themes.dark) {
|
|
1689
|
+
root.classList.add(this.darkPrefix);
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
root.classList.remove(this.darkPrefix);
|
|
1693
|
+
}
|
|
1694
|
+
const className = this.resolveThemeClassName(theme);
|
|
1695
|
+
if (className) {
|
|
1696
|
+
root.classList.add(className);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
nextTheme() {
|
|
1700
|
+
const currentIndex = this.registry.indexOf(this.#theme());
|
|
1701
|
+
if (currentIndex < 0) {
|
|
1702
|
+
return this.themeDefault;
|
|
1703
|
+
}
|
|
1704
|
+
return this.registry[(currentIndex + 1) % this.registry.length] ?? this.themeDefault;
|
|
1705
|
+
}
|
|
1706
|
+
resolveRootElement() {
|
|
1707
|
+
if (!this.rootSelector) {
|
|
1708
|
+
return this.document.documentElement;
|
|
1709
|
+
}
|
|
1710
|
+
return this.document.querySelector(this.rootSelector);
|
|
1711
|
+
}
|
|
1712
|
+
resolveThemeClassName(theme) {
|
|
1713
|
+
if (this.classNameBuilder) {
|
|
1714
|
+
return this.classNameBuilder(theme);
|
|
1715
|
+
}
|
|
1716
|
+
if (this.themeClassPrefix) {
|
|
1717
|
+
return `${this.themeClassPrefix}${theme}`;
|
|
1718
|
+
}
|
|
1719
|
+
return null;
|
|
1170
1720
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1721
|
+
clearThemeClasses(root) {
|
|
1722
|
+
if (!this.classNameBuilder && !this.themeClassPrefix) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
for (const registeredTheme of this.registry) {
|
|
1726
|
+
const className = this.resolveThemeClassName(registeredTheme);
|
|
1727
|
+
if (className) {
|
|
1728
|
+
root.classList.remove(className);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1733
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
1173
1734
|
}
|
|
1174
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1735
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ThemeService, decorators: [{
|
|
1175
1736
|
type: Injectable,
|
|
1176
1737
|
args: [{
|
|
1177
1738
|
providedIn: 'root',
|
|
1178
1739
|
}]
|
|
1179
1740
|
}], ctorParameters: () => [] });
|
|
1180
1741
|
|
|
1742
|
+
let warnedLegacyProvider = false;
|
|
1743
|
+
/**
|
|
1744
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1745
|
+
*/
|
|
1181
1746
|
function provideReInit(config) {
|
|
1747
|
+
if (typeof ngDevMode !== 'undefined' && !warnedLegacyProvider) {
|
|
1748
|
+
warnedLegacyProvider = true;
|
|
1749
|
+
// eslint-disable-next-line no-console
|
|
1750
|
+
console.warn('[presentia] provideReInit(...) is the legacy 1.x provider. Prefer providePresentia(...) for the grouped v2 config API.');
|
|
1751
|
+
}
|
|
1752
|
+
return buildPresentiaProviders(config);
|
|
1753
|
+
}
|
|
1754
|
+
function __resetPresentiaProviderWarningsForTests() {
|
|
1755
|
+
warnedLegacyProvider = false;
|
|
1756
|
+
}
|
|
1757
|
+
function buildPresentiaProviders(config) {
|
|
1182
1758
|
return makeEnvironmentProviders([
|
|
1183
1759
|
{ provide: TRANSLATION, deps: [LangService], useFactory: (ls) => ls },
|
|
1184
1760
|
{ provide: SELECTED_LANG, deps: [LangService], useFactory: (ls) => ls[innerLangVal] },
|
|
@@ -1192,13 +1768,78 @@ function provideReInit(config) {
|
|
|
1192
1768
|
{ provide: CURRENT_DEVICE, deps: [AdaptiveService], useFactory: (ls) => ls.device },
|
|
1193
1769
|
{ provide: DEVICE_BREAKPOINTS, useValue: config.breakpoints || defaultBreakpoints },
|
|
1194
1770
|
{ provide: THEME_CONFIG, useValue: config.theme || defaultThemeConfig },
|
|
1771
|
+
{ provide: THEME_PERSISTENCE_ADAPTER, useValue: defaultThemePersistenceAdapter },
|
|
1772
|
+
{ provide: LANG_PERSISTENCE_ADAPTER, useValue: defaultLangPersistenceAdapter },
|
|
1195
1773
|
{ provide: LANG_CONFIG, useValue: config.locale || { defaultValue: '--------' } },
|
|
1196
1774
|
{ provide: LANG_PIPE_CONFIG, useValue: config.langPipe || {} },
|
|
1197
1775
|
{ provide: LANG_MISSING_KEY_HANDLER, useValue: config.langMissingKeyHandler ?? null },
|
|
1198
1776
|
{ provide: LOCALE_ID, useValue: config.locale.defaultLang ?? 'ru' },
|
|
1777
|
+
providePresentiaRouteNamespacePreload(),
|
|
1778
|
+
]);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function providePresentia(config) {
|
|
1782
|
+
const routesConfig = config.lang.preload?.routes;
|
|
1783
|
+
const diagnosticsEnabled = config.lang.diagnostics?.lateNamespaceLoads;
|
|
1784
|
+
const routeNamespacePreload = routesConfig || diagnosticsEnabled
|
|
1785
|
+
? { ...routesConfig, diagnostics: diagnosticsEnabled ?? routesConfig?.diagnostics }
|
|
1786
|
+
: undefined;
|
|
1787
|
+
const extraProviders = [];
|
|
1788
|
+
const langStorage = createPresentiaStorage(config.lang.persistence, config.lang.storage ?? config.lang.persistenceAdapter);
|
|
1789
|
+
const themeStorage = createPresentiaStorage(config.theme?.persistence, config.theme?.storage ?? config.theme?.persistenceAdapter);
|
|
1790
|
+
extraProviders.push({ provide: LANG_PERSISTENCE_ADAPTER, useValue: langStorage });
|
|
1791
|
+
extraProviders.push({ provide: THEME_PERSISTENCE_ADAPTER, useValue: themeStorage });
|
|
1792
|
+
return makeEnvironmentProviders([
|
|
1793
|
+
buildPresentiaProviders({
|
|
1794
|
+
locale: {
|
|
1795
|
+
url: config.lang.source.url,
|
|
1796
|
+
isFromAssets: config.lang.source.fromAssets,
|
|
1797
|
+
defaultLang: config.lang.source.defaultLang,
|
|
1798
|
+
fallbackLang: config.lang.source.fallbackLang,
|
|
1799
|
+
supportedLangs: config.lang.source.supportedLangs,
|
|
1800
|
+
kgValue: config.lang.source.kgValue,
|
|
1801
|
+
defaultValue: config.lang.rendering?.missingValue,
|
|
1802
|
+
preloadNamespaces: config.lang.preload?.global,
|
|
1803
|
+
requestBuilder: config.lang.transport?.requestBuilder,
|
|
1804
|
+
requestOptionsFactory: config.lang.transport?.requestOptionsFactory,
|
|
1805
|
+
responseAdapter: config.lang.transport?.responseAdapter,
|
|
1806
|
+
batchRequestBuilder: config.lang.transport?.batchRequestBuilder,
|
|
1807
|
+
batchResponseAdapter: config.lang.transport?.batchResponseAdapter,
|
|
1808
|
+
namespaceCache: config.lang.cache,
|
|
1809
|
+
routeNamespacePreload,
|
|
1810
|
+
maxBatchSize: config.lang.transport?.maxBatchSize,
|
|
1811
|
+
},
|
|
1812
|
+
theme: config.theme
|
|
1813
|
+
? {
|
|
1814
|
+
registry: config.theme.registry,
|
|
1815
|
+
defaultTheme: config.theme.defaultTheme,
|
|
1816
|
+
darkThemePrefix: config.theme.dom?.darkClassName,
|
|
1817
|
+
dom: config.theme.dom
|
|
1818
|
+
? {
|
|
1819
|
+
strategy: config.theme.dom.strategy,
|
|
1820
|
+
rootSelector: config.theme.dom.rootSelector,
|
|
1821
|
+
darkThemePrefix: config.theme.dom.darkClassName,
|
|
1822
|
+
attributeName: config.theme.dom.attributeName,
|
|
1823
|
+
themeClassPrefix: config.theme.dom.classPrefix,
|
|
1824
|
+
classNameBuilder: config.theme.dom.classNameBuilder,
|
|
1825
|
+
}
|
|
1826
|
+
: undefined,
|
|
1827
|
+
}
|
|
1828
|
+
: undefined,
|
|
1829
|
+
breakpoints: config.adaptive?.breakpoints,
|
|
1830
|
+
langPipe: {
|
|
1831
|
+
placeholder: config.lang.rendering?.placeholder,
|
|
1832
|
+
},
|
|
1833
|
+
langMissingKeyHandler: config.lang.missingKeyHandler,
|
|
1834
|
+
}),
|
|
1835
|
+
...extraProviders,
|
|
1199
1836
|
]);
|
|
1200
1837
|
}
|
|
1201
1838
|
|
|
1839
|
+
/**
|
|
1840
|
+
* @deprecated Prefer `providePresentia(...)` for new integrations.
|
|
1841
|
+
*/
|
|
1842
|
+
|
|
1202
1843
|
/**
|
|
1203
1844
|
* Service for managing page SEO metadata.
|
|
1204
1845
|
*
|
|
@@ -1326,10 +1967,10 @@ class SeoService {
|
|
|
1326
1967
|
link.href = href;
|
|
1327
1968
|
}
|
|
1328
1969
|
}
|
|
1329
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.
|
|
1330
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.
|
|
1970
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1971
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, providedIn: 'root' });
|
|
1331
1972
|
}
|
|
1332
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
1973
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoService, decorators: [{
|
|
1333
1974
|
type: Injectable,
|
|
1334
1975
|
args: [{ providedIn: 'root' }]
|
|
1335
1976
|
}] });
|
|
@@ -1355,7 +1996,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImpor
|
|
|
1355
1996
|
class SeoRouteListener {
|
|
1356
1997
|
router = inject(Router);
|
|
1357
1998
|
seo = inject(SeoService);
|
|
1358
|
-
ar = inject(ActivatedRoute);
|
|
1359
1999
|
destroyRef = inject(DestroyRef);
|
|
1360
2000
|
initialized = false;
|
|
1361
2001
|
baseUrl = '';
|
|
@@ -1369,9 +2009,9 @@ class SeoRouteListener {
|
|
|
1369
2009
|
init(baseUrl) {
|
|
1370
2010
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
1371
2011
|
const applyRouteSeo = () => {
|
|
1372
|
-
const
|
|
1373
|
-
const data =
|
|
1374
|
-
const url = data.canonical
|
|
2012
|
+
const snapshot = deepestRouteSnapshot(this.router.routerState.snapshot.root);
|
|
2013
|
+
const data = snapshotMergedData(snapshot);
|
|
2014
|
+
const url = resolveCanonicalUrl(data.canonical, this.baseUrl, snapshotFullPath(snapshot));
|
|
1375
2015
|
data.title && this.seo.setTitle(data.title);
|
|
1376
2016
|
data.description && this.seo.setDescription(data.description);
|
|
1377
2017
|
data.twitter && this.seo.setTwitter(data.twitter);
|
|
@@ -1397,123 +2037,33 @@ class SeoRouteListener {
|
|
|
1397
2037
|
});
|
|
1398
2038
|
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
1399
2039
|
}
|
|
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' });
|
|
2040
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2041
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, providedIn: 'root' });
|
|
1416
2042
|
}
|
|
1417
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
2043
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SeoRouteListener, decorators: [{
|
|
1418
2044
|
type: Injectable,
|
|
1419
2045
|
args: [{ providedIn: 'root' }]
|
|
1420
2046
|
}] });
|
|
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());
|
|
2047
|
+
function resolveCanonicalUrl(canonical, baseUrl, path) {
|
|
2048
|
+
if (!canonical) {
|
|
2049
|
+
return joinBaseUrl(baseUrl, path);
|
|
1478
2050
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
return computed(() => this.#data()[key]);
|
|
2051
|
+
if (/^https?:\/\//i.test(canonical)) {
|
|
2052
|
+
return canonical;
|
|
1482
2053
|
}
|
|
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('/') : '';
|
|
2054
|
+
return joinBaseUrl(baseUrl, canonical);
|
|
1501
2055
|
}
|
|
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 }), {});
|
|
2056
|
+
function joinBaseUrl(baseUrl, path) {
|
|
2057
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
2058
|
+
if (!baseUrl) {
|
|
2059
|
+
return normalizedPath ? `/${normalizedPath}` : '/';
|
|
2060
|
+
}
|
|
2061
|
+
return normalizedPath ? `${baseUrl}/${normalizedPath}` : baseUrl;
|
|
1512
2062
|
}
|
|
1513
2063
|
|
|
1514
2064
|
/**
|
|
1515
2065
|
* Generated bundle index. Do not edit.
|
|
1516
2066
|
*/
|
|
1517
2067
|
|
|
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 };
|
|
2068
|
+
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, defaultLangPersistenceAdapter, defaultThemeConfig, defaultThemePersistenceAdapter, innerLangVal, providePresentia, providePresentiaRouteNamespacePreload, provideReInit, themes };
|
|
1519
2069
|
//# sourceMappingURL=reforgium-presentia.mjs.map
|