@ama-mfe/ng-utils 14.0.0-next.0 → 14.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -6
- package/fesm2022/ama-mfe-ng-utils.mjs +831 -325
- package/fesm2022/ama-mfe-ng-utils.mjs.map +1 -1
- package/index.d.ts +534 -191
- package/index.d.ts.map +1 -1
- package/package.json +11 -11
|
@@ -1,16 +1,79 @@
|
|
|
1
1
|
import { MessagePeerService, MESSAGE_PEER_CONFIG, MESSAGE_PEER_CONNECT_OPTIONS } from '@amadeus-it-group/microfrontends-angular';
|
|
2
2
|
export { MessagePeerService as ConnectionService } from '@amadeus-it-group/microfrontends-angular';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { input, inject, ElementRef, computed, SecurityContext, effect, HostBinding, Directive, Injectable, signal, DestroyRef, provideAppInitializer, Pipe, makeEnvironmentProviders, untracked, afterNextRender, Renderer2 } from '@angular/core';
|
|
5
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
6
6
|
import { LoggerService } from '@o3r/logger';
|
|
7
|
-
import {
|
|
7
|
+
import { HISTORY_MESSAGE_TYPE, USER_ACTIVITY_MESSAGE_TYPE, NAVIGATION_MESSAGE_TYPE, RESIZE_MESSAGE_TYPE, THEME_MESSAGE_TYPE } from '@ama-mfe/messages';
|
|
8
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
8
9
|
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
|
9
10
|
import { Subject, filter, map } from 'rxjs';
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
class ConnectDirective {
|
|
13
|
+
/**
|
|
14
|
+
* Binds the `src` attribute of the iframe to the sanitized source URL.
|
|
15
|
+
*/
|
|
16
|
+
get srcAttr() {
|
|
17
|
+
return this.src();
|
|
18
|
+
}
|
|
19
|
+
constructor() {
|
|
20
|
+
/**
|
|
21
|
+
* The connection ID required for the message peer service.
|
|
22
|
+
*/
|
|
23
|
+
this.connect = input.required(...(ngDevMode ? [{ debugName: "connect" }] : []));
|
|
24
|
+
/**
|
|
25
|
+
* The sanitized source URL for the iframe.
|
|
26
|
+
*/
|
|
27
|
+
this.src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
|
|
28
|
+
this.messageService = inject(MessagePeerService);
|
|
29
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
30
|
+
this.iframeElement = inject(ElementRef).nativeElement;
|
|
31
|
+
this.clientOrigin = computed(() => {
|
|
32
|
+
const src = this.src();
|
|
33
|
+
const srcString = src && this.domSanitizer.sanitize(SecurityContext.RESOURCE_URL, src);
|
|
34
|
+
return srcString && new URL(srcString).origin;
|
|
35
|
+
}, ...(ngDevMode ? [{ debugName: "clientOrigin" }] : []));
|
|
36
|
+
const logger = inject(LoggerService);
|
|
37
|
+
// When the origin or connection ID change - reconnect the message service
|
|
38
|
+
effect((onCleanup) => {
|
|
39
|
+
let stopHandshakeListening = () => { };
|
|
40
|
+
const origin = this.clientOrigin();
|
|
41
|
+
const id = this.connect();
|
|
42
|
+
const source = this.iframeElement.contentWindow;
|
|
43
|
+
// listen for handshakes only if we know the origin and were given a connection ID
|
|
44
|
+
if (origin && source && id) {
|
|
45
|
+
try {
|
|
46
|
+
stopHandshakeListening = this.messageService.listen({ id, source, origin });
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
logger.error(`Failed to start listening for (connection ID: ${id})`, e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// stop listening for handshakes and disconnect previous connection when:
|
|
53
|
+
// - origin/connection ID change
|
|
54
|
+
// - the directive is destroyed
|
|
55
|
+
onCleanup(() => {
|
|
56
|
+
stopHandshakeListening();
|
|
57
|
+
this.messageService.disconnect();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConnectDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
62
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: ConnectDirective, isStandalone: true, selector: "iframe[connect]", inputs: { connect: { classPropertyName: "connect", publicName: "connect", isSignal: true, isRequired: true, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "src": "this.srcAttr" } }, ngImport: i0 }); }
|
|
63
|
+
}
|
|
64
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConnectDirective, decorators: [{
|
|
65
|
+
type: Directive,
|
|
66
|
+
args: [{
|
|
67
|
+
selector: 'iframe[connect]',
|
|
68
|
+
standalone: true
|
|
69
|
+
}]
|
|
70
|
+
}], ctorParameters: () => [], propDecorators: { connect: [{ type: i0.Input, args: [{ isSignal: true, alias: "connect", required: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], srcAttr: [{
|
|
71
|
+
type: HostBinding,
|
|
72
|
+
args: ['src']
|
|
73
|
+
}] } });
|
|
11
74
|
|
|
12
75
|
/**
|
|
13
|
-
* Gets the available consumers and formats them into a {@
|
|
76
|
+
* Gets the available consumers and formats them into a {@link DeclareMessages} object.
|
|
14
77
|
* @param consumers - The list of registered message consumers.
|
|
15
78
|
* @returns The formatted DeclareMessages object.
|
|
16
79
|
*/
|
|
@@ -74,14 +137,16 @@ class ProducerManagerService {
|
|
|
74
137
|
.filter(({ types }) => (Array.isArray(types) ? types : [types]).includes(message.source.type));
|
|
75
138
|
const handlersPresent = handlers.length > 0;
|
|
76
139
|
if (handlersPresent) {
|
|
77
|
-
await Promise.all(
|
|
140
|
+
await Promise.all(
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable -- `handleError` can return void or Promise<void>
|
|
142
|
+
handlers.map((handler) => handler.handleError(message)));
|
|
78
143
|
}
|
|
79
144
|
return handlersPresent;
|
|
80
145
|
}
|
|
81
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
82
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
146
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProducerManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
147
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProducerManagerService, providedIn: 'root' }); }
|
|
83
148
|
}
|
|
84
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
149
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ProducerManagerService, decorators: [{
|
|
85
150
|
type: Injectable,
|
|
86
151
|
args: [{
|
|
87
152
|
providedIn: 'root'
|
|
@@ -173,10 +238,10 @@ class ConsumerManagerService {
|
|
|
173
238
|
return consumers.filter((c) => c !== consumer);
|
|
174
239
|
});
|
|
175
240
|
}
|
|
176
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
177
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
241
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConsumerManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
242
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConsumerManagerService, providedIn: 'root' }); }
|
|
178
243
|
}
|
|
179
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
244
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConsumerManagerService, decorators: [{
|
|
180
245
|
type: Injectable,
|
|
181
246
|
args: [{
|
|
182
247
|
providedIn: 'root'
|
|
@@ -208,53 +273,49 @@ const registerConsumer = (consumer) => {
|
|
|
208
273
|
});
|
|
209
274
|
};
|
|
210
275
|
|
|
211
|
-
/** the error message type */
|
|
212
|
-
const ERROR_MESSAGE_TYPE = 'error';
|
|
213
|
-
|
|
214
276
|
/**
|
|
215
|
-
*
|
|
277
|
+
* A service that handles history messages.
|
|
278
|
+
*
|
|
279
|
+
* This service listens for history messages and navigates accordingly.
|
|
216
280
|
*/
|
|
217
|
-
class
|
|
281
|
+
class HistoryConsumerService {
|
|
218
282
|
constructor() {
|
|
219
|
-
this.newHeight = signal(undefined, ...(ngDevMode ? [{ debugName: "newHeight" }] : []));
|
|
220
|
-
/**
|
|
221
|
-
* A readonly signal that provides the new height information from the channel.
|
|
222
|
-
*/
|
|
223
|
-
this.newHeightFromChannel = this.newHeight.asReadonly();
|
|
224
283
|
/**
|
|
225
|
-
* The type of messages this service handles
|
|
284
|
+
* The type of messages this service handles.
|
|
226
285
|
*/
|
|
227
|
-
this.type =
|
|
286
|
+
this.type = HISTORY_MESSAGE_TYPE;
|
|
228
287
|
/**
|
|
229
|
-
*
|
|
288
|
+
* @inheritdoc
|
|
230
289
|
*/
|
|
231
290
|
this.supportedVersions = {
|
|
232
291
|
/**
|
|
233
|
-
* Use the message
|
|
292
|
+
* Use the message payload to navigate in the history
|
|
234
293
|
* @param message message to consume
|
|
235
294
|
*/
|
|
236
|
-
'1.0': (message) =>
|
|
295
|
+
'1.0': (message) => {
|
|
296
|
+
history.go(message.payload.delta);
|
|
297
|
+
}
|
|
237
298
|
};
|
|
238
299
|
this.consumerManagerService = inject(ConsumerManagerService);
|
|
239
300
|
this.start();
|
|
240
301
|
inject(DestroyRef).onDestroy(() => this.stop());
|
|
241
302
|
}
|
|
242
303
|
/**
|
|
243
|
-
*
|
|
304
|
+
* @inheritdoc
|
|
244
305
|
*/
|
|
245
306
|
start() {
|
|
246
307
|
this.consumerManagerService.register(this);
|
|
247
308
|
}
|
|
248
309
|
/**
|
|
249
|
-
*
|
|
310
|
+
* @inheritdoc
|
|
250
311
|
*/
|
|
251
312
|
stop() {
|
|
252
313
|
this.consumerManagerService.unregister(this);
|
|
253
314
|
}
|
|
254
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
255
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
315
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: HistoryConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
316
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: HistoryConsumerService, providedIn: 'root' }); }
|
|
256
317
|
}
|
|
257
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: HistoryConsumerService, decorators: [{
|
|
258
319
|
type: Injectable,
|
|
259
320
|
args: [{
|
|
260
321
|
providedIn: 'root'
|
|
@@ -262,102 +323,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImpor
|
|
|
262
323
|
}], ctorParameters: () => [] });
|
|
263
324
|
|
|
264
325
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
326
|
+
* Provides necessary overrides to make the module navigation in history work in an embedded context :
|
|
327
|
+
* - Prevent pushing states to history, replace state instead
|
|
328
|
+
* - Handle history navigation via History messages to let the host manage the states
|
|
267
329
|
*/
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* This method sets up a `ResizeObserver` to observe changes in the document's body height.
|
|
286
|
-
* When the height changes, it sends a resize message with the new height, to the connected peers
|
|
287
|
-
*/
|
|
288
|
-
startResizeObserver() {
|
|
289
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
290
|
-
const newHeight = document.body.getBoundingClientRect().height;
|
|
291
|
-
if (!this.actualHeight || newHeight !== this.actualHeight) {
|
|
292
|
-
this.actualHeight = newHeight;
|
|
293
|
-
const messageV10 = {
|
|
294
|
-
type: 'resize',
|
|
295
|
-
version: '1.0',
|
|
296
|
-
height: this.actualHeight
|
|
297
|
-
};
|
|
298
|
-
// TODO: sendBest() is not implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
299
|
-
this.messageService.send(messageV10);
|
|
300
|
-
}
|
|
330
|
+
function provideHistoryOverrides() {
|
|
331
|
+
return provideAppInitializer(() => {
|
|
332
|
+
const messageService = inject((MessagePeerService));
|
|
333
|
+
const navigate = (delta) => {
|
|
334
|
+
messageService.send({
|
|
335
|
+
type: 'history',
|
|
336
|
+
version: '1.0',
|
|
337
|
+
delta
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
Object.defineProperty(history, 'pushState', {
|
|
341
|
+
value: (data, unused, url) => {
|
|
342
|
+
history.replaceState(data, unused, url);
|
|
343
|
+
},
|
|
344
|
+
writable: false,
|
|
345
|
+
configurable: false
|
|
301
346
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: ResizeService, decorators: [{
|
|
308
|
-
type: Injectable,
|
|
309
|
-
args: [{
|
|
310
|
-
providedIn: 'root'
|
|
311
|
-
}]
|
|
312
|
-
}], ctorParameters: () => [] });
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* A directive that adjusts the height of an element based on resize messages from a specified channel.
|
|
316
|
-
*/
|
|
317
|
-
class ScalableDirective {
|
|
318
|
-
constructor() {
|
|
319
|
-
/**
|
|
320
|
-
* The connection ID for the element, used as channel id backup
|
|
321
|
-
*/
|
|
322
|
-
this.connect = input(...(ngDevMode ? [undefined, { debugName: "connect" }] : []));
|
|
323
|
-
/**
|
|
324
|
-
* The channel id
|
|
325
|
-
*/
|
|
326
|
-
this.scalable = input(...(ngDevMode ? [undefined, { debugName: "scalable" }] : []));
|
|
327
|
-
this.resizeHandler = inject(ResizeConsumerService);
|
|
328
|
-
/**
|
|
329
|
-
* This signal checks if the current channel requesting the resize matches the channel ID from the resize handler.
|
|
330
|
-
* If they match, it returns the new height information; otherwise, it returns undefined.
|
|
331
|
-
*/
|
|
332
|
-
this.newHeightFromChannel = computed(() => {
|
|
333
|
-
const channelAskingResize = this.scalable() || this.connect();
|
|
334
|
-
const newHeightFromChannel = this.resizeHandler.newHeightFromChannel();
|
|
335
|
-
if (channelAskingResize && newHeightFromChannel?.channelId === channelAskingResize) {
|
|
336
|
-
return newHeightFromChannel;
|
|
337
|
-
}
|
|
338
|
-
return undefined;
|
|
339
|
-
}, ...(ngDevMode ? [{ debugName: "newHeightFromChannel" }] : []));
|
|
340
|
-
const elem = inject(ElementRef);
|
|
341
|
-
const renderer = inject(Renderer2);
|
|
342
|
-
this.resizeHandler.start();
|
|
343
|
-
/** When a new height value is received set the height of the host element (in pixels) */
|
|
344
|
-
effect(() => {
|
|
345
|
-
const newHeightFromChannel = this.newHeightFromChannel();
|
|
346
|
-
if (newHeightFromChannel) {
|
|
347
|
-
renderer.setStyle(elem.nativeElement, 'height', `${newHeightFromChannel.height}px`);
|
|
348
|
-
}
|
|
347
|
+
Object.defineProperty(history, 'back', {
|
|
348
|
+
value: () => navigate(-1),
|
|
349
|
+
writable: false,
|
|
350
|
+
configurable: false
|
|
349
351
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
Object.defineProperty(history, 'forward', {
|
|
353
|
+
value: () => navigate(1),
|
|
354
|
+
writable: false,
|
|
355
|
+
configurable: false
|
|
356
|
+
});
|
|
357
|
+
Object.defineProperty(history, 'go', {
|
|
358
|
+
value: (delta) => navigate(delta),
|
|
359
|
+
writable: false,
|
|
360
|
+
configurable: false
|
|
361
|
+
});
|
|
362
|
+
});
|
|
353
363
|
}
|
|
354
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: ScalableDirective, decorators: [{
|
|
355
|
-
type: Directive,
|
|
356
|
-
args: [{
|
|
357
|
-
selector: '[scalable]',
|
|
358
|
-
standalone: true
|
|
359
|
-
}]
|
|
360
|
-
}], ctorParameters: () => [] });
|
|
361
364
|
|
|
362
365
|
const SESSION_STORAGE_KEY = 'ama-mfe-host-info';
|
|
363
366
|
/**
|
|
@@ -384,12 +387,13 @@ const hostQueryParams = [MFE_HOST_URL_PARAM, MFE_HOST_APPLICATION_ID_PARAM, MFE_
|
|
|
384
387
|
* - use `document.referrer` (will only work if called before any redirection in the iframe)
|
|
385
388
|
* The host application ID is taken from the search parameter {@link MFE_HOST_APPLICATION_ID_PARAM} in the URL of the iframe
|
|
386
389
|
* The module application ID is taken from the search parameter {@link MFE_APPLICATION_ID_PARAM} in the URL of the iframe
|
|
390
|
+
* @param locationParam - A {@link Location} object with information about the current location of the document. Defaults to global {@link location}.
|
|
387
391
|
*/
|
|
388
|
-
function getHostInfo() {
|
|
389
|
-
const searchParams = new URLSearchParams(
|
|
392
|
+
function getHostInfo(locationParam = location) {
|
|
393
|
+
const searchParams = new URLSearchParams(locationParam.search);
|
|
390
394
|
const storedHostInfo = JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY) || '{}');
|
|
391
395
|
return {
|
|
392
|
-
hostURL: searchParams.get(MFE_HOST_URL_PARAM) || storedHostInfo.hostURL ||
|
|
396
|
+
hostURL: searchParams.get(MFE_HOST_URL_PARAM) || storedHostInfo.hostURL || locationParam.ancestorOrigins?.[0] || document.referrer,
|
|
393
397
|
hostApplicationId: searchParams.get(MFE_HOST_APPLICATION_ID_PARAM) || storedHostInfo.hostApplicationId,
|
|
394
398
|
moduleApplicationId: searchParams.get(MFE_MODULE_APPLICATION_ID_PARAM) || storedHostInfo.moduleApplicationId
|
|
395
399
|
};
|
|
@@ -427,16 +431,92 @@ class HostInfoPipe {
|
|
|
427
431
|
return typeof url === 'string' ? moduleUrlStringyfied : this.domSanitizer.bypassSecurityTrustResourceUrl(moduleUrlStringyfied);
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
431
|
-
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.
|
|
434
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: HostInfoPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
435
|
+
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: HostInfoPipe, isStandalone: true, name: "hostInfo" }); }
|
|
432
436
|
}
|
|
433
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
437
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: HostInfoPipe, decorators: [{
|
|
434
438
|
type: Pipe,
|
|
435
439
|
args: [{
|
|
436
440
|
name: 'hostInfo'
|
|
437
441
|
}]
|
|
438
442
|
}] });
|
|
439
443
|
|
|
444
|
+
/** the error message type */
|
|
445
|
+
const ERROR_MESSAGE_TYPE = 'error';
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Type guard to check if a message is a user activity message
|
|
449
|
+
* @param message The message to check
|
|
450
|
+
*/
|
|
451
|
+
function isUserActivityMessage(message) {
|
|
452
|
+
return (typeof message === 'object'
|
|
453
|
+
&& message !== null
|
|
454
|
+
&& 'type' in message
|
|
455
|
+
&& message.type === USER_ACTIVITY_MESSAGE_TYPE);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* A constant array of known message types and their versions.
|
|
460
|
+
*/
|
|
461
|
+
const KNOWN_MESSAGES = [
|
|
462
|
+
{
|
|
463
|
+
type: ERROR_MESSAGE_TYPE,
|
|
464
|
+
version: '1.0'
|
|
465
|
+
}
|
|
466
|
+
];
|
|
467
|
+
/**
|
|
468
|
+
* Returns the default options for starting a client endpoint peer connection.
|
|
469
|
+
* As `origin`, it will take the hostURL from {@link getHostInfo} and the `window` will be the parent window.
|
|
470
|
+
*/
|
|
471
|
+
function getDefaultClientEndpointStartOptions() {
|
|
472
|
+
const hostInfo = getHostInfo();
|
|
473
|
+
if (hostInfo.hostURL) {
|
|
474
|
+
return {
|
|
475
|
+
origin: new URL(hostInfo.hostURL).origin,
|
|
476
|
+
window: window.parent
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Return `true` if embedded inside an iframe, `false` otherwise
|
|
483
|
+
* @param windowParam - A {@link window} object with information about the current window of the document. Defaults to global {@link window}.
|
|
484
|
+
*/
|
|
485
|
+
function isEmbedded(windowParam = window) {
|
|
486
|
+
return windowParam.top !== windowParam.self;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Provide the communication protocol connection configuration
|
|
491
|
+
* @param connectionConfigOptions The identifier of the application in the communication protocol ecosystem plus the types of messages able to exchange and a logger object
|
|
492
|
+
*/
|
|
493
|
+
function provideConnection(connectionConfigOptions) {
|
|
494
|
+
persistHostInfo();
|
|
495
|
+
const connectionId = (isEmbedded() && getHostInfo().moduleApplicationId) || connectionConfigOptions?.id;
|
|
496
|
+
if (!connectionId) {
|
|
497
|
+
(connectionConfigOptions?.logger || console).error('An id (moduleId) needs to be provided for the application in order to establish a connection inside the communication protocol');
|
|
498
|
+
return makeEnvironmentProviders([]);
|
|
499
|
+
}
|
|
500
|
+
const config = {
|
|
501
|
+
id: connectionId,
|
|
502
|
+
messageCheckStrategy: 'version',
|
|
503
|
+
knownMessages: [...KNOWN_MESSAGES, ...(connectionConfigOptions?.knownMessages || [])]
|
|
504
|
+
};
|
|
505
|
+
return makeEnvironmentProviders([
|
|
506
|
+
{
|
|
507
|
+
provide: MESSAGE_PEER_CONFIG, useValue: config
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
provide: MESSAGE_PEER_CONNECT_OPTIONS, useValue: getDefaultClientEndpointStartOptions()
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
// in the case of the ConnectionService will extend the base service 'useExisting' should be used
|
|
514
|
+
provide: MessagePeerService, useClass: MessagePeerService, deps: [MESSAGE_PEER_CONFIG]
|
|
515
|
+
},
|
|
516
|
+
...isEmbedded() ? [provideHistoryOverrides()] : []
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
519
|
+
|
|
440
520
|
/**
|
|
441
521
|
* A service that handles navigation messages and routing.
|
|
442
522
|
*
|
|
@@ -507,10 +587,10 @@ class NavigationConsumerService {
|
|
|
507
587
|
stop() {
|
|
508
588
|
this.consumerManagerService.unregister(this);
|
|
509
589
|
}
|
|
510
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
511
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
590
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NavigationConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
591
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NavigationConsumerService, providedIn: 'root' }); }
|
|
512
592
|
}
|
|
513
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
593
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: NavigationConsumerService, decorators: [{
|
|
514
594
|
type: Injectable,
|
|
515
595
|
args: [{
|
|
516
596
|
providedIn: 'root'
|
|
@@ -553,21 +633,72 @@ class RouteMemorizeService {
|
|
|
553
633
|
getRoute(channelId) {
|
|
554
634
|
return this.routeStack[channelId];
|
|
555
635
|
}
|
|
556
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
557
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
636
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RouteMemorizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
637
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RouteMemorizeService, providedIn: 'root' }); }
|
|
558
638
|
}
|
|
559
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
639
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RouteMemorizeService, decorators: [{
|
|
560
640
|
type: Injectable,
|
|
561
641
|
args: [{
|
|
562
642
|
providedIn: 'root'
|
|
563
643
|
}]
|
|
564
644
|
}] });
|
|
565
645
|
|
|
566
|
-
|
|
646
|
+
/**
|
|
647
|
+
* A pipe that restores a route with optional query parameters and memory channel ID.
|
|
648
|
+
*
|
|
649
|
+
* This pipe is used to transform a URL or SafeResourceUrl by appending query parameters
|
|
650
|
+
* and adjusting the pathname based on the current active route and memorized route.
|
|
651
|
+
*/
|
|
652
|
+
class RestoreRoute {
|
|
567
653
|
constructor() {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
654
|
+
this.activeRoute = inject(ActivatedRoute);
|
|
655
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
656
|
+
this.routeMemorizeService = inject(RouteMemorizeService, { optional: true });
|
|
657
|
+
this.window = inject(Window, { optional: true }) || window;
|
|
658
|
+
}
|
|
659
|
+
transform(url, options) {
|
|
660
|
+
const urlString = typeof url === 'string'
|
|
661
|
+
? url
|
|
662
|
+
: this.domSanitizer.sanitize(SecurityContext.RESOURCE_URL, url || null);
|
|
663
|
+
if (!url) {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
if (urlString) {
|
|
667
|
+
const moduleUrl = new URL(urlString);
|
|
668
|
+
const queryParamsModule = new URLSearchParams(moduleUrl.searchParams);
|
|
669
|
+
const channelId = options?.memoryChannelId;
|
|
670
|
+
const memorizedRoute = channelId && this.routeMemorizeService?.getRoute(channelId);
|
|
671
|
+
const topWindowUrl = new URL(memorizedRoute ? this.window.origin + memorizedRoute : this.window.location.href);
|
|
672
|
+
const queryParamsTopWindow = new URLSearchParams(topWindowUrl.search);
|
|
673
|
+
if (options?.propagateQueryParams) {
|
|
674
|
+
for (const [key, value] of queryParamsTopWindow) {
|
|
675
|
+
if (options?.overrideQueryParams || !queryParamsModule.has(key)) {
|
|
676
|
+
queryParamsModule.set(key, value);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
moduleUrl.search = queryParamsModule.toString();
|
|
681
|
+
moduleUrl.pathname += topWindowUrl.pathname.split(`/${this.activeRoute.routeConfig?.path}`).pop() || '';
|
|
682
|
+
moduleUrl.pathname = moduleUrl.pathname.replace(/\/{2,}/g, '/');
|
|
683
|
+
const moduleUrlStringyfied = moduleUrl.toString();
|
|
684
|
+
return typeof url === 'string' ? moduleUrlStringyfied : this.domSanitizer.bypassSecurityTrustResourceUrl(moduleUrlStringyfied);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RestoreRoute, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
688
|
+
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: RestoreRoute, isStandalone: true, name: "restoreRoute" }); }
|
|
689
|
+
}
|
|
690
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RestoreRoute, decorators: [{
|
|
691
|
+
type: Pipe,
|
|
692
|
+
args: [{
|
|
693
|
+
name: 'restoreRoute'
|
|
694
|
+
}]
|
|
695
|
+
}] });
|
|
696
|
+
|
|
697
|
+
class RouteMemorizeDirective {
|
|
698
|
+
constructor() {
|
|
699
|
+
/**
|
|
700
|
+
* Whether to memorize the route.
|
|
701
|
+
* Default is true.
|
|
571
702
|
*/
|
|
572
703
|
this.memorizeRoute = input(true, ...(ngDevMode ? [{ debugName: "memorizeRoute" }] : []));
|
|
573
704
|
/**
|
|
@@ -603,52 +734,23 @@ class RouteMemorizeDirective {
|
|
|
603
734
|
return;
|
|
604
735
|
}
|
|
605
736
|
const requested = requestedUrlSignal();
|
|
606
|
-
const
|
|
607
|
-
|
|
737
|
+
const channelId = this.connect();
|
|
738
|
+
const id = this.memorizeRouteId() || channelId;
|
|
739
|
+
if (requested && id && requested.channelId === channelId) {
|
|
608
740
|
memory.memorizeRoute(id, requested.url, untracked(this.maxAge));
|
|
609
741
|
}
|
|
610
742
|
});
|
|
611
743
|
}
|
|
612
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
613
|
-
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.
|
|
744
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RouteMemorizeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
745
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: RouteMemorizeDirective, isStandalone: true, selector: "iframe[memorizeRoute]", inputs: { memorizeRoute: { classPropertyName: "memorizeRoute", publicName: "memorizeRoute", isSignal: true, isRequired: false, transformFunction: null }, memorizeRouteId: { classPropertyName: "memorizeRouteId", publicName: "memorizeRouteId", isSignal: true, isRequired: false, transformFunction: null }, memorizeMaxAge: { classPropertyName: "memorizeMaxAge", publicName: "memorizeMaxAge", isSignal: true, isRequired: false, transformFunction: null }, memorizeRouteMaxAge: { classPropertyName: "memorizeRouteMaxAge", publicName: "memorizeRouteMaxAge", isSignal: true, isRequired: false, transformFunction: null }, connect: { classPropertyName: "connect", publicName: "connect", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
614
746
|
}
|
|
615
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
747
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RouteMemorizeDirective, decorators: [{
|
|
616
748
|
type: Directive,
|
|
617
749
|
args: [{
|
|
618
750
|
selector: 'iframe[memorizeRoute]',
|
|
619
751
|
standalone: true
|
|
620
752
|
}]
|
|
621
|
-
}], ctorParameters: () => [] });
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* A constant array of known message types and their versions.
|
|
625
|
-
*/
|
|
626
|
-
const KNOWN_MESSAGES = [
|
|
627
|
-
{
|
|
628
|
-
type: ERROR_MESSAGE_TYPE,
|
|
629
|
-
version: '1.0'
|
|
630
|
-
}
|
|
631
|
-
];
|
|
632
|
-
/**
|
|
633
|
-
* Returns the default options for starting a client endpoint peer connection.
|
|
634
|
-
* As `origin`, it will take the hostURL from {@link getHostInfo} and the `window` will be the parent window.
|
|
635
|
-
*/
|
|
636
|
-
function getDefaultClientEndpointStartOptions() {
|
|
637
|
-
const hostInfo = getHostInfo();
|
|
638
|
-
if (hostInfo.hostURL) {
|
|
639
|
-
return {
|
|
640
|
-
origin: new URL(hostInfo.hostURL).origin,
|
|
641
|
-
window: window.parent
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
return {};
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Return `true` if embedded inside an iframe, `false` otherwise
|
|
648
|
-
*/
|
|
649
|
-
function isEmbedded() {
|
|
650
|
-
return window.top !== window.self;
|
|
651
|
-
}
|
|
753
|
+
}], ctorParameters: () => [], propDecorators: { memorizeRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "memorizeRoute", required: false }] }], memorizeRouteId: [{ type: i0.Input, args: [{ isSignal: true, alias: "memorizeRouteId", required: false }] }], memorizeMaxAge: [{ type: i0.Input, args: [{ isSignal: true, alias: "memorizeMaxAge", required: false }] }], memorizeRouteMaxAge: [{ type: i0.Input, args: [{ isSignal: true, alias: "memorizeRouteMaxAge", required: false }] }], connect: [{ type: i0.Input, args: [{ isSignal: true, alias: "connect", required: false }] }] } });
|
|
652
754
|
|
|
653
755
|
/**
|
|
654
756
|
* A service that keeps in sync Router navigation and navigation messages.
|
|
@@ -662,6 +764,7 @@ class RoutingService {
|
|
|
662
764
|
this.activatedRoute = inject(ActivatedRoute);
|
|
663
765
|
this.messageService = inject((MessagePeerService));
|
|
664
766
|
this.logger = inject(LoggerService);
|
|
767
|
+
this.window = inject(Window, { optional: true }) || window;
|
|
665
768
|
/**
|
|
666
769
|
* @inheritdoc
|
|
667
770
|
*/
|
|
@@ -700,7 +803,7 @@ class RoutingService {
|
|
|
700
803
|
* Handles embedded routing by listening to router events and sending navigation messages to the connected endpoints.
|
|
701
804
|
* It can be a parent window or another iframe
|
|
702
805
|
* @note - This method has to be called in an injection context
|
|
703
|
-
* @param options - Optional parameters to control the routing behavior {@
|
|
806
|
+
* @param options - Optional parameters to control the routing behavior {@link RoutingServiceOptions}.
|
|
704
807
|
*/
|
|
705
808
|
handleEmbeddedRouting(options) {
|
|
706
809
|
const subRouteOnly = options?.subRouteOnly ?? false;
|
|
@@ -715,7 +818,7 @@ class RoutingService {
|
|
|
715
818
|
url
|
|
716
819
|
};
|
|
717
820
|
// TODO: sendBest() is not implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
718
|
-
if (isEmbedded()) {
|
|
821
|
+
if (isEmbedded(this.window)) {
|
|
719
822
|
this.messageService.send(messageV10);
|
|
720
823
|
}
|
|
721
824
|
else {
|
|
@@ -733,10 +836,10 @@ class RoutingService {
|
|
|
733
836
|
}
|
|
734
837
|
});
|
|
735
838
|
}
|
|
736
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
737
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
839
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RoutingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
840
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RoutingService, providedIn: 'root' }); }
|
|
738
841
|
}
|
|
739
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
842
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RoutingService, decorators: [{
|
|
740
843
|
type: Injectable,
|
|
741
844
|
args: [{
|
|
742
845
|
providedIn: 'root'
|
|
@@ -744,54 +847,152 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImpor
|
|
|
744
847
|
}], ctorParameters: () => [] });
|
|
745
848
|
|
|
746
849
|
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* This pipe is used to transform a URL or SafeResourceUrl by appending query parameters
|
|
750
|
-
* and adjusting the pathname based on the current active route and memorized route.
|
|
850
|
+
* This service listens for resize messages and updates the height of elements based on the received messages.
|
|
751
851
|
*/
|
|
752
|
-
class
|
|
852
|
+
class ResizeConsumerService {
|
|
753
853
|
constructor() {
|
|
754
|
-
this.
|
|
755
|
-
|
|
756
|
-
|
|
854
|
+
this.newHeight = signal(undefined, ...(ngDevMode ? [{ debugName: "newHeight" }] : []));
|
|
855
|
+
/**
|
|
856
|
+
* A readonly signal that provides the new height information from the channel.
|
|
857
|
+
*/
|
|
858
|
+
this.newHeightFromChannel = this.newHeight.asReadonly();
|
|
859
|
+
/**
|
|
860
|
+
* The type of messages this service handles ('resize').
|
|
861
|
+
*/
|
|
862
|
+
this.type = RESIZE_MESSAGE_TYPE;
|
|
863
|
+
/**
|
|
864
|
+
* The supported versions of resize messages and their handlers.
|
|
865
|
+
*/
|
|
866
|
+
this.supportedVersions = {
|
|
867
|
+
/**
|
|
868
|
+
* Use the message paylod to compute a new height and emit it via the public signal
|
|
869
|
+
* @param message message to consume
|
|
870
|
+
*/
|
|
871
|
+
'1.0': (message) => this.newHeight.set({ height: message.payload.height, channelId: message.from })
|
|
872
|
+
};
|
|
873
|
+
this.consumerManagerService = inject(ConsumerManagerService);
|
|
874
|
+
this.start();
|
|
875
|
+
inject(DestroyRef).onDestroy(() => this.stop());
|
|
757
876
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
877
|
+
/**
|
|
878
|
+
* Starts the resize handler service by registering it into the consumer manager service.
|
|
879
|
+
*/
|
|
880
|
+
start() {
|
|
881
|
+
this.consumerManagerService.register(this);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Stops the resize handler service by unregistering it from the consumer manager service.
|
|
885
|
+
*/
|
|
886
|
+
stop() {
|
|
887
|
+
this.consumerManagerService.unregister(this);
|
|
888
|
+
}
|
|
889
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
890
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeConsumerService, providedIn: 'root' }); }
|
|
891
|
+
}
|
|
892
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeConsumerService, decorators: [{
|
|
893
|
+
type: Injectable,
|
|
894
|
+
args: [{
|
|
895
|
+
providedIn: 'root'
|
|
896
|
+
}]
|
|
897
|
+
}], ctorParameters: () => [] });
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* This service observe changes in the document's body height.
|
|
901
|
+
* When the height changes, it sends a resize message with the new height, to the connected peers
|
|
902
|
+
*/
|
|
903
|
+
class ResizeService {
|
|
904
|
+
constructor() {
|
|
905
|
+
this.messageService = inject((MessagePeerService));
|
|
906
|
+
/**
|
|
907
|
+
* @inheritdoc
|
|
908
|
+
*/
|
|
909
|
+
this.types = RESIZE_MESSAGE_TYPE;
|
|
910
|
+
registerProducer(this);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* @inheritdoc
|
|
914
|
+
*/
|
|
915
|
+
handleError(message) {
|
|
916
|
+
// eslint-disable-next-line no-console -- error handling placeholder
|
|
917
|
+
console.error('Error in resize service message', message);
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* This method sets up a `ResizeObserver` to observe changes in the document's body height.
|
|
921
|
+
* When the height changes, it sends a resize message with the new height, to the connected peers
|
|
922
|
+
*/
|
|
923
|
+
startResizeObserver() {
|
|
924
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
925
|
+
const newHeight = document.body.getBoundingClientRect().height;
|
|
926
|
+
if (!this.actualHeight || newHeight !== this.actualHeight) {
|
|
927
|
+
this.actualHeight = newHeight;
|
|
928
|
+
const messageV10 = {
|
|
929
|
+
type: 'resize',
|
|
930
|
+
version: '1.0',
|
|
931
|
+
height: this.actualHeight
|
|
932
|
+
};
|
|
933
|
+
// TODO: sendBest() is not implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
934
|
+
this.messageService.send(messageV10);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
afterNextRender(() => this.resizeObserver?.observe(document.body));
|
|
938
|
+
}
|
|
939
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
940
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeService, providedIn: 'root' }); }
|
|
941
|
+
}
|
|
942
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ResizeService, decorators: [{
|
|
943
|
+
type: Injectable,
|
|
944
|
+
args: [{
|
|
945
|
+
providedIn: 'root'
|
|
946
|
+
}]
|
|
947
|
+
}], ctorParameters: () => [] });
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* A directive that adjusts the height of an element based on resize messages from a specified channel.
|
|
951
|
+
*/
|
|
952
|
+
class ScalableDirective {
|
|
953
|
+
constructor() {
|
|
954
|
+
/**
|
|
955
|
+
* The connection ID for the element, used as channel id backup
|
|
956
|
+
*/
|
|
957
|
+
this.connect = input(...(ngDevMode ? [undefined, { debugName: "connect" }] : []));
|
|
958
|
+
/**
|
|
959
|
+
* The channel id
|
|
960
|
+
*/
|
|
961
|
+
this.scalable = input(...(ngDevMode ? [undefined, { debugName: "scalable" }] : []));
|
|
962
|
+
this.resizeHandler = inject(ResizeConsumerService);
|
|
963
|
+
/**
|
|
964
|
+
* This signal checks if the current channel requesting the resize matches the channel ID from the resize handler.
|
|
965
|
+
* If they match, it returns the new height information; otherwise, it returns undefined.
|
|
966
|
+
*/
|
|
967
|
+
this.newHeightFromChannel = computed(() => {
|
|
968
|
+
const channelAskingResize = this.scalable() || this.connect();
|
|
969
|
+
const newHeightFromChannel = this.resizeHandler.newHeightFromChannel();
|
|
970
|
+
if (channelAskingResize && newHeightFromChannel?.channelId === channelAskingResize) {
|
|
971
|
+
return newHeightFromChannel;
|
|
972
|
+
}
|
|
763
973
|
return undefined;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
for (const [key, value] of queryParamsTopWindow) {
|
|
774
|
-
if (options?.overrideQueryParams || !queryParamsModule.has(key)) {
|
|
775
|
-
queryParamsModule.set(key, value);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
974
|
+
}, ...(ngDevMode ? [{ debugName: "newHeightFromChannel" }] : []));
|
|
975
|
+
const elem = inject(ElementRef);
|
|
976
|
+
const renderer = inject(Renderer2);
|
|
977
|
+
this.resizeHandler.start();
|
|
978
|
+
/** When a new height value is received set the height of the host element (in pixels) */
|
|
979
|
+
effect(() => {
|
|
980
|
+
const newHeightFromChannel = this.newHeightFromChannel();
|
|
981
|
+
if (newHeightFromChannel) {
|
|
982
|
+
renderer.setStyle(elem.nativeElement, 'height', `${newHeightFromChannel.height}px`);
|
|
778
983
|
}
|
|
779
|
-
|
|
780
|
-
moduleUrl.pathname += topWindowUrl.pathname.split(`/${this.activeRoute.routeConfig?.path}`).pop() || '';
|
|
781
|
-
moduleUrl.pathname = moduleUrl.pathname.replace(/\/{2,}/g, '/');
|
|
782
|
-
const moduleUrlStringyfied = moduleUrl.toString();
|
|
783
|
-
return typeof url === 'string' ? moduleUrlStringyfied : this.domSanitizer.bypassSecurityTrustResourceUrl(moduleUrlStringyfied);
|
|
784
|
-
}
|
|
984
|
+
});
|
|
785
985
|
}
|
|
786
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
787
|
-
/** @nocollapse */ static { this.ɵ
|
|
986
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ScalableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
987
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.15", type: ScalableDirective, isStandalone: true, selector: "[scalable]", inputs: { connect: { classPropertyName: "connect", publicName: "connect", isSignal: true, isRequired: false, transformFunction: null }, scalable: { classPropertyName: "scalable", publicName: "scalable", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
|
|
788
988
|
}
|
|
789
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
790
|
-
type:
|
|
989
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ScalableDirective, decorators: [{
|
|
990
|
+
type: Directive,
|
|
791
991
|
args: [{
|
|
792
|
-
|
|
992
|
+
selector: '[scalable]',
|
|
993
|
+
standalone: true
|
|
793
994
|
}]
|
|
794
|
-
}] });
|
|
995
|
+
}], ctorParameters: () => [], propDecorators: { connect: [{ type: i0.Input, args: [{ isSignal: true, alias: "connect", required: false }] }], scalable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scalable", required: false }] }] } });
|
|
795
996
|
|
|
796
997
|
/** Default suffix for an url containing a theme css file */
|
|
797
998
|
const THEME_URL_SUFFIX = '-theme.css';
|
|
@@ -851,8 +1052,9 @@ async function applyInitialTheme(options) {
|
|
|
851
1052
|
const theme = searchParams.get(THEME_QUERY_PARAM_NAME);
|
|
852
1053
|
document.adoptedStyleSheets = [];
|
|
853
1054
|
if (theme) {
|
|
854
|
-
const themeRequest = [
|
|
855
|
-
|
|
1055
|
+
const themeRequest = [
|
|
1056
|
+
downloadApplicationThemeCss(theme, options).then((styleToApply) => applyTheme(styleToApply, false))
|
|
1057
|
+
];
|
|
856
1058
|
const hostInfo = getHostInfo();
|
|
857
1059
|
if (hostInfo.hostURL) {
|
|
858
1060
|
const url = new URL(hostInfo.hostURL);
|
|
@@ -871,13 +1073,14 @@ class ThemeProducerService {
|
|
|
871
1073
|
constructor() {
|
|
872
1074
|
this.messageService = inject((MessagePeerService));
|
|
873
1075
|
this.logger = inject(LoggerService);
|
|
1076
|
+
this.window = inject(Window, { optional: true }) || window;
|
|
874
1077
|
/**
|
|
875
1078
|
* The type of messages this service handles ('theme').
|
|
876
1079
|
*/
|
|
877
1080
|
this.types = THEME_MESSAGE_TYPE;
|
|
878
1081
|
registerProducer(this);
|
|
879
1082
|
// get the current theme name from the url (if any) and emit a first value for the current theme
|
|
880
|
-
const parentUrl = new URL(window.location.toString());
|
|
1083
|
+
const parentUrl = new URL(this.window.location.toString());
|
|
881
1084
|
const selectedThemeName = parentUrl.searchParams.get(THEME_QUERY_PARAM_NAME);
|
|
882
1085
|
this.currentThemeSelection = signal(selectedThemeName
|
|
883
1086
|
? {
|
|
@@ -941,10 +1144,10 @@ class ThemeProducerService {
|
|
|
941
1144
|
this.logger.error('Error in theme service message', message);
|
|
942
1145
|
this.revertToPreviousTheme();
|
|
943
1146
|
}
|
|
944
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
945
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1147
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeProducerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1148
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeProducerService, providedIn: 'root' }); }
|
|
946
1149
|
}
|
|
947
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1150
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeProducerService, decorators: [{
|
|
948
1151
|
type: Injectable,
|
|
949
1152
|
args: [{
|
|
950
1153
|
providedIn: 'root'
|
|
@@ -977,10 +1180,10 @@ class ApplyTheme {
|
|
|
977
1180
|
}
|
|
978
1181
|
return undefined;
|
|
979
1182
|
}
|
|
980
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
981
|
-
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.
|
|
1183
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyTheme, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
1184
|
+
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: ApplyTheme, isStandalone: true, name: "applyTheme" }); }
|
|
982
1185
|
}
|
|
983
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1186
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ApplyTheme, decorators: [{
|
|
984
1187
|
type: Pipe,
|
|
985
1188
|
args: [{
|
|
986
1189
|
name: 'applyTheme'
|
|
@@ -1036,132 +1239,435 @@ class ThemeConsumerService {
|
|
|
1036
1239
|
stop() {
|
|
1037
1240
|
this.consumerManagerService.unregister(this);
|
|
1038
1241
|
}
|
|
1039
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
1040
|
-
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.
|
|
1242
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1243
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeConsumerService, providedIn: 'root' }); }
|
|
1041
1244
|
}
|
|
1042
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1245
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ThemeConsumerService, decorators: [{
|
|
1043
1246
|
type: Injectable,
|
|
1044
1247
|
args: [{
|
|
1045
1248
|
providedIn: 'root'
|
|
1046
1249
|
}]
|
|
1047
1250
|
}], ctorParameters: () => [] });
|
|
1048
1251
|
|
|
1049
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* DOM events that indicate user activity
|
|
1254
|
+
*/
|
|
1255
|
+
const ACTIVITY_EVENTS = [
|
|
1256
|
+
'click',
|
|
1257
|
+
'keydown',
|
|
1258
|
+
'scroll',
|
|
1259
|
+
'touchstart',
|
|
1260
|
+
'focus'
|
|
1261
|
+
];
|
|
1262
|
+
/**
|
|
1263
|
+
* Custom activity event type for iframe interactions.
|
|
1264
|
+
* Emitted programmatically when an iframe gains focus, not from a DOM event listener.
|
|
1265
|
+
*/
|
|
1266
|
+
const IFRAME_INTERACTION_EVENT = 'iframeinteraction';
|
|
1267
|
+
/**
|
|
1268
|
+
* Custom activity event type for visibility changes.
|
|
1269
|
+
* Emitted programmatically when the document becomes visible, not from a DOM event listener.
|
|
1270
|
+
*/
|
|
1271
|
+
const VISIBILITY_CHANGE_EVENT = 'visibilitychange';
|
|
1272
|
+
/**
|
|
1273
|
+
* High-frequency events that require throttling to avoid performance issues
|
|
1274
|
+
*/
|
|
1275
|
+
const HIGH_FREQUENCY_EVENTS = [
|
|
1276
|
+
'scroll'
|
|
1277
|
+
];
|
|
1278
|
+
/**
|
|
1279
|
+
* Default configuration values for the ActivityProducerService
|
|
1280
|
+
*/
|
|
1281
|
+
const DEFAULT_ACTIVITY_PRODUCER_CONFIG = {
|
|
1282
|
+
/** Default throttle time in milliseconds between activity messages */
|
|
1283
|
+
throttleMs: 1000,
|
|
1284
|
+
/** Default throttle time in milliseconds for high-frequency events */
|
|
1285
|
+
highFrequencyThrottleMs: 300,
|
|
1286
|
+
/** Whether to track nested iframes by default */
|
|
1287
|
+
trackNestedIframes: false,
|
|
1288
|
+
/** Default interval for iframe activity signals (30 seconds) */
|
|
1289
|
+
nestedIframeActivityEmitIntervalMs: 30_000,
|
|
1290
|
+
/** Default polling interval for detecting iframe focus changes (1 second) */
|
|
1291
|
+
nestedIframePollIntervalMs: 1000
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Service that tracks user activity within nested iframes.
|
|
1296
|
+
*
|
|
1297
|
+
* Polls document.activeElement frequently to detect when an iframe has focus.
|
|
1298
|
+
* When an iframe gains focus, emits immediately and then at the configured interval.
|
|
1299
|
+
* When focus leaves the iframe, it stops emitting.
|
|
1300
|
+
*
|
|
1301
|
+
* This is needed because cross-origin iframes don't fire focus/blur events
|
|
1302
|
+
* that bubble to the parent, and the regular activity tracker can't detect
|
|
1303
|
+
* user interactions inside iframes.
|
|
1304
|
+
*/
|
|
1305
|
+
class IframeActivityTrackerService {
|
|
1306
|
+
constructor() {
|
|
1307
|
+
/**
|
|
1308
|
+
* Bound visibility change handler for cleanup
|
|
1309
|
+
*/
|
|
1310
|
+
this.visibilityChangeHandler = () => this.handleVisibilityChange();
|
|
1311
|
+
}
|
|
1050
1312
|
/**
|
|
1051
|
-
*
|
|
1313
|
+
* Whether the service has been started
|
|
1052
1314
|
*/
|
|
1053
|
-
get
|
|
1054
|
-
return this.
|
|
1315
|
+
get started() {
|
|
1316
|
+
return this.config !== undefined;
|
|
1055
1317
|
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Whether we are currently tracking iframe activity (iframe had focus on last poll)
|
|
1320
|
+
*/
|
|
1321
|
+
get isTrackingIframeActivity() {
|
|
1322
|
+
return this.activityIntervalId !== undefined;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Polls document.activeElement to detect iframe focus changes.
|
|
1326
|
+
* When iframe gains focus: emit immediately and start activity interval.
|
|
1327
|
+
* When iframe loses focus: stop activity interval.
|
|
1328
|
+
*/
|
|
1329
|
+
checkActiveElement() {
|
|
1330
|
+
const activeElement = document.activeElement;
|
|
1331
|
+
const iframeFocused = activeElement instanceof HTMLIFrameElement;
|
|
1332
|
+
if (iframeFocused && !this.isTrackingIframeActivity) {
|
|
1333
|
+
// Iframe just gained focus - emit immediately and start activity interval
|
|
1334
|
+
this.config?.onActivity();
|
|
1335
|
+
this.startActivityInterval();
|
|
1336
|
+
}
|
|
1337
|
+
else if (!iframeFocused && this.isTrackingIframeActivity) {
|
|
1338
|
+
// Focus left the iframe - stop activity interval
|
|
1339
|
+
this.stopActivityInterval();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Handles visibility change events to pause/resume polling when tab visibility changes.
|
|
1344
|
+
*/
|
|
1345
|
+
handleVisibilityChange() {
|
|
1346
|
+
if (document.visibilityState === 'visible') {
|
|
1347
|
+
this.startPolling();
|
|
1348
|
+
}
|
|
1349
|
+
else {
|
|
1350
|
+
this.stopPolling();
|
|
1351
|
+
this.stopActivityInterval();
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Starts polling for active element changes.
|
|
1356
|
+
*/
|
|
1357
|
+
startPolling() {
|
|
1358
|
+
if (this.pollIntervalId) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
this.pollIntervalId = setInterval(() => this.checkActiveElement(), this.config.pollIntervalMs);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Stops polling for active element changes.
|
|
1365
|
+
*/
|
|
1366
|
+
stopPolling() {
|
|
1367
|
+
if (this.pollIntervalId) {
|
|
1368
|
+
clearInterval(this.pollIntervalId);
|
|
1369
|
+
this.pollIntervalId = undefined;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Starts the activity emission interval.
|
|
1374
|
+
*/
|
|
1375
|
+
startActivityInterval() {
|
|
1376
|
+
this.stopActivityInterval();
|
|
1377
|
+
this.activityIntervalId = setInterval(() => {
|
|
1378
|
+
this.config?.onActivity();
|
|
1379
|
+
}, this.config.activityIntervalMs);
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Stops the activity emission interval.
|
|
1383
|
+
*/
|
|
1384
|
+
stopActivityInterval() {
|
|
1385
|
+
if (this.activityIntervalId) {
|
|
1386
|
+
clearInterval(this.activityIntervalId);
|
|
1387
|
+
this.activityIntervalId = undefined;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Starts tracking nested iframes within the document.
|
|
1392
|
+
* @param config Configuration for the tracker
|
|
1393
|
+
*/
|
|
1394
|
+
start(config) {
|
|
1395
|
+
if (this.started) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
this.config = config;
|
|
1399
|
+
// Listen for visibility changes to pause/resume polling
|
|
1400
|
+
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
|
|
1401
|
+
// Only start polling if document is currently visible
|
|
1402
|
+
if (document.visibilityState === 'visible') {
|
|
1403
|
+
this.startPolling();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Stops tracking nested iframes and cleans up resources.
|
|
1408
|
+
*/
|
|
1409
|
+
stop() {
|
|
1410
|
+
if (!this.started) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
|
|
1414
|
+
this.stopPolling();
|
|
1415
|
+
this.stopActivityInterval();
|
|
1416
|
+
this.config = undefined;
|
|
1417
|
+
}
|
|
1418
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: IframeActivityTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1419
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: IframeActivityTrackerService, providedIn: 'root' }); }
|
|
1420
|
+
}
|
|
1421
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: IframeActivityTrackerService, decorators: [{
|
|
1422
|
+
type: Injectable,
|
|
1423
|
+
args: [{
|
|
1424
|
+
providedIn: 'root'
|
|
1425
|
+
}]
|
|
1426
|
+
}] });
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Generic service that tracks user activity and sends messages.
|
|
1430
|
+
* Can be configured for different contexts (cockpit or modules) via start() method parameter.
|
|
1431
|
+
*/
|
|
1432
|
+
class ActivityProducerService {
|
|
1056
1433
|
constructor() {
|
|
1434
|
+
this.messageService = inject(MessagePeerService);
|
|
1435
|
+
this.destroyRef = inject(DestroyRef);
|
|
1436
|
+
this.iframeActivityTracker = inject(IframeActivityTrackerService);
|
|
1437
|
+
this.logger = inject(LoggerService);
|
|
1057
1438
|
/**
|
|
1058
|
-
*
|
|
1439
|
+
* Timestamp of the last sent activity message
|
|
1059
1440
|
*/
|
|
1060
|
-
this.
|
|
1441
|
+
this.lastSentTimestamp = 0;
|
|
1061
1442
|
/**
|
|
1062
|
-
*
|
|
1443
|
+
* Bound event listeners for cleanup
|
|
1063
1444
|
*/
|
|
1064
|
-
this.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
this.
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1445
|
+
this.boundListeners = new Map();
|
|
1446
|
+
/**
|
|
1447
|
+
* Last emission timestamps for throttled high-frequency events
|
|
1448
|
+
*/
|
|
1449
|
+
this.lastEmitTimestamps = new Map();
|
|
1450
|
+
/**
|
|
1451
|
+
* Whether the service has been started
|
|
1452
|
+
*/
|
|
1453
|
+
this.started = false;
|
|
1454
|
+
/**
|
|
1455
|
+
* Signal that emits local activity information.
|
|
1456
|
+
* This allows consumers to react to activity detected by this producer.
|
|
1457
|
+
*/
|
|
1458
|
+
this.localActivityWritable = signal(undefined, ...(ngDevMode ? [{ debugName: "localActivityWritable" }] : []));
|
|
1459
|
+
/**
|
|
1460
|
+
* Read-only signal containing the latest local activity info.
|
|
1461
|
+
* Use this signal to react to locally detected activity.
|
|
1462
|
+
*/
|
|
1463
|
+
this.localActivity = this.localActivityWritable.asReadonly();
|
|
1464
|
+
/**
|
|
1465
|
+
* @inheritdoc
|
|
1466
|
+
*/
|
|
1467
|
+
this.types = USER_ACTIVITY_MESSAGE_TYPE;
|
|
1468
|
+
registerProducer(this);
|
|
1469
|
+
this.destroyRef.onDestroy(() => this.stop());
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Handles high-frequency events by applying a per-eventType throttle before calling onActivity.
|
|
1473
|
+
*
|
|
1474
|
+
* Difference with onActivity:
|
|
1475
|
+
* - onActivityThrottled limits how often a given high-frequency event type (e.g. scroll) is processed
|
|
1476
|
+
* (based on highFrequencyThrottleMs and lastEmitTimestamps)
|
|
1477
|
+
* - onActivity updates the local activity signal and applies the global message throttle
|
|
1478
|
+
* (based on global throttleMs and lastSentTimestamp)
|
|
1479
|
+
* @param eventType The type of activity event that occurred
|
|
1480
|
+
* @param configObject
|
|
1481
|
+
*/
|
|
1482
|
+
onActivityThrottled(eventType, configObject) {
|
|
1483
|
+
const now = Date.now();
|
|
1484
|
+
const lastEmit = this.lastEmitTimestamps.get(eventType) ?? 0;
|
|
1485
|
+
const throttleMs = configObject.highFrequencyThrottleMs;
|
|
1486
|
+
if (now - lastEmit >= throttleMs) {
|
|
1487
|
+
this.lastEmitTimestamps.set(eventType, now);
|
|
1488
|
+
this.onActivity(eventType, configObject);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Handles activity by sending a throttled message and emitting to local signal.
|
|
1493
|
+
* @param eventType The type of activity event that occurred
|
|
1494
|
+
* @param configObject
|
|
1495
|
+
*/
|
|
1496
|
+
onActivity(eventType, configObject) {
|
|
1497
|
+
const now = Date.now();
|
|
1498
|
+
// Always emit local activity signal (not throttled for local detection)
|
|
1499
|
+
this.localActivityWritable.set({
|
|
1500
|
+
channelId: 'local',
|
|
1501
|
+
eventType,
|
|
1502
|
+
timestamp: now
|
|
1503
|
+
});
|
|
1504
|
+
// Send message with throttling
|
|
1505
|
+
if (now - this.lastSentTimestamp >= configObject.throttleMs) {
|
|
1506
|
+
this.lastSentTimestamp = now;
|
|
1507
|
+
this.sendActivityMessage(eventType, now);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Sends an activity message.
|
|
1512
|
+
* @param eventType The type of activity event
|
|
1513
|
+
* @param timestamp The timestamp of the event
|
|
1514
|
+
*/
|
|
1515
|
+
sendActivityMessage(eventType, timestamp) {
|
|
1516
|
+
const message = {
|
|
1517
|
+
type: USER_ACTIVITY_MESSAGE_TYPE,
|
|
1518
|
+
version: '1.0',
|
|
1519
|
+
eventType,
|
|
1520
|
+
timestamp
|
|
1521
|
+
};
|
|
1522
|
+
const registeredPeersIdsForUserActivity = [...this.messageService.knownPeers.entries()]
|
|
1523
|
+
.filter(([peerId]) => peerId !== this.messageService.id)
|
|
1524
|
+
.filter(([, messages]) => messages.some((msg) => msg.type === USER_ACTIVITY_MESSAGE_TYPE))
|
|
1525
|
+
.map((peer) => peer[0]);
|
|
1526
|
+
// send messages to the peers waiting for user activity
|
|
1527
|
+
// avoids sending the message to modules which are not using it
|
|
1528
|
+
if (registeredPeersIdsForUserActivity.length > 0) {
|
|
1529
|
+
this.messageService.send(message, { to: registeredPeersIdsForUserActivity });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* @inheritdoc
|
|
1534
|
+
*/
|
|
1535
|
+
handleError(message) {
|
|
1536
|
+
this.logger.error('Error in user activity service message', message);
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Starts observing user activity events.
|
|
1540
|
+
* When activity is detected, it sends a throttled message.
|
|
1541
|
+
* Event listeners are attached after the next render to ensure DOM is ready.
|
|
1542
|
+
* @param config Configuration for the activity producer
|
|
1543
|
+
*/
|
|
1544
|
+
start(config) {
|
|
1545
|
+
if (this.started) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
this.started = true;
|
|
1549
|
+
const configObject = { ...DEFAULT_ACTIVITY_PRODUCER_CONFIG, ...config };
|
|
1550
|
+
// Always use afterNextRender to ensure DOM is ready in all contexts
|
|
1551
|
+
afterNextRender(() => {
|
|
1552
|
+
ACTIVITY_EVENTS.forEach((eventType) => {
|
|
1553
|
+
const isHighFrequency = HIGH_FREQUENCY_EVENTS.includes(eventType);
|
|
1554
|
+
const listener = (event) => {
|
|
1555
|
+
// do nothing if the event is a key kept pressed
|
|
1556
|
+
if (eventType === 'keydown' && event instanceof KeyboardEvent && event.repeat) {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
// Apply filter if provided
|
|
1560
|
+
if (configObject.shouldBroadcast?.(event) === false) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (isHighFrequency) {
|
|
1564
|
+
this.onActivityThrottled(eventType, configObject);
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
this.onActivity(eventType, configObject);
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
this.boundListeners.set(eventType, listener);
|
|
1571
|
+
document.addEventListener(eventType, listener, { passive: true, capture: true });
|
|
1572
|
+
});
|
|
1573
|
+
// Also listen for visibility changes
|
|
1574
|
+
const visibilityListener = () => {
|
|
1575
|
+
if (document.visibilityState === 'visible') {
|
|
1576
|
+
this.onActivity(VISIBILITY_CHANGE_EVENT, configObject);
|
|
1087
1577
|
}
|
|
1578
|
+
};
|
|
1579
|
+
this.boundListeners.set(VISIBILITY_CHANGE_EVENT, visibilityListener);
|
|
1580
|
+
document.addEventListener(VISIBILITY_CHANGE_EVENT, visibilityListener, { passive: true, capture: true });
|
|
1581
|
+
// Set up nested iframe tracking if enabled
|
|
1582
|
+
if (configObject.trackNestedIframes) {
|
|
1583
|
+
this.iframeActivityTracker.start({
|
|
1584
|
+
pollIntervalMs: configObject.nestedIframePollIntervalMs,
|
|
1585
|
+
activityIntervalMs: configObject.nestedIframeActivityEmitIntervalMs,
|
|
1586
|
+
onActivity: () => this.onActivity(IFRAME_INTERACTION_EVENT, configObject)
|
|
1587
|
+
});
|
|
1088
1588
|
}
|
|
1089
|
-
// stop listening for handshakes and disconnect previous connection when:
|
|
1090
|
-
// - origin/connection ID change
|
|
1091
|
-
// - the directive is destroyed
|
|
1092
|
-
onCleanup(() => {
|
|
1093
|
-
stopHandshakeListening();
|
|
1094
|
-
this.messageService.disconnect();
|
|
1095
|
-
});
|
|
1096
1589
|
});
|
|
1097
1590
|
}
|
|
1098
|
-
/**
|
|
1099
|
-
|
|
1591
|
+
/**
|
|
1592
|
+
* Stops observing user activity events.
|
|
1593
|
+
*/
|
|
1594
|
+
stop() {
|
|
1595
|
+
this.boundListeners.forEach((listener, eventType) => {
|
|
1596
|
+
document.removeEventListener(eventType, listener, { capture: true });
|
|
1597
|
+
});
|
|
1598
|
+
this.boundListeners.clear();
|
|
1599
|
+
this.lastEmitTimestamps.clear();
|
|
1600
|
+
this.iframeActivityTracker.stop();
|
|
1601
|
+
}
|
|
1602
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityProducerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1603
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityProducerService, providedIn: 'root' }); }
|
|
1100
1604
|
}
|
|
1101
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
1102
|
-
type:
|
|
1605
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityProducerService, decorators: [{
|
|
1606
|
+
type: Injectable,
|
|
1103
1607
|
args: [{
|
|
1104
|
-
|
|
1105
|
-
standalone: true
|
|
1608
|
+
providedIn: 'root'
|
|
1106
1609
|
}]
|
|
1107
|
-
}], ctorParameters: () => []
|
|
1108
|
-
type: HostBinding,
|
|
1109
|
-
args: ['src']
|
|
1110
|
-
}] } });
|
|
1111
|
-
|
|
1112
|
-
/**
|
|
1113
|
-
* No operation function used to override the history API methods.
|
|
1114
|
-
*/
|
|
1115
|
-
function noop() { }
|
|
1116
|
-
/**
|
|
1117
|
-
* If an iframe is not sandboxed or is of the same origin, navigation inside it will mess up the main window history.
|
|
1118
|
-
* This disables writing to and overriding history API from inside the iframe to prevent this.
|
|
1119
|
-
* Theoretically, this might break applications that rely on reading history API from inside the iframe.
|
|
1120
|
-
*
|
|
1121
|
-
* This should also allow having `CustomPathLocationStrategy` in the iframe if necessary.
|
|
1122
|
-
*/
|
|
1123
|
-
function provideDisableHistoryWrites() {
|
|
1124
|
-
return provideAppInitializer(() => {
|
|
1125
|
-
Object.defineProperty(history, 'pushState', { value: noop, writable: false, configurable: false });
|
|
1126
|
-
Object.defineProperty(history, 'replaceState', { value: noop, writable: false, configurable: false });
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1610
|
+
}], ctorParameters: () => [] });
|
|
1129
1611
|
|
|
1130
1612
|
/**
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1613
|
+
* Generic service that consumes user activity messages.
|
|
1614
|
+
* Can be used in both shell (to receive from modules) and modules (to receive from shell).
|
|
1133
1615
|
*/
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1616
|
+
class ActivityConsumerService {
|
|
1617
|
+
constructor() {
|
|
1618
|
+
this.consumerManagerService = inject(ConsumerManagerService);
|
|
1619
|
+
/**
|
|
1620
|
+
* Signal containing the latest activity info
|
|
1621
|
+
*/
|
|
1622
|
+
this.latestReceivedActivityWritable = signal(undefined, ...(ngDevMode ? [{ debugName: "latestReceivedActivityWritable" }] : []));
|
|
1623
|
+
/**
|
|
1624
|
+
* Read-only signal containing the latest activity info received from other peers via the message protocol.
|
|
1625
|
+
* Access the timestamp via latestReceivedActivity()?.timestamp
|
|
1626
|
+
*/
|
|
1627
|
+
this.latestReceivedActivity = this.latestReceivedActivityWritable.asReadonly();
|
|
1628
|
+
/**
|
|
1629
|
+
* @inheritdoc
|
|
1630
|
+
*/
|
|
1631
|
+
this.type = USER_ACTIVITY_MESSAGE_TYPE;
|
|
1632
|
+
/**
|
|
1633
|
+
* @inheritdoc
|
|
1634
|
+
*/
|
|
1635
|
+
this.supportedVersions = {
|
|
1636
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention -- Version keys follow message versioning convention
|
|
1637
|
+
'1.0': (message) => {
|
|
1638
|
+
this.latestReceivedActivityWritable.set({
|
|
1639
|
+
channelId: message.from || 'unknown',
|
|
1640
|
+
eventType: message.payload.eventType,
|
|
1641
|
+
timestamp: message.payload.timestamp
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1140
1645
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
provide: MessagePeerService, useClass: MessagePeerService, deps: [MESSAGE_PEER_CONFIG]
|
|
1156
|
-
},
|
|
1157
|
-
// deactivate history writes to avoid embedded app writing to the host history
|
|
1158
|
-
...isEmbedded() ? [provideDisableHistoryWrites()] : []
|
|
1159
|
-
]);
|
|
1646
|
+
/**
|
|
1647
|
+
* Starts the activity consumer service by registering it with the consumer manager.
|
|
1648
|
+
*/
|
|
1649
|
+
start() {
|
|
1650
|
+
this.consumerManagerService.register(this);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Stops the activity consumer service by unregistering it from the consumer manager.
|
|
1654
|
+
*/
|
|
1655
|
+
stop() {
|
|
1656
|
+
this.consumerManagerService.unregister(this);
|
|
1657
|
+
}
|
|
1658
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1659
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityConsumerService, providedIn: 'root' }); }
|
|
1160
1660
|
}
|
|
1661
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ActivityConsumerService, decorators: [{
|
|
1662
|
+
type: Injectable,
|
|
1663
|
+
args: [{
|
|
1664
|
+
providedIn: 'root'
|
|
1665
|
+
}]
|
|
1666
|
+
}] });
|
|
1161
1667
|
|
|
1162
1668
|
/**
|
|
1163
1669
|
* Generated bundle index. Do not edit.
|
|
1164
1670
|
*/
|
|
1165
1671
|
|
|
1166
|
-
export { ApplyTheme, ConnectDirective, ConsumerManagerService, ERROR_MESSAGE_TYPE, HostInfoPipe, KNOWN_MESSAGES, MFE_HOST_APPLICATION_ID_PARAM, MFE_HOST_URL_PARAM, MFE_MODULE_APPLICATION_ID_PARAM, NavigationConsumerService, ProducerManagerService, ResizeConsumerService, ResizeService, RestoreRoute, RouteMemorizeDirective, RouteMemorizeService, RoutingService, ScalableDirective, THEME_QUERY_PARAM_NAME, THEME_URL_SUFFIX, ThemeConsumerService, ThemeProducerService, applyInitialTheme, applyTheme, downloadApplicationThemeCss, getAvailableConsumers, getDefaultClientEndpointStartOptions, getHostInfo, getStyle, hostQueryParams, isEmbedded, isErrorMessage, persistHostInfo, provideConnection, registerConsumer, registerProducer, sendError };
|
|
1672
|
+
export { ACTIVITY_EVENTS, ActivityConsumerService, ActivityProducerService, ApplyTheme, ConnectDirective, ConsumerManagerService, DEFAULT_ACTIVITY_PRODUCER_CONFIG, ERROR_MESSAGE_TYPE, HIGH_FREQUENCY_EVENTS, HistoryConsumerService, HostInfoPipe, IFRAME_INTERACTION_EVENT, IframeActivityTrackerService, KNOWN_MESSAGES, MFE_HOST_APPLICATION_ID_PARAM, MFE_HOST_URL_PARAM, MFE_MODULE_APPLICATION_ID_PARAM, NavigationConsumerService, ProducerManagerService, ResizeConsumerService, ResizeService, RestoreRoute, RouteMemorizeDirective, RouteMemorizeService, RoutingService, ScalableDirective, THEME_QUERY_PARAM_NAME, THEME_URL_SUFFIX, ThemeConsumerService, ThemeProducerService, VISIBILITY_CHANGE_EVENT, applyInitialTheme, applyTheme, downloadApplicationThemeCss, getAvailableConsumers, getDefaultClientEndpointStartOptions, getHostInfo, getStyle, hostQueryParams, isEmbedded, isErrorMessage, isUserActivityMessage, persistHostInfo, provideConnection, provideHistoryOverrides, registerConsumer, registerProducer, sendError };
|
|
1167
1673
|
//# sourceMappingURL=ama-mfe-ng-utils.mjs.map
|