@ama-mfe/ng-utils 12.1.0-prerelease.73
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/LICENSE +26 -0
- package/README.md +3 -0
- package/ama-mfe-ng-utils.d.ts.map +1 -0
- package/collection.json +11 -0
- package/connect/connect.directive.d.ts +24 -0
- package/connect/connect.directive.d.ts.map +1 -0
- package/connect/connect.providers.d.ts +7 -0
- package/connect/connect.providers.d.ts.map +1 -0
- package/connect/connect.resources.d.ts +3 -0
- package/connect/connect.resources.d.ts.map +1 -0
- package/connect/index.d.ts +4 -0
- package/connect/index.d.ts.map +1 -0
- package/fesm2022/ama-mfe-ng-utils.mjs +1016 -0
- package/fesm2022/ama-mfe-ng-utils.mjs.map +1 -0
- package/index.d.ts +6 -0
- package/managers/consumer.manager.service.d.ts +35 -0
- package/managers/consumer.manager.service.d.ts.map +1 -0
- package/managers/index.d.ts +4 -0
- package/managers/index.d.ts.map +1 -0
- package/managers/interfaces.d.ts +49 -0
- package/managers/interfaces.d.ts.map +1 -0
- package/managers/producer.manager.service.d.ts +29 -0
- package/managers/producer.manager.service.d.ts.map +1 -0
- package/messages/available.sender.d.ts +15 -0
- package/messages/available.sender.d.ts.map +1 -0
- package/messages/error/base.d.ts +18 -0
- package/messages/error/base.d.ts.map +1 -0
- package/messages/error/error.versions.d.ts +10 -0
- package/messages/error/error.versions.d.ts.map +1 -0
- package/messages/error/index.d.ts +6 -0
- package/messages/error/index.d.ts.map +1 -0
- package/messages/error.sender.d.ts +16 -0
- package/messages/error.sender.d.ts.map +1 -0
- package/messages/index.d.ts +4 -0
- package/messages/index.d.ts.map +1 -0
- package/navigation/index.d.ts +5 -0
- package/navigation/index.d.ts.map +1 -0
- package/navigation/navigation.consumer.service.d.ts +61 -0
- package/navigation/navigation.consumer.service.d.ts.map +1 -0
- package/navigation/navigation.producer.service.d.ts +43 -0
- package/navigation/navigation.producer.service.d.ts.map +1 -0
- package/navigation/restore-route.pipe.d.ts +44 -0
- package/navigation/restore-route.pipe.d.ts.map +1 -0
- package/navigation/route-memorize/index.d.ts +3 -0
- package/navigation/route-memorize/index.d.ts.map +1 -0
- package/navigation/route-memorize/route-memorize.directive.d.ts +31 -0
- package/navigation/route-memorize/route-memorize.directive.d.ts.map +1 -0
- package/navigation/route-memorize/route-memorize.service.d.ts +27 -0
- package/navigation/route-memorize/route-memorize.service.d.ts.map +1 -0
- package/package.json +136 -0
- package/public_api.d.ts +8 -0
- package/public_api.d.ts.map +1 -0
- package/resize/index.d.ts +4 -0
- package/resize/index.d.ts.map +1 -0
- package/resize/resize.consumer.service.d.ts +44 -0
- package/resize/resize.consumer.service.d.ts.map +1 -0
- package/resize/resize.directive.d.ts +24 -0
- package/resize/resize.directive.d.ts.map +1 -0
- package/resize/resize.producer.service.d.ts +30 -0
- package/resize/resize.producer.service.d.ts.map +1 -0
- package/schematics/ng-add/index.d.ts +8 -0
- package/schematics/ng-add/index.d.ts.map +1 -0
- package/schematics/ng-add/index.js +60 -0
- package/schematics/ng-add/schema.d.ts +10 -0
- package/schematics/ng-add/schema.d.ts.map +1 -0
- package/schematics/ng-add/schema.js +3 -0
- package/schematics/ng-add/schema.json +27 -0
- package/schematics/package.json +3 -0
- package/theme/apply-theme.pipe.d.ts +21 -0
- package/theme/apply-theme.pipe.d.ts.map +1 -0
- package/theme/index.d.ts +5 -0
- package/theme/index.d.ts.map +1 -0
- package/theme/theme.consumer.service.d.ts +37 -0
- package/theme/theme.consumer.service.d.ts.map +1 -0
- package/theme/theme.helpers.d.ts +24 -0
- package/theme/theme.helpers.d.ts.map +1 -0
- package/theme/theme.producer.service.d.ts +36 -0
- package/theme/theme.producer.service.d.ts.map +1 -0
- package/utils.d.ts +14 -0
- package/utils.d.ts.map +1 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import { isServiceMessage } from '@amadeus-it-group/microfrontends';
|
|
2
|
+
import { MessagePeerService, MESSAGE_PEER_CONFIG, MESSAGE_PEER_CONNECT_OPTIONS } from '@amadeus-it-group/microfrontends-angular';
|
|
3
|
+
export { MessagePeerService as ConnectionService } from '@amadeus-it-group/microfrontends-angular';
|
|
4
|
+
import * as i0 from '@angular/core';
|
|
5
|
+
import { Injectable, inject, signal, effect, DestroyRef, afterNextRender, input, computed, ElementRef, Renderer2, Directive, untracked, SecurityContext, Pipe, HostBinding, makeEnvironmentProviders } from '@angular/core';
|
|
6
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { RESIZE_MESSAGE_TYPE, NAVIGATION_MESSAGE_TYPE, THEME_MESSAGE_TYPE } from '@ama-mfe/messages';
|
|
8
|
+
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
|
9
|
+
import { Subject, filter, map } from 'rxjs';
|
|
10
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Gets the available consumers and formats them into a {@see DeclareMessages} object.
|
|
14
|
+
* @param consumers - The list of registered message consumers.
|
|
15
|
+
* @returns The formatted DeclareMessages object.
|
|
16
|
+
*/
|
|
17
|
+
const getAvailableConsumers = (consumers) => {
|
|
18
|
+
return {
|
|
19
|
+
type: 'declare_messages',
|
|
20
|
+
version: '1.0',
|
|
21
|
+
messages: consumers.flatMap(({ type, supportedVersions }) => Object.keys(supportedVersions).map((version) => ({ type, version })))
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper function to send an error message by the given endpoint (peer)
|
|
27
|
+
* @param peer The endpoint sending the message
|
|
28
|
+
* @param content the content of the error message to be sent
|
|
29
|
+
*/
|
|
30
|
+
const sendError = (peer, content) => {
|
|
31
|
+
return peer.send({
|
|
32
|
+
type: 'error',
|
|
33
|
+
version: '1.0',
|
|
34
|
+
...content
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Check if the given message is of type error and the error reson is present too
|
|
39
|
+
* @param message the message to be checked
|
|
40
|
+
*/
|
|
41
|
+
// eslint-disable-next-line @stylistic/max-len -- constant definition
|
|
42
|
+
const isErrorMessage = (message) => (message && typeof message === 'object' && message.type === 'error' && !!message.reason);
|
|
43
|
+
|
|
44
|
+
class ProducerManagerService {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.registeredProducers = new Set();
|
|
47
|
+
}
|
|
48
|
+
/** Get the list of registered producers of messages. The list will contain unique elements */
|
|
49
|
+
get producers() {
|
|
50
|
+
return [...this.registeredProducers];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register a producer of a message
|
|
54
|
+
* @param producer The instance of the message producer
|
|
55
|
+
*/
|
|
56
|
+
register(producer) {
|
|
57
|
+
this.registeredProducers.add((producer));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Unregister a producer of a message
|
|
61
|
+
* @param producer The instance of the message producer
|
|
62
|
+
*/
|
|
63
|
+
unregister(producer) {
|
|
64
|
+
this.registeredProducers.delete((producer));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Handles the received error message for the given message type by invoking the appropriate producer handlers.
|
|
68
|
+
* @template T - The type of the message, extending from Message.
|
|
69
|
+
* @param message - The error message to handle.
|
|
70
|
+
* @returns - A promise that resolves to true if the error was handled by at least one handler, false otherwise.
|
|
71
|
+
*/
|
|
72
|
+
async dispatchError(message) {
|
|
73
|
+
const handlers = this.producers
|
|
74
|
+
.filter(({ types }) => (Array.isArray(types) ? types : [types]).includes(message.source.type));
|
|
75
|
+
const handlersPresent = handlers.length > 0;
|
|
76
|
+
if (handlersPresent) {
|
|
77
|
+
await Promise.all(handlers.map((handler) => handler.handleError(message)));
|
|
78
|
+
}
|
|
79
|
+
return handlersPresent;
|
|
80
|
+
}
|
|
81
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ProducerManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
82
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ProducerManagerService, providedIn: 'root' }); }
|
|
83
|
+
}
|
|
84
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ProducerManagerService, decorators: [{
|
|
85
|
+
type: Injectable,
|
|
86
|
+
args: [{
|
|
87
|
+
providedIn: 'root'
|
|
88
|
+
}]
|
|
89
|
+
}] });
|
|
90
|
+
|
|
91
|
+
class ConsumerManagerService {
|
|
92
|
+
constructor() {
|
|
93
|
+
this.messageService = inject(MessagePeerService);
|
|
94
|
+
this.producerManagerService = inject(ProducerManagerService);
|
|
95
|
+
this.registeredConsumers = signal([]);
|
|
96
|
+
/** The list of registered consumers */
|
|
97
|
+
this.consumers = this.registeredConsumers.asReadonly();
|
|
98
|
+
this.messageService.messages$.pipe(takeUntilDestroyed()).subscribe((message) => this.consumeMessage(message));
|
|
99
|
+
// Each time a consumer is registered/unregistered update the list of registered messages
|
|
100
|
+
effect(() => {
|
|
101
|
+
const declareMessages = getAvailableConsumers(this.consumers());
|
|
102
|
+
// registering consumed messages locally for validation
|
|
103
|
+
for (const message of declareMessages.messages) {
|
|
104
|
+
this.messageService.registerMessage(message);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Consume a received message
|
|
110
|
+
* @param message the received message body
|
|
111
|
+
*/
|
|
112
|
+
async consumeMessage(message) {
|
|
113
|
+
if (isErrorMessage(message.payload)) {
|
|
114
|
+
const isHandled = await this.producerManagerService.dispatchError(message.payload);
|
|
115
|
+
if (!isHandled) {
|
|
116
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
117
|
+
// eslint-disable-next-line no-console -- placeholder for implementation with logger
|
|
118
|
+
console.warn('Error message not handled', message);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
return this.consumeAdditionalMessage(message);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Call the registered message callback(s) to consume the given message
|
|
126
|
+
* Handle error messages of internal communication protocol messages
|
|
127
|
+
* @param message message to consume
|
|
128
|
+
*/
|
|
129
|
+
async consumeAdditionalMessage(message) {
|
|
130
|
+
if (!message.payload) {
|
|
131
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
132
|
+
// eslint-disable-next-line no-console -- placeholder for implementation with logger
|
|
133
|
+
console.warn('Cannot consume a messages with undefined payload.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// not interested in service messages like 'connect' or 'disconnect'
|
|
137
|
+
if (isServiceMessage(message.payload)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const consumers = this.consumers();
|
|
141
|
+
const typeMatchingConsumers = consumers
|
|
142
|
+
.filter((consumer) => consumer.type === message.payload.type);
|
|
143
|
+
if (typeMatchingConsumers.length === 0) {
|
|
144
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
145
|
+
// eslint-disable-next-line no-console -- placeholder for implementation with logger
|
|
146
|
+
console.warn(`No consumer found for message type: ${message.payload.type}`);
|
|
147
|
+
return sendError(this.messageService, { reason: 'unknown_type', source: message.payload });
|
|
148
|
+
}
|
|
149
|
+
const versionMatchingConsumers = typeMatchingConsumers
|
|
150
|
+
.filter((consumer) => consumer.supportedVersions[message.payload.version])
|
|
151
|
+
.flat();
|
|
152
|
+
if (versionMatchingConsumers.length === 0) {
|
|
153
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
154
|
+
// eslint-disable-next-line no-console -- placeholder for implementation with logger
|
|
155
|
+
console.warn(`No consumer found for message version: ${message.payload.version}`);
|
|
156
|
+
return sendError(this.messageService, { reason: 'version_mismatch', source: message.payload });
|
|
157
|
+
}
|
|
158
|
+
await Promise.all(versionMatchingConsumers
|
|
159
|
+
.map(async (consumer) => {
|
|
160
|
+
try {
|
|
161
|
+
await consumer.supportedVersions[message.payload.version](message);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
165
|
+
// eslint-disable-next-line no-console -- notify directly in the console the error at message consume
|
|
166
|
+
console.error('Error while consuming message', error);
|
|
167
|
+
sendError(this.messageService, { reason: 'internal_error', source: message.payload });
|
|
168
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Register a message consumer
|
|
173
|
+
* @param consumer an instance of message consumer
|
|
174
|
+
*/
|
|
175
|
+
register(consumer) {
|
|
176
|
+
this.registeredConsumers.update((consumers) => {
|
|
177
|
+
return [...new Set(consumers).add(consumer)];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Unregister a message consumer
|
|
182
|
+
* @param consumer an instance of message consumer
|
|
183
|
+
*/
|
|
184
|
+
unregister(consumer) {
|
|
185
|
+
this.registeredConsumers.update((consumers) => {
|
|
186
|
+
return consumers.filter((c) => c !== consumer);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ConsumerManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
190
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ConsumerManagerService, providedIn: 'root' }); }
|
|
191
|
+
}
|
|
192
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ConsumerManagerService, decorators: [{
|
|
193
|
+
type: Injectable,
|
|
194
|
+
args: [{
|
|
195
|
+
providedIn: 'root'
|
|
196
|
+
}]
|
|
197
|
+
}], ctorParameters: () => [] });
|
|
198
|
+
|
|
199
|
+
/** the error message type */
|
|
200
|
+
const ERROR_MESSAGE_TYPE = 'error';
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* This service listens for resize messages and updates the height of elements based on the received messages.
|
|
204
|
+
*/
|
|
205
|
+
class ResizeConsumerService {
|
|
206
|
+
constructor() {
|
|
207
|
+
this.newHeight = signal(undefined);
|
|
208
|
+
/**
|
|
209
|
+
* A readonly signal that provides the new height information from the channel.
|
|
210
|
+
*/
|
|
211
|
+
this.newHeightFromChannel = this.newHeight.asReadonly();
|
|
212
|
+
/**
|
|
213
|
+
* The type of messages this service handles ('resize').
|
|
214
|
+
*/
|
|
215
|
+
this.type = RESIZE_MESSAGE_TYPE;
|
|
216
|
+
/**
|
|
217
|
+
* The supported versions of resize messages and their handlers.
|
|
218
|
+
*/
|
|
219
|
+
this.supportedVersions = {
|
|
220
|
+
/**
|
|
221
|
+
* Use the message paylod to compute a new height and emit it via the public signal
|
|
222
|
+
* @param message message to consume
|
|
223
|
+
*/
|
|
224
|
+
'1.0': (message) => this.newHeight.set({ height: message.payload.height, channelId: message.from })
|
|
225
|
+
};
|
|
226
|
+
this.consumerManagerService = inject(ConsumerManagerService);
|
|
227
|
+
this.start();
|
|
228
|
+
inject(DestroyRef).onDestroy(() => this.stop());
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Starts the resize handler service by registering it into the consumer manager service.
|
|
232
|
+
*/
|
|
233
|
+
start() {
|
|
234
|
+
this.consumerManagerService.register(this);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Stops the resize handler service by unregistering it from the consumer manager service.
|
|
238
|
+
*/
|
|
239
|
+
stop() {
|
|
240
|
+
this.consumerManagerService.unregister(this);
|
|
241
|
+
}
|
|
242
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
243
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeConsumerService, providedIn: 'root' }); }
|
|
244
|
+
}
|
|
245
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeConsumerService, decorators: [{
|
|
246
|
+
type: Injectable,
|
|
247
|
+
args: [{
|
|
248
|
+
providedIn: 'root'
|
|
249
|
+
}]
|
|
250
|
+
}], ctorParameters: () => [] });
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* This service observe changes in the document's body height.
|
|
254
|
+
* When the height changes, it sends a resize message with the new height, to the connected peers
|
|
255
|
+
*/
|
|
256
|
+
class ResizeService {
|
|
257
|
+
constructor() {
|
|
258
|
+
this.messageService = inject((MessagePeerService));
|
|
259
|
+
/**
|
|
260
|
+
* @inheritdoc
|
|
261
|
+
*/
|
|
262
|
+
this.types = RESIZE_MESSAGE_TYPE;
|
|
263
|
+
const producerManagerService = inject(ProducerManagerService);
|
|
264
|
+
producerManagerService.register(this);
|
|
265
|
+
inject(DestroyRef).onDestroy(() => {
|
|
266
|
+
this.resizeObserver?.disconnect();
|
|
267
|
+
producerManagerService.unregister(this);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @inheritdoc
|
|
272
|
+
*/
|
|
273
|
+
handleError(message) {
|
|
274
|
+
// eslint-disable-next-line no-console -- error handling placeholder
|
|
275
|
+
console.error('Error in resize service message', message);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* This method sets up a `ResizeObserver` to observe changes in the document's body height.
|
|
279
|
+
* When the height changes, it sends a resize message with the new height, to the connected peers
|
|
280
|
+
*/
|
|
281
|
+
startResizeObserver() {
|
|
282
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
283
|
+
if (document.body.scrollHeight !== this.actualHeight) {
|
|
284
|
+
this.actualHeight = document.body.scrollHeight;
|
|
285
|
+
const messageV10 = {
|
|
286
|
+
type: 'resize',
|
|
287
|
+
version: '1.0',
|
|
288
|
+
height: document.body.scrollHeight
|
|
289
|
+
};
|
|
290
|
+
// TODO: sendBest() is not implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
291
|
+
this.messageService.send(messageV10);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
afterNextRender(() => this.resizeObserver?.observe(document.body));
|
|
295
|
+
}
|
|
296
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
297
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeService, providedIn: 'root' }); }
|
|
298
|
+
}
|
|
299
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ResizeService, decorators: [{
|
|
300
|
+
type: Injectable,
|
|
301
|
+
args: [{
|
|
302
|
+
providedIn: 'root'
|
|
303
|
+
}]
|
|
304
|
+
}], ctorParameters: () => [] });
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A directive that adjusts the height of an element based on resize messages from a specified channel.
|
|
308
|
+
*/
|
|
309
|
+
class ScalableDirective {
|
|
310
|
+
constructor() {
|
|
311
|
+
/**
|
|
312
|
+
* The connection ID for the element, used as channel id backup
|
|
313
|
+
*/
|
|
314
|
+
this.connect = input();
|
|
315
|
+
/**
|
|
316
|
+
* The channel id
|
|
317
|
+
*/
|
|
318
|
+
this.scalable = input();
|
|
319
|
+
this.resizeHandler = inject(ResizeConsumerService);
|
|
320
|
+
/**
|
|
321
|
+
* This signal checks if the current channel requesting the resize matches the channel ID from the resize handler.
|
|
322
|
+
* If they match, it returns the new height information; otherwise, it returns undefined.
|
|
323
|
+
*/
|
|
324
|
+
this.newHeightFromChannel = computed(() => {
|
|
325
|
+
const channelAskingResize = this.scalable() || this.connect();
|
|
326
|
+
const newHeightFromChannel = this.resizeHandler.newHeightFromChannel();
|
|
327
|
+
if (channelAskingResize && newHeightFromChannel?.channelId === channelAskingResize) {
|
|
328
|
+
return newHeightFromChannel;
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
});
|
|
332
|
+
const elem = inject(ElementRef);
|
|
333
|
+
const renderer = inject(Renderer2);
|
|
334
|
+
this.resizeHandler.start();
|
|
335
|
+
/** When a new height value is received set the height of the host element (in pixels) */
|
|
336
|
+
effect(() => {
|
|
337
|
+
const newHeightFromChannel = this.newHeightFromChannel();
|
|
338
|
+
if (newHeightFromChannel) {
|
|
339
|
+
renderer.setStyle(elem.nativeElement, 'height', `${newHeightFromChannel.height}px`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ScalableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
344
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.7", 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 }); }
|
|
345
|
+
}
|
|
346
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ScalableDirective, decorators: [{
|
|
347
|
+
type: Directive,
|
|
348
|
+
args: [{
|
|
349
|
+
selector: '[scalable]',
|
|
350
|
+
standalone: true
|
|
351
|
+
}]
|
|
352
|
+
}], ctorParameters: () => [] });
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* A service that handles navigation messages and routing.
|
|
356
|
+
*
|
|
357
|
+
* This service listens for navigation messages and updates the router state accordingly.
|
|
358
|
+
*/
|
|
359
|
+
class NavigationConsumerService {
|
|
360
|
+
constructor() {
|
|
361
|
+
this.router = inject(Router);
|
|
362
|
+
this.activeRoute = inject(ActivatedRoute);
|
|
363
|
+
this.requestedUrl = new Subject();
|
|
364
|
+
/**
|
|
365
|
+
* An observable that emits the requested URL and optional channel ID.
|
|
366
|
+
*/
|
|
367
|
+
this.requestedUrl$ = this.requestedUrl.asObservable();
|
|
368
|
+
/**
|
|
369
|
+
* The type of messages this service handles.
|
|
370
|
+
*/
|
|
371
|
+
this.type = NAVIGATION_MESSAGE_TYPE;
|
|
372
|
+
/**
|
|
373
|
+
* @inheritdoc
|
|
374
|
+
*/
|
|
375
|
+
this.supportedVersions = {
|
|
376
|
+
/**
|
|
377
|
+
* Use the message paylod to compute a new url and emit it via the public subject
|
|
378
|
+
* Additionally navigate to the new url
|
|
379
|
+
* @param message message to consume
|
|
380
|
+
*/
|
|
381
|
+
'1.0': (message) => {
|
|
382
|
+
const channelId = message.from || undefined;
|
|
383
|
+
this.requestedUrl.next({ url: message.payload.url, channelId });
|
|
384
|
+
this.navigate(message.payload.url, channelId);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
this.consumerManagerService = inject(ConsumerManagerService);
|
|
388
|
+
this.start();
|
|
389
|
+
inject(DestroyRef).onDestroy(() => this.stop());
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Parses a URL and returns an object containing the paths and query parameters.
|
|
393
|
+
* @param url - The URL to parse.
|
|
394
|
+
* @returns An object containing the paths and query parameters.
|
|
395
|
+
*/
|
|
396
|
+
parseUrl(url) {
|
|
397
|
+
const urlObject = new URL(window.origin + url);
|
|
398
|
+
const paths = urlObject.pathname.split('/').filter((segment) => !!segment);
|
|
399
|
+
const queryParams = Object.fromEntries(urlObject.searchParams.entries());
|
|
400
|
+
return { paths, queryParams };
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Navigates to the specified URL with optional channel ID.
|
|
404
|
+
* @param url - The URL to navigate to.
|
|
405
|
+
* @param channelId - The optional channel ID for the navigation state. This is the endpoint from where the message is received
|
|
406
|
+
*/
|
|
407
|
+
navigate(url, channelId) {
|
|
408
|
+
const { paths, queryParams } = this.parseUrl(url);
|
|
409
|
+
void this.router.navigate(paths, { relativeTo: this.activeRoute.children.at(-1), queryParams, state: { channelId }, replaceUrl: true });
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* @inheritdoc
|
|
413
|
+
*/
|
|
414
|
+
start() {
|
|
415
|
+
this.consumerManagerService.register(this);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* @inheritdoc
|
|
419
|
+
*/
|
|
420
|
+
stop() {
|
|
421
|
+
this.consumerManagerService.unregister(this);
|
|
422
|
+
}
|
|
423
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: NavigationConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
424
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: NavigationConsumerService, providedIn: 'root' }); }
|
|
425
|
+
}
|
|
426
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: NavigationConsumerService, decorators: [{
|
|
427
|
+
type: Injectable,
|
|
428
|
+
args: [{
|
|
429
|
+
providedIn: 'root'
|
|
430
|
+
}]
|
|
431
|
+
}], ctorParameters: () => [] });
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* This service allows routes to be memorized with an optional lifetime and provides methods to retrieve and manage these routes.
|
|
435
|
+
*/
|
|
436
|
+
class RouteMemorizeService {
|
|
437
|
+
constructor() {
|
|
438
|
+
this.routeTimers = {};
|
|
439
|
+
/** All memorized routes */
|
|
440
|
+
this.routeStack = {};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Memorizes a route for a given channel ID with an optional lifetime.
|
|
444
|
+
* @param channelId - The ID of the channel to memorize the route for.
|
|
445
|
+
* @param url - The URL of the route to memorize.
|
|
446
|
+
* @param liveTime - The optional lifetime of the memorized route in milliseconds. If provided, the route will be removed after this time.
|
|
447
|
+
*/
|
|
448
|
+
memorizeRoute(channelId, url, liveTime) {
|
|
449
|
+
this.routeStack[channelId] = url;
|
|
450
|
+
const timerRef = this.routeTimers[channelId];
|
|
451
|
+
if (timerRef) {
|
|
452
|
+
clearTimeout(timerRef);
|
|
453
|
+
}
|
|
454
|
+
if (liveTime && liveTime > 0) {
|
|
455
|
+
this.routeTimers[channelId] = setTimeout(() => {
|
|
456
|
+
delete this.routeStack[channelId];
|
|
457
|
+
delete this.routeTimers[channelId];
|
|
458
|
+
}, liveTime);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Retrieves the memorized route for a given channel ID.
|
|
463
|
+
* @param channelId - The ID of the channel to retrieve the memorized route for.
|
|
464
|
+
* @returns The memorized route URL or undefined if no route is memorized for the given channel ID.
|
|
465
|
+
*/
|
|
466
|
+
getRoute(channelId) {
|
|
467
|
+
return this.routeStack[channelId];
|
|
468
|
+
}
|
|
469
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RouteMemorizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
470
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RouteMemorizeService, providedIn: 'root' }); }
|
|
471
|
+
}
|
|
472
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RouteMemorizeService, decorators: [{
|
|
473
|
+
type: Injectable,
|
|
474
|
+
args: [{
|
|
475
|
+
providedIn: 'root'
|
|
476
|
+
}]
|
|
477
|
+
}] });
|
|
478
|
+
|
|
479
|
+
class RouteMemorizeDirective {
|
|
480
|
+
constructor() {
|
|
481
|
+
/**
|
|
482
|
+
* Whether to memorize the route.
|
|
483
|
+
* Default is true.
|
|
484
|
+
*/
|
|
485
|
+
this.memorizeRoute = input(true);
|
|
486
|
+
/**
|
|
487
|
+
* The ID used to memorize the route.
|
|
488
|
+
*/
|
|
489
|
+
this.memorizeRouteId = input();
|
|
490
|
+
/**
|
|
491
|
+
* The maximum age for memorizing the route.
|
|
492
|
+
* Default is 0.
|
|
493
|
+
*/
|
|
494
|
+
this.memorizeMaxAge = input(0);
|
|
495
|
+
/**
|
|
496
|
+
* The maximum age for memorizing the route, used as a fallback.
|
|
497
|
+
* Default is 0.
|
|
498
|
+
*/
|
|
499
|
+
this.memorizeRouteMaxAge = input(0);
|
|
500
|
+
/**
|
|
501
|
+
* The connection ID for the iframe where the actual directive is applied.
|
|
502
|
+
*/
|
|
503
|
+
this.connect = input();
|
|
504
|
+
this.maxAge = computed(() => {
|
|
505
|
+
return this.memorizeMaxAge() || this.memorizeRouteMaxAge();
|
|
506
|
+
});
|
|
507
|
+
const memory = inject(RouteMemorizeService);
|
|
508
|
+
const requestedUrlSignal = toSignal(inject(NavigationConsumerService).requestedUrl$);
|
|
509
|
+
/**
|
|
510
|
+
* This effect listens for changes in the `memorizeRoute`, `requestedUrlSignal`, and `memorizeRouteId` or `connect` inputs.
|
|
511
|
+
* If `memorizeRoute` is not false and a requested URL with a matching channel ID is found, it memorizes the route using the route memory service.
|
|
512
|
+
*/
|
|
513
|
+
effect(() => {
|
|
514
|
+
const memorizeRoute = this.memorizeRoute();
|
|
515
|
+
if (memorizeRoute === false) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const requested = requestedUrlSignal();
|
|
519
|
+
const id = this.memorizeRouteId() || this.connect();
|
|
520
|
+
if (requested && id && requested.channelId === id) {
|
|
521
|
+
memory.memorizeRoute(id, requested.url, untracked(this.maxAge));
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RouteMemorizeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
526
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.7", 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 }); }
|
|
527
|
+
}
|
|
528
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RouteMemorizeDirective, decorators: [{
|
|
529
|
+
type: Directive,
|
|
530
|
+
args: [{
|
|
531
|
+
selector: 'iframe[memorizeRoute]',
|
|
532
|
+
standalone: true
|
|
533
|
+
}]
|
|
534
|
+
}], ctorParameters: () => [] });
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A service that handles routing and message production for navigation events.
|
|
538
|
+
*
|
|
539
|
+
* This service listens to Angular router events and sends navigation messages
|
|
540
|
+
* to a message peer service. It also handles errors related to navigation messages.
|
|
541
|
+
*/
|
|
542
|
+
class RoutingService {
|
|
543
|
+
constructor() {
|
|
544
|
+
this.router = inject(Router);
|
|
545
|
+
this.activatedRoute = inject(ActivatedRoute);
|
|
546
|
+
this.messageService = inject((MessagePeerService));
|
|
547
|
+
/**
|
|
548
|
+
* @inheritdoc
|
|
549
|
+
*/
|
|
550
|
+
this.types = NAVIGATION_MESSAGE_TYPE;
|
|
551
|
+
const producerManagerService = inject(ProducerManagerService);
|
|
552
|
+
producerManagerService.register(this);
|
|
553
|
+
inject(DestroyRef).onDestroy(() => {
|
|
554
|
+
producerManagerService.unregister(this);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* @inheritdoc
|
|
559
|
+
*/
|
|
560
|
+
handleError(message) {
|
|
561
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
562
|
+
// eslint-disable-next-line no-console -- placeholder for the implementation with a logger
|
|
563
|
+
console.error('Error in navigation service message', message);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Handles embedded routing by listening to router events and sending navigation messages to the connected endpoints.
|
|
567
|
+
* It can be a parent window or another iframe
|
|
568
|
+
* @note - This method has to be called in an injection context
|
|
569
|
+
* @param options - Optional parameters to control the routing behavior {@see RoutingServiceOptions}.
|
|
570
|
+
*/
|
|
571
|
+
handleEmbeddedRouting(options) {
|
|
572
|
+
const subRouteOnly = options?.subRouteOnly ?? false;
|
|
573
|
+
this.router.events.pipe(takeUntilDestroyed(), filter((event) => event instanceof NavigationEnd), map(({ urlAfterRedirects }) => {
|
|
574
|
+
const channelId = this.router.getCurrentNavigation()?.extras?.state?.channelId;
|
|
575
|
+
const currentRouteRegExp = subRouteOnly && this.activatedRoute.routeConfig?.path && new RegExp('^' + this.activatedRoute.routeConfig.path.replace(/(?=\W)/g, '\\'), 'i');
|
|
576
|
+
return ({ url: currentRouteRegExp ? urlAfterRedirects.replace(currentRouteRegExp, '') : urlAfterRedirects, channelId });
|
|
577
|
+
})).subscribe(({ url, channelId }) => {
|
|
578
|
+
const messageV10 = {
|
|
579
|
+
type: 'navigation',
|
|
580
|
+
version: '1.0',
|
|
581
|
+
url
|
|
582
|
+
};
|
|
583
|
+
// TODO: sendBest() is not implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
584
|
+
if (document.referrer) {
|
|
585
|
+
this.messageService.send(messageV10);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
if (channelId === undefined) {
|
|
589
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
590
|
+
// eslint-disable-next-line no-console -- warning message as channel id not mandatory
|
|
591
|
+
console.warn('No channelId provided for navigation message');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
try {
|
|
595
|
+
this.messageService.send(messageV10, { to: [channelId] });
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
599
|
+
// eslint-disable-next-line no-console -- send the error in the console, do not fail silently
|
|
600
|
+
console.error('Error sending navigation message', error);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RoutingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
607
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RoutingService, providedIn: 'root' }); }
|
|
608
|
+
}
|
|
609
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RoutingService, decorators: [{
|
|
610
|
+
type: Injectable,
|
|
611
|
+
args: [{
|
|
612
|
+
providedIn: 'root'
|
|
613
|
+
}]
|
|
614
|
+
}], ctorParameters: () => [] });
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* A pipe that restores a route with optional query parameters and memory channel ID.
|
|
618
|
+
*
|
|
619
|
+
* This pipe is used to transform a URL or SafeResourceUrl by appending query parameters
|
|
620
|
+
* and adjusting the pathname based on the current active route and memorized route.
|
|
621
|
+
*/
|
|
622
|
+
class RestoreRoute {
|
|
623
|
+
constructor() {
|
|
624
|
+
this.activeRoute = inject(ActivatedRoute);
|
|
625
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
626
|
+
this.routeMemorizeService = inject(RouteMemorizeService, { optional: true });
|
|
627
|
+
}
|
|
628
|
+
transform(url, options) {
|
|
629
|
+
const urlString = typeof url === 'string'
|
|
630
|
+
? url
|
|
631
|
+
: this.domSanitizer.sanitize(SecurityContext.RESOURCE_URL, url || null);
|
|
632
|
+
if (!url) {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
if (urlString) {
|
|
636
|
+
const moduleUrl = new URL(urlString);
|
|
637
|
+
const queryParamsModule = new URLSearchParams(moduleUrl.searchParams);
|
|
638
|
+
const channelId = options?.memoryChannelId;
|
|
639
|
+
const memorizedRoute = channelId && this.routeMemorizeService?.getRoute(channelId);
|
|
640
|
+
const topWindowUrl = new URL(memorizedRoute ? window.origin + memorizedRoute : window.location.href);
|
|
641
|
+
const queryParamsTopWindow = new URLSearchParams(topWindowUrl.search);
|
|
642
|
+
if (options?.propagateQueryParams) {
|
|
643
|
+
for (const [key, value] of queryParamsTopWindow) {
|
|
644
|
+
if (options?.overrideQueryParams || !queryParamsModule.has(key)) {
|
|
645
|
+
queryParamsModule.set(key, value);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
moduleUrl.search = queryParamsModule.toString();
|
|
650
|
+
moduleUrl.pathname += topWindowUrl.pathname.split(`/${this.activeRoute.routeConfig?.path}`).pop() || '';
|
|
651
|
+
moduleUrl.pathname = moduleUrl.pathname.replace(/\/{2,}/g, '/');
|
|
652
|
+
const moduleUrlStringyfied = moduleUrl.toString();
|
|
653
|
+
return typeof url === 'string' ? moduleUrlStringyfied : this.domSanitizer.bypassSecurityTrustResourceUrl(moduleUrlStringyfied);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RestoreRoute, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
657
|
+
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.1.7", ngImport: i0, type: RestoreRoute, isStandalone: true, name: "restoreRoute" }); }
|
|
658
|
+
}
|
|
659
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: RestoreRoute, decorators: [{
|
|
660
|
+
type: Pipe,
|
|
661
|
+
args: [{
|
|
662
|
+
name: 'restoreRoute'
|
|
663
|
+
}]
|
|
664
|
+
}] });
|
|
665
|
+
|
|
666
|
+
/** Default suffix for an url containing a theme css file */
|
|
667
|
+
const THEME_URL_SUFFIX = '-theme.css';
|
|
668
|
+
/** Default name for the query parameter containing the theme name */
|
|
669
|
+
const THEME_QUERY_PARAM_NAME = 'theme';
|
|
670
|
+
/**
|
|
671
|
+
* Fetches a CSS document and returns the content as a string.
|
|
672
|
+
* @param cssPath - The path to download the CSS.
|
|
673
|
+
* @returns The content of the CSS document as a string, empty string if the fetch fails.
|
|
674
|
+
*/
|
|
675
|
+
async function getStyle(cssPath) {
|
|
676
|
+
try {
|
|
677
|
+
const myRequest = new Request(cssPath);
|
|
678
|
+
const response = await fetch(myRequest);
|
|
679
|
+
const cssText = response.ok ? await response.text() : '';
|
|
680
|
+
return cssText;
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
684
|
+
// eslint-disable-next-line no-console -- display the warning instead of failing silently
|
|
685
|
+
console.warn(`Failed to download style from: ${cssPath} with error: ${error?.toString()}`);
|
|
686
|
+
}
|
|
687
|
+
return '';
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Applies the given CSS theme as a stylesheet.
|
|
691
|
+
* @param themeValue - CSS as text containing a theme definition.
|
|
692
|
+
* @param cleanPrevious - Whether to remove previously applied stylesheets if no themeValue provided. Default is true.
|
|
693
|
+
*/
|
|
694
|
+
function applyTheme(themeValue, cleanPrevious = true) {
|
|
695
|
+
if (themeValue !== undefined) {
|
|
696
|
+
const sheet = new CSSStyleSheet();
|
|
697
|
+
sheet.replaceSync(themeValue);
|
|
698
|
+
document.adoptedStyleSheets = cleanPrevious ? [sheet] : [...document.adoptedStyleSheets, sheet];
|
|
699
|
+
}
|
|
700
|
+
else if (cleanPrevious) { // remove the styles if the theme value comes undefined or empty string
|
|
701
|
+
document.adoptedStyleSheets = [];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Applies the initial theme based on the URL query parameters.
|
|
706
|
+
*
|
|
707
|
+
* This function fetches the CSS theme specified in the URL query parameters and applies it as a stylesheet.
|
|
708
|
+
* If a referrer is present, it also attempts to fetch and apply the theme from the referrer's URL.
|
|
709
|
+
*/
|
|
710
|
+
async function applyInitialTheme() {
|
|
711
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
712
|
+
let cssHref = searchParams.get(THEME_QUERY_PARAM_NAME);
|
|
713
|
+
document.adoptedStyleSheets = [];
|
|
714
|
+
if (cssHref) {
|
|
715
|
+
cssHref = `${cssHref.endsWith('.css') ? cssHref : cssHref + THEME_URL_SUFFIX}`;
|
|
716
|
+
const themeRequest = [];
|
|
717
|
+
themeRequest.push(getStyle(cssHref).then((styleToApply) => applyTheme(styleToApply, false)));
|
|
718
|
+
if (document.referrer) {
|
|
719
|
+
const url = new URL(document.referrer);
|
|
720
|
+
url.pathname += `${url.pathname.endsWith('/') ? '' : '/'}${cssHref}`;
|
|
721
|
+
themeRequest.push(getStyle(url.toString()).then((styleToApply) => applyTheme(styleToApply, false)));
|
|
722
|
+
}
|
|
723
|
+
return Promise.allSettled(themeRequest);
|
|
724
|
+
}
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* This service exposing the current theme signal
|
|
730
|
+
*/
|
|
731
|
+
class ThemeProducerService {
|
|
732
|
+
constructor() {
|
|
733
|
+
this.messageService = inject((MessagePeerService));
|
|
734
|
+
/**
|
|
735
|
+
* The type of messages this service handles ('theme').
|
|
736
|
+
*/
|
|
737
|
+
this.types = THEME_MESSAGE_TYPE;
|
|
738
|
+
const producerManagerService = inject(ProducerManagerService);
|
|
739
|
+
producerManagerService.register(this);
|
|
740
|
+
inject(DestroyRef).onDestroy(() => {
|
|
741
|
+
producerManagerService.unregister(this);
|
|
742
|
+
});
|
|
743
|
+
// get the current theme name from the url (if any) and emit a first value for the current theme
|
|
744
|
+
const parentUrl = new URL(window.location.toString());
|
|
745
|
+
const selectedThemeName = parentUrl.searchParams.get(THEME_QUERY_PARAM_NAME);
|
|
746
|
+
this.currentThemeSelection = signal(selectedThemeName
|
|
747
|
+
? {
|
|
748
|
+
name: selectedThemeName,
|
|
749
|
+
css: null
|
|
750
|
+
}
|
|
751
|
+
: undefined);
|
|
752
|
+
this.currentTheme = this.currentThemeSelection.asReadonly();
|
|
753
|
+
if (selectedThemeName) {
|
|
754
|
+
void this.changeTheme(selectedThemeName);
|
|
755
|
+
}
|
|
756
|
+
// When the current theme changes, apply it to the current application
|
|
757
|
+
effect(() => {
|
|
758
|
+
const themeObj = this.currentTheme();
|
|
759
|
+
if (themeObj?.css !== null) {
|
|
760
|
+
applyTheme(themeObj?.css);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
/**
|
|
764
|
+
* This effect listens for changes in the `currentTheme` signal. If a valid theme object with CSS is present,
|
|
765
|
+
* it creates a theme message and sends it via the message service.
|
|
766
|
+
*/
|
|
767
|
+
effect(() => {
|
|
768
|
+
const themeObj = this.currentTheme();
|
|
769
|
+
if (themeObj && themeObj.css !== null) {
|
|
770
|
+
const messageV10 = {
|
|
771
|
+
type: 'theme',
|
|
772
|
+
name: themeObj.name,
|
|
773
|
+
css: themeObj.css,
|
|
774
|
+
version: '1.0'
|
|
775
|
+
};
|
|
776
|
+
// TODO: sendBest() is not yet implemented -- https://github.com/AmadeusITGroup/microfrontends/issues/11
|
|
777
|
+
this.messageService.send(messageV10);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Changes the current theme to the specified theme name.
|
|
783
|
+
* @param themeName - The name of the theme to change to.
|
|
784
|
+
*/
|
|
785
|
+
async changeTheme(themeName) {
|
|
786
|
+
const cssHref = themeName && `${themeName}${THEME_URL_SUFFIX}`;
|
|
787
|
+
const styleObj = cssHref ? await getStyle(cssHref) : '';
|
|
788
|
+
this.currentThemeSelection.update((theme) => {
|
|
789
|
+
this.previousTheme = theme;
|
|
790
|
+
return themeName
|
|
791
|
+
? { name: themeName, css: styleObj }
|
|
792
|
+
: undefined;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Reverts to the previous theme.
|
|
797
|
+
*/
|
|
798
|
+
revertToPreviousTheme() {
|
|
799
|
+
this.currentThemeSelection.set(this.previousTheme);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* @inheritdoc
|
|
803
|
+
*/
|
|
804
|
+
handleError(message) {
|
|
805
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
806
|
+
// eslint-disable-next-line no-console -- error message should be made available with the logger
|
|
807
|
+
console.error('Error in theme service message', message);
|
|
808
|
+
this.revertToPreviousTheme();
|
|
809
|
+
}
|
|
810
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeProducerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
811
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeProducerService, providedIn: 'root' }); }
|
|
812
|
+
}
|
|
813
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeProducerService, decorators: [{
|
|
814
|
+
type: Injectable,
|
|
815
|
+
args: [{
|
|
816
|
+
providedIn: 'root'
|
|
817
|
+
}]
|
|
818
|
+
}], ctorParameters: () => [] });
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* A pipe that applies the current theme from a theme manager service, to a given URL or SafeResourceUrl, as query param
|
|
822
|
+
*/
|
|
823
|
+
class ApplyTheme {
|
|
824
|
+
constructor() {
|
|
825
|
+
this.themeManagerService = inject(ThemeProducerService);
|
|
826
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
827
|
+
}
|
|
828
|
+
transform(url) {
|
|
829
|
+
if (!url) {
|
|
830
|
+
return undefined;
|
|
831
|
+
}
|
|
832
|
+
const currentTheme = this.themeManagerService.currentTheme();
|
|
833
|
+
const urlString = typeof url === 'string'
|
|
834
|
+
? url
|
|
835
|
+
: this.domSanitizer.sanitize(SecurityContext.RESOURCE_URL, url);
|
|
836
|
+
if (urlString) {
|
|
837
|
+
const moduleUrl = new URL(urlString);
|
|
838
|
+
if (currentTheme) {
|
|
839
|
+
moduleUrl.searchParams.set('theme', currentTheme.name);
|
|
840
|
+
}
|
|
841
|
+
const moduleUrlStringyfied = moduleUrl.toString();
|
|
842
|
+
return typeof url === 'string' ? moduleUrlStringyfied : this.domSanitizer.bypassSecurityTrustResourceUrl(moduleUrlStringyfied);
|
|
843
|
+
}
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ApplyTheme, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
|
|
847
|
+
/** @nocollapse */ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.1.7", ngImport: i0, type: ApplyTheme, isStandalone: true, name: "applyTheme" }); }
|
|
848
|
+
}
|
|
849
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ApplyTheme, decorators: [{
|
|
850
|
+
type: Pipe,
|
|
851
|
+
args: [{
|
|
852
|
+
name: 'applyTheme'
|
|
853
|
+
}]
|
|
854
|
+
}] });
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* A service that handles theme messages and applies the received theme.
|
|
858
|
+
*/
|
|
859
|
+
class ThemeConsumerService {
|
|
860
|
+
constructor() {
|
|
861
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
862
|
+
this.consumerManagerService = inject(ConsumerManagerService);
|
|
863
|
+
/**
|
|
864
|
+
* The type of messages this service handles ('theme').
|
|
865
|
+
*/
|
|
866
|
+
this.type = THEME_MESSAGE_TYPE;
|
|
867
|
+
/**
|
|
868
|
+
* The supported versions of theme messages and their handlers.
|
|
869
|
+
*/
|
|
870
|
+
this.supportedVersions = {
|
|
871
|
+
/**
|
|
872
|
+
* Use the message paylod to get the theme and apply it
|
|
873
|
+
* @param message message to consume
|
|
874
|
+
*/
|
|
875
|
+
'1.0': (message) => {
|
|
876
|
+
const sanitizedCss = this.domSanitizer.sanitize(SecurityContext.STYLE, message.payload.css);
|
|
877
|
+
if (sanitizedCss !== null) {
|
|
878
|
+
applyTheme(sanitizedCss);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
this.start();
|
|
883
|
+
inject(DestroyRef).onDestroy(() => this.stop());
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Starts the theme handler service by registering it into the consumer manager service.
|
|
887
|
+
*/
|
|
888
|
+
start() {
|
|
889
|
+
this.consumerManagerService.register(this);
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Stops the theme handler service by unregistering it from the consumer manager service.
|
|
893
|
+
*/
|
|
894
|
+
stop() {
|
|
895
|
+
this.consumerManagerService.unregister(this);
|
|
896
|
+
}
|
|
897
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeConsumerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
898
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeConsumerService, providedIn: 'root' }); }
|
|
899
|
+
}
|
|
900
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ThemeConsumerService, decorators: [{
|
|
901
|
+
type: Injectable,
|
|
902
|
+
args: [{
|
|
903
|
+
providedIn: 'root'
|
|
904
|
+
}]
|
|
905
|
+
}], ctorParameters: () => [] });
|
|
906
|
+
|
|
907
|
+
class ConnectDirective {
|
|
908
|
+
/**
|
|
909
|
+
* Binds the `src` attribute of the iframe to the sanitized source URL.
|
|
910
|
+
*/
|
|
911
|
+
get srcAttr() {
|
|
912
|
+
return this.src();
|
|
913
|
+
}
|
|
914
|
+
constructor() {
|
|
915
|
+
/**
|
|
916
|
+
* The connection ID required for the message peer service.
|
|
917
|
+
*/
|
|
918
|
+
this.connect = input.required();
|
|
919
|
+
/**
|
|
920
|
+
* The sanitized source URL for the iframe.
|
|
921
|
+
*/
|
|
922
|
+
this.src = input();
|
|
923
|
+
this.messageService = inject(MessagePeerService);
|
|
924
|
+
this.domSanitizer = inject(DomSanitizer);
|
|
925
|
+
this.elRef = inject(ElementRef);
|
|
926
|
+
this.clientOrigin = computed(() => {
|
|
927
|
+
const src = this.src();
|
|
928
|
+
const srcString = src && this.domSanitizer.sanitize(SecurityContext.RESOURCE_URL, src);
|
|
929
|
+
return srcString && new URL(srcString).origin;
|
|
930
|
+
});
|
|
931
|
+
// When the origin url or the peer id changes it will remake the connection with the new updates. The old connection is closed
|
|
932
|
+
effect(async () => {
|
|
933
|
+
const clientOrigin = this.clientOrigin();
|
|
934
|
+
const connectId = this.connect();
|
|
935
|
+
const moduleWindow = this.elRef.nativeElement.contentWindow;
|
|
936
|
+
this.messageService.disconnect();
|
|
937
|
+
if (clientOrigin && moduleWindow) {
|
|
938
|
+
try {
|
|
939
|
+
await this.messageService.listen(connectId, {
|
|
940
|
+
window: moduleWindow,
|
|
941
|
+
origin: clientOrigin
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
// TODO https://github.com/AmadeusITGroup/otter/issues/2887 - proper logger
|
|
946
|
+
// eslint-disable-next-line no-console -- log the error - replace this with a proper logger
|
|
947
|
+
console.error(e);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
// When the directive is destroyed clean up the connection too.
|
|
952
|
+
inject(DestroyRef).onDestroy(() => this.messageService.disconnect());
|
|
953
|
+
}
|
|
954
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ConnectDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
955
|
+
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.7", 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 }); }
|
|
956
|
+
}
|
|
957
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.7", ngImport: i0, type: ConnectDirective, decorators: [{
|
|
958
|
+
type: Directive,
|
|
959
|
+
args: [{
|
|
960
|
+
selector: 'iframe[connect]',
|
|
961
|
+
standalone: true
|
|
962
|
+
}]
|
|
963
|
+
}], ctorParameters: () => [], propDecorators: { srcAttr: [{
|
|
964
|
+
type: HostBinding,
|
|
965
|
+
args: ['src']
|
|
966
|
+
}] } });
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* A constant array of known message types and their versions.
|
|
970
|
+
*/
|
|
971
|
+
const KNOWN_MESSAGES = [
|
|
972
|
+
{
|
|
973
|
+
type: ERROR_MESSAGE_TYPE,
|
|
974
|
+
version: '1.0'
|
|
975
|
+
}
|
|
976
|
+
];
|
|
977
|
+
/**
|
|
978
|
+
* Returns the default options for starting a client endpoint peer connection.
|
|
979
|
+
* As origin it will take the parent origin and the window will be the parent window
|
|
980
|
+
*/
|
|
981
|
+
function getDefaultClientEndpointStartOptions() {
|
|
982
|
+
if (document.referrer) {
|
|
983
|
+
return {
|
|
984
|
+
origin: new URL(document.referrer).origin,
|
|
985
|
+
window: window.parent
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
return {};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Provide the communication protocol connection configuration
|
|
993
|
+
* @param connectionConfig The identifier of the application in the communication protocol ecosystem plus the types of messages able to exchange
|
|
994
|
+
*/
|
|
995
|
+
function provideConnection(connectionConfig) {
|
|
996
|
+
const config = { id: connectionConfig.id, knownMessages: [...KNOWN_MESSAGES, ...(connectionConfig.knownMessages || [])] };
|
|
997
|
+
return makeEnvironmentProviders([
|
|
998
|
+
{
|
|
999
|
+
provide: MESSAGE_PEER_CONFIG, useValue: config
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
provide: MESSAGE_PEER_CONNECT_OPTIONS, useValue: getDefaultClientEndpointStartOptions()
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
// in the case of the ConnectionService will extends the base service 'useExisting' should be used
|
|
1006
|
+
provide: MessagePeerService, useClass: MessagePeerService, deps: [MESSAGE_PEER_CONFIG]
|
|
1007
|
+
}
|
|
1008
|
+
]);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Generated bundle index. Do not edit.
|
|
1013
|
+
*/
|
|
1014
|
+
|
|
1015
|
+
export { ApplyTheme, ConnectDirective, ConsumerManagerService, ERROR_MESSAGE_TYPE, KNOWN_MESSAGES, NavigationConsumerService, ProducerManagerService, ResizeConsumerService, ResizeService, RestoreRoute, RouteMemorizeDirective, RouteMemorizeService, RoutingService, ScalableDirective, THEME_QUERY_PARAM_NAME, THEME_URL_SUFFIX, ThemeConsumerService, ThemeProducerService, applyInitialTheme, applyTheme, getAvailableConsumers, getDefaultClientEndpointStartOptions, getStyle, isErrorMessage, provideConnection, sendError };
|
|
1016
|
+
//# sourceMappingURL=ama-mfe-ng-utils.mjs.map
|