@schukai/monster 4.23.5 → 4.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/source/components/content/viewer/html.mjs +171 -0
- package/source/components/content/viewer/message.mjs +704 -0
- package/source/components/content/viewer/style/html.pcss +10 -0
- package/source/components/content/viewer/style/message.pcss +148 -0
- package/source/components/content/viewer/stylesheet/html.mjs +38 -0
- package/source/components/content/viewer/stylesheet/message.mjs +38 -0
- package/source/components/content/viewer.mjs +626 -522
- package/source/components/form/digits.mjs +0 -1
- package/source/components/form/select.mjs +2787 -2845
- package/source/components/form/style/select.pcss +0 -4
- package/source/components/form/stylesheet/select.mjs +14 -7
- package/source/components/form/util/floating-ui.mjs +2 -1
- package/source/components/layout/board.mjs +0 -5
- package/source/components/layout/panel.mjs +1 -1
- package/source/components/layout/tabs.mjs +0 -2
- package/source/components/navigation/table-of-content.mjs +0 -1
- package/source/dom/sanitize-html.mjs +54 -0
- package/source/monster.mjs +3 -0
- package/source/text/markdown-parser.mjs +253 -241
- package/source/types/version.mjs +1 -1
- package/test/cases/monster.mjs +1 -1
- package/test/web/import.js +1 -0
- package/test/web/test.html +2 -2
- package/test/web/tests.js +555 -149
@@ -0,0 +1,704 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
|
3
|
+
* Node module: @schukai/monster
|
4
|
+
*
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
7
|
+
*
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
10
|
+
* For more information about purchasing a commercial license, please contact schukai GmbH.
|
11
|
+
*
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
13
|
+
*/
|
14
|
+
|
15
|
+
import { instanceSymbol } from "../../../constants.mjs";
|
16
|
+
import {
|
17
|
+
assembleMethodSymbol,
|
18
|
+
CustomElement,
|
19
|
+
registerCustomElement,
|
20
|
+
updaterTransformerMethodsSymbol,
|
21
|
+
} from "../../../dom/customelement.mjs";
|
22
|
+
|
23
|
+
import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
|
24
|
+
import { MessageStyleSheet } from "./stylesheet/message.mjs";
|
25
|
+
import { isArray, isObject } from "../../../types/is.mjs";
|
26
|
+
import { findTargetElementFromEvent } from "../../../dom/events.mjs";
|
27
|
+
import { getLocaleOfDocument } from "../../../dom/locale.mjs";
|
28
|
+
|
29
|
+
export { MessageContent };
|
30
|
+
|
31
|
+
/**
|
32
|
+
* @private
|
33
|
+
* @type {symbol}
|
34
|
+
*/
|
35
|
+
const containerElementSymbol = Symbol("containerElement");
|
36
|
+
|
37
|
+
/**
|
38
|
+
* @private
|
39
|
+
* @type {symbol}
|
40
|
+
*/
|
41
|
+
const contentContainerElementSymbol = Symbol("contentContainerElement");
|
42
|
+
|
43
|
+
/**
|
44
|
+
* @private
|
45
|
+
* @type {symbol}
|
46
|
+
*/
|
47
|
+
const embeddedImageUrlsSymbol = Symbol("embeddedImageUrls");
|
48
|
+
|
49
|
+
/**
|
50
|
+
* The MessageContent component is used to display arbitrary HTML content within its shadow DOM.
|
51
|
+
* It provides options for sanitization to prevent XSS attacks.
|
52
|
+
*
|
53
|
+
* @copyright schukai GmbH
|
54
|
+
* @summary An HTML content component that can display sanitized HTML.
|
55
|
+
*/
|
56
|
+
class MessageContent extends CustomElement {
|
57
|
+
constructor() {
|
58
|
+
super();
|
59
|
+
this[embeddedImageUrlsSymbol] = [];
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* This method is called by the `instanceof` operator.
|
64
|
+
* @return {symbol}
|
65
|
+
*/
|
66
|
+
static get [instanceSymbol]() {
|
67
|
+
return Symbol.for(
|
68
|
+
"@schukai/monster/components/content/viewer/message-content@@instance",
|
69
|
+
);
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
|
74
|
+
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
|
75
|
+
*
|
76
|
+
* The individual configuration values can be found in the table.
|
77
|
+
*
|
78
|
+
* @property {Object} templates Template definitions
|
79
|
+
* @property {string} templates.main Main template
|
80
|
+
* @property {string} content The HTML string to be displayed.
|
81
|
+
* @property {Object} features Features to enable or disable specific functionalities.
|
82
|
+
* @property {boolean} features.sanitize Whether to sanitize the HTML content (removes scripts, etc.). Defaults to true.
|
83
|
+
* @property {object} sanitize Sanitization options.
|
84
|
+
* @property {function} sanitize.callback A callback function to sanitize the HTML content. Defaults to a built-in sanitizer. You can use libraries like DOMPurify for this purpose.
|
85
|
+
* @property {Object} message The message object containing email details.
|
86
|
+
* @property {Object} message.from The sender's information.
|
87
|
+
* @property {string|null} message.from.name The sender's name.
|
88
|
+
* @property {string|null} message.from.address The sender's email address.
|
89
|
+
* @property {Object} message.to The recipient's information.
|
90
|
+
* @property {string|null} message.to.name The recipient's name.
|
91
|
+
* @property {string|null} message.to.address The recipient's email address.
|
92
|
+
* @property {Array} message.cc An array of CC recipients.
|
93
|
+
* @property {string|null} message.subject The subject of the email.
|
94
|
+
* @property {string|null} message.date The date of the email, formatted as a string.
|
95
|
+
* @property {string|null} message.messageID The unique identifier of the email message.
|
96
|
+
* @property {Array} message.parts An array of parts of the email, which can include text, HTML, attachments, etc.
|
97
|
+
* @property {Array} message.attachments An array of attachments processed from the email parts.
|
98
|
+
* @property {Object} message.headers Additional headers of the email.
|
99
|
+
*/
|
100
|
+
get defaults() {
|
101
|
+
return Object.assign({}, super.defaults, {
|
102
|
+
templates: {
|
103
|
+
main: getTemplate(),
|
104
|
+
},
|
105
|
+
|
106
|
+
templateFormatter: {
|
107
|
+
marker: {
|
108
|
+
open: null,
|
109
|
+
close: null,
|
110
|
+
},
|
111
|
+
i18n: true,
|
112
|
+
},
|
113
|
+
|
114
|
+
content: "", // Default content is an empty slot
|
115
|
+
|
116
|
+
features: {
|
117
|
+
sanitize: true, // Enable sanitization by default
|
118
|
+
},
|
119
|
+
|
120
|
+
sanitize: {
|
121
|
+
callback: sanitizeHtml.bind(this),
|
122
|
+
},
|
123
|
+
|
124
|
+
labels: getTranslations(),
|
125
|
+
|
126
|
+
message: {
|
127
|
+
from: {
|
128
|
+
name: null,
|
129
|
+
address: null,
|
130
|
+
},
|
131
|
+
to: {
|
132
|
+
name: null,
|
133
|
+
address: null,
|
134
|
+
},
|
135
|
+
cc: [],
|
136
|
+
subject: null,
|
137
|
+
date: null,
|
138
|
+
messageID: null,
|
139
|
+
parts: [],
|
140
|
+
attachments: [], // Added for processed attachments
|
141
|
+
headers: [],
|
142
|
+
},
|
143
|
+
});
|
144
|
+
}
|
145
|
+
|
146
|
+
/**
|
147
|
+
* Returns the updater transformer methods for this component.
|
148
|
+
* @returns {{sanitizeHtml: ((function(*): (*))|*)}}
|
149
|
+
*/
|
150
|
+
[updaterTransformerMethodsSymbol]() {
|
151
|
+
return {
|
152
|
+
sanitizeHtml: (value) => {
|
153
|
+
if (this.getOption("features.sanitize")) {
|
154
|
+
return this.getOption("sanitize.callback")(value);
|
155
|
+
}
|
156
|
+
return value;
|
157
|
+
},
|
158
|
+
};
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Sets the content of the MessageContent component.
|
163
|
+
* @param {Object} message The message object containing parts, headers, etc.
|
164
|
+
* @returns {MessageContent}
|
165
|
+
*/
|
166
|
+
setMessage(message) {
|
167
|
+
if (!isObject(message)) {
|
168
|
+
throw new Error("message must be an object");
|
169
|
+
}
|
170
|
+
|
171
|
+
this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
|
172
|
+
this[embeddedImageUrlsSymbol] = [];
|
173
|
+
|
174
|
+
this.setOption("message.from.name", message?.from?.name || null);
|
175
|
+
this.setOption("message.from.address", message?.from?.address || null);
|
176
|
+
this.setOption("message.to.name", message?.to?.name || null);
|
177
|
+
this.setOption("message.to.address", message?.to?.address || null);
|
178
|
+
|
179
|
+
const dateTime = message?.date ? new Date(message.date) : null;
|
180
|
+
const localeDateTime = dateTime?.toLocaleString(navigator.language, {
|
181
|
+
year: "numeric",
|
182
|
+
month: "long",
|
183
|
+
day: "numeric",
|
184
|
+
hour: "2-digit",
|
185
|
+
minute: "2-digit",
|
186
|
+
});
|
187
|
+
|
188
|
+
this.setOption("message.date", localeDateTime || null);
|
189
|
+
this.setOption("message.cc", message?.cc || []);
|
190
|
+
this.setOption("message.subject", message?.subject || null);
|
191
|
+
this.setOption("message.messageID", message?.messageID || null);
|
192
|
+
|
193
|
+
const headers = [];
|
194
|
+
for (const [key, value] of Object.entries(message?.headers || {})) {
|
195
|
+
if (key && value) {
|
196
|
+
let valueString = value;
|
197
|
+
if (isArray(valueString)) {
|
198
|
+
valueString = "<ul>";
|
199
|
+
for (const item of value) {
|
200
|
+
valueString += `<li>${item}</li>`;
|
201
|
+
}
|
202
|
+
valueString += "</ul>";
|
203
|
+
}
|
204
|
+
|
205
|
+
headers.push({
|
206
|
+
key: key,
|
207
|
+
value: valueString,
|
208
|
+
});
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
this.setOption("message.headers", headers || []);
|
213
|
+
|
214
|
+
let htmlContent = "";
|
215
|
+
let plainTextContent = "";
|
216
|
+
const attachments = [];
|
217
|
+
const embeddedImages = {};
|
218
|
+
|
219
|
+
const processParts = (parts) => {
|
220
|
+
if (!parts || !Array.isArray(parts)) {
|
221
|
+
return;
|
222
|
+
}
|
223
|
+
|
224
|
+
for (const part of parts) {
|
225
|
+
try {
|
226
|
+
if (part.parts && part.parts.length > 0) {
|
227
|
+
processParts(part.parts);
|
228
|
+
} else if (
|
229
|
+
part.dispositionType === "attachment" &&
|
230
|
+
part.contentType
|
231
|
+
) {
|
232
|
+
part["index"] = attachments.length; // Füge Index hinzu, um die Reihenfolge zu verfolgen
|
233
|
+
part["fileSize"] = part.content ? part.content.length : 0; // Dateigröße in Bytes
|
234
|
+
part["humanReadableSize"] = part.content
|
235
|
+
? `${(part.content.length / 1024).toFixed(2)} KB`
|
236
|
+
: "0 KB"; // Menschlich lesbare Größe
|
237
|
+
|
238
|
+
attachments.push(part);
|
239
|
+
} else if (
|
240
|
+
part.contentType &&
|
241
|
+
part.contentType.startsWith("text/html")
|
242
|
+
) {
|
243
|
+
htmlContent = part.content;
|
244
|
+
} else if (
|
245
|
+
part.contentType &&
|
246
|
+
part.contentType.startsWith("text/plain")
|
247
|
+
) {
|
248
|
+
if (!htmlContent) {
|
249
|
+
plainTextContent = part.content;
|
250
|
+
}
|
251
|
+
} else if (
|
252
|
+
part.dispositionType === "inline" &&
|
253
|
+
part.contentType &&
|
254
|
+
part.contentType.startsWith("image/")
|
255
|
+
) {
|
256
|
+
const cid =
|
257
|
+
part?.["contentId"] ||
|
258
|
+
(part.filename
|
259
|
+
? part.filename.split(".").slice(0, -1).join(".")
|
260
|
+
: null);
|
261
|
+
if (cid) {
|
262
|
+
embeddedImages[cid] = part;
|
263
|
+
} else {
|
264
|
+
console.warn(
|
265
|
+
"Inline image part found without Content-ID or filename:",
|
266
|
+
part,
|
267
|
+
);
|
268
|
+
}
|
269
|
+
}
|
270
|
+
} catch (e) {
|
271
|
+
console.error("Error processing part:", part, e);
|
272
|
+
}
|
273
|
+
}
|
274
|
+
};
|
275
|
+
|
276
|
+
if (message?.parts) {
|
277
|
+
processParts(message.parts);
|
278
|
+
}
|
279
|
+
|
280
|
+
if (!htmlContent && plainTextContent) {
|
281
|
+
htmlContent = plainTextContent.replace(/\n/g, "<br>");
|
282
|
+
}
|
283
|
+
|
284
|
+
for (const cid in embeddedImages) {
|
285
|
+
const imagePart = embeddedImages[cid];
|
286
|
+
if (imagePart.content && imagePart.contentType) {
|
287
|
+
try {
|
288
|
+
const base64ImageContent = imagePart.content.replace(/\s/g, "");
|
289
|
+
const imageContentType = imagePart.contentType;
|
290
|
+
let cleanCid = imagePart.contentId;
|
291
|
+
|
292
|
+
if (cleanCid) {
|
293
|
+
cleanCid = cleanCid.trim();
|
294
|
+
cleanCid = cleanCid.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); // Steuerzeichen entfernen
|
295
|
+
} else {
|
296
|
+
cleanCid = imagePart.filename
|
297
|
+
? imagePart.filename.split(".").slice(0, -1).join(".")
|
298
|
+
: null;
|
299
|
+
if (!cleanCid) {
|
300
|
+
console.warn(
|
301
|
+
"Content-ID or filename not found for an image part. Cannot replace CID in HTML.",
|
302
|
+
);
|
303
|
+
continue; // Überspringe dieses Bild, wenn CID fehlt
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
const decodedContent = atob(base64ImageContent);
|
308
|
+
const uint8Array = new Uint8Array(decodedContent.length);
|
309
|
+
for (let i = 0; i < decodedContent.length; i++) {
|
310
|
+
uint8Array[i] = decodedContent.charCodeAt(i);
|
311
|
+
}
|
312
|
+
|
313
|
+
const blob = new Blob([uint8Array], { type: imageContentType });
|
314
|
+
const objectUrl = URL.createObjectURL(blob);
|
315
|
+
this[embeddedImageUrlsSymbol].push(objectUrl); // Speichern zur späteren Widerrufung
|
316
|
+
|
317
|
+
// Den CID für die RegExp escapen, um Sonderzeichen zu behandeln
|
318
|
+
const escapedCid = cleanCid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
319
|
+
const cidRegex = new RegExp(
|
320
|
+
`src=["']cid:[^"']*?${escapedCid}[^"']*?["']`,
|
321
|
+
"gi",
|
322
|
+
);
|
323
|
+
|
324
|
+
htmlContent = htmlContent.replace(cidRegex, `src="${objectUrl}"`);
|
325
|
+
} catch (e) {
|
326
|
+
console.error(
|
327
|
+
`Error processing embedded image with CID '${cid}':`,
|
328
|
+
e,
|
329
|
+
);
|
330
|
+
htmlContent = htmlContent.replace(
|
331
|
+
new RegExp(`cid:${cid}`, "g"),
|
332
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAA C0lEQVR42mP8Xw8AAo8GgAAAAgA+cQIPQAAAABJRU5ErkJggg==",
|
333
|
+
);
|
334
|
+
}
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
this[contentContainerElementSymbol].setOption("content", htmlContent);
|
339
|
+
this.setOption("message.attachments", attachments);
|
340
|
+
|
341
|
+
this.setOption("message.parts", message?.parts || []);
|
342
|
+
|
343
|
+
return this;
|
344
|
+
}
|
345
|
+
|
346
|
+
/**
|
347
|
+
* Handles the click event for an attachment download button.
|
348
|
+
* @param {Event} event
|
349
|
+
* @param {Object} part The attachment part data.
|
350
|
+
*/
|
351
|
+
onDownloadAttachmentClick(event, part) {
|
352
|
+
event.preventDefault();
|
353
|
+
|
354
|
+
if (part.content && part.filename && part.contentType) {
|
355
|
+
try {
|
356
|
+
// Assuming part.content is base64 encoded. Adjust if your content is raw binary.
|
357
|
+
const decodedContent = atob(part.content);
|
358
|
+
const uint8Array = new Uint8Array(decodedContent.length);
|
359
|
+
for (let i = 0; i < decodedContent.length; i++) {
|
360
|
+
uint8Array[i] = decodedContent.charCodeAt(i);
|
361
|
+
}
|
362
|
+
const blob = new Blob([uint8Array], { type: part.contentType });
|
363
|
+
|
364
|
+
const url = URL.createObjectURL(blob);
|
365
|
+
const a = document.createElement("a");
|
366
|
+
a.href = url;
|
367
|
+
a.download = part.filename;
|
368
|
+
document.body.appendChild(a);
|
369
|
+
a.click();
|
370
|
+
document.body.removeChild(a);
|
371
|
+
URL.revokeObjectURL(url);
|
372
|
+
} catch (e) {
|
373
|
+
console.error("Error downloading attachment:", e);
|
374
|
+
alert(
|
375
|
+
"Could not download file. Content might not be base64 or is corrupted.",
|
376
|
+
);
|
377
|
+
}
|
378
|
+
} else {
|
379
|
+
alert("Attachment content not available for download.");
|
380
|
+
}
|
381
|
+
}
|
382
|
+
|
383
|
+
/**
|
384
|
+
* @return {string}
|
385
|
+
*/
|
386
|
+
static getTag() {
|
387
|
+
return "monster-message-content";
|
388
|
+
}
|
389
|
+
|
390
|
+
/**
|
391
|
+
* @return {MessageContent}
|
392
|
+
*/
|
393
|
+
[assembleMethodSymbol]() {
|
394
|
+
super[assembleMethodSymbol]();
|
395
|
+
initControlReferences.call(this);
|
396
|
+
initEventHandler.call(this);
|
397
|
+
}
|
398
|
+
|
399
|
+
/**
|
400
|
+
* @return {Array}
|
401
|
+
*/
|
402
|
+
static getCSSStyleSheet() {
|
403
|
+
return [MessageStyleSheet];
|
404
|
+
}
|
405
|
+
|
406
|
+
/**
|
407
|
+
* Cleans up any resources when the element is removed from the DOM.
|
408
|
+
* Note: This method relies on the CustomElement base class calling it.
|
409
|
+
* If CustomElement does not have a disconnectedCallback equivalent,
|
410
|
+
* manual cleanup or a different strategy will be needed.
|
411
|
+
*/
|
412
|
+
disconnectedCallback() {
|
413
|
+
super.disconnectedCallback?.(); // Call super's disconnectedCallback if it exists
|
414
|
+
this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
|
415
|
+
this[embeddedImageUrlsSymbol] = [];
|
416
|
+
}
|
417
|
+
}
|
418
|
+
|
419
|
+
/**
|
420
|
+
* @private
|
421
|
+
* @return {MessageContent}
|
422
|
+
*/
|
423
|
+
function initControlReferences() {
|
424
|
+
if (!this.shadowRoot) {
|
425
|
+
throw new Error("no shadow-root is defined");
|
426
|
+
}
|
427
|
+
|
428
|
+
this[containerElementSymbol] = this.shadowRoot.querySelector(
|
429
|
+
"[data-monster-role=container]",
|
430
|
+
);
|
431
|
+
|
432
|
+
this[contentContainerElementSymbol] = this.shadowRoot.querySelector(
|
433
|
+
"[data-monster-role=content-container]",
|
434
|
+
);
|
435
|
+
|
436
|
+
return this;
|
437
|
+
}
|
438
|
+
|
439
|
+
function getTranslations() {
|
440
|
+
const locale = getLocaleOfDocument();
|
441
|
+
switch (locale.language) {
|
442
|
+
case "de":
|
443
|
+
return {
|
444
|
+
content: "Inhalt",
|
445
|
+
headers: "Kopfzeilen",
|
446
|
+
};
|
447
|
+
case "es":
|
448
|
+
return {
|
449
|
+
content: "Contenido",
|
450
|
+
headers: "Encabezados",
|
451
|
+
};
|
452
|
+
case "zh":
|
453
|
+
return {
|
454
|
+
content: "内容",
|
455
|
+
headers: "标题",
|
456
|
+
};
|
457
|
+
|
458
|
+
case "hi":
|
459
|
+
return {
|
460
|
+
content: "सामग्री",
|
461
|
+
headers: "शीर्षक",
|
462
|
+
};
|
463
|
+
|
464
|
+
case "bn":
|
465
|
+
return {
|
466
|
+
content: "বিষয়বস্তু",
|
467
|
+
headers: "শিরোনাম",
|
468
|
+
};
|
469
|
+
|
470
|
+
case "pt": // Portuguese
|
471
|
+
return {
|
472
|
+
content: "Conteúdo",
|
473
|
+
headers: "Cabeçalhos",
|
474
|
+
};
|
475
|
+
|
476
|
+
case "ru": // Russian
|
477
|
+
return {
|
478
|
+
content: "Содержание",
|
479
|
+
headers: "Заголовки",
|
480
|
+
};
|
481
|
+
|
482
|
+
case "ja": // Japanese
|
483
|
+
return {
|
484
|
+
content: "コンテンツ",
|
485
|
+
headers: "ヘッダー",
|
486
|
+
};
|
487
|
+
|
488
|
+
case "pa": // Western Punjabi
|
489
|
+
return {
|
490
|
+
content: "ਸਮੱਗਰੀ",
|
491
|
+
headers: "ਸਿਰਲੇਖ",
|
492
|
+
};
|
493
|
+
|
494
|
+
case "mr": // Marathi
|
495
|
+
return {
|
496
|
+
content: "सामग्री",
|
497
|
+
headers: "शीर्षके",
|
498
|
+
};
|
499
|
+
|
500
|
+
case "fr": // French
|
501
|
+
return {
|
502
|
+
content: "Contenu",
|
503
|
+
headers: "En-têtes",
|
504
|
+
};
|
505
|
+
|
506
|
+
case "it": // Italian
|
507
|
+
return {
|
508
|
+
content: "Contenuto",
|
509
|
+
headers: "Intestazioni",
|
510
|
+
};
|
511
|
+
|
512
|
+
case "nl": // Dutch
|
513
|
+
return {
|
514
|
+
content: "Inhoud",
|
515
|
+
headers: "Headers",
|
516
|
+
};
|
517
|
+
|
518
|
+
case "sv": // Swedish
|
519
|
+
return {
|
520
|
+
content: "Innehåll",
|
521
|
+
headers: "Rubriker",
|
522
|
+
};
|
523
|
+
|
524
|
+
case "pl": // Polish
|
525
|
+
return {
|
526
|
+
content: "Zawartość",
|
527
|
+
headers: "Nagłówki",
|
528
|
+
};
|
529
|
+
|
530
|
+
case "da": // Danish
|
531
|
+
return {
|
532
|
+
content: "Indhold",
|
533
|
+
headers: "Overskrifter",
|
534
|
+
};
|
535
|
+
|
536
|
+
case "no": // Norwegian
|
537
|
+
return {
|
538
|
+
content: "Innhold",
|
539
|
+
headers: "Overskrifter",
|
540
|
+
};
|
541
|
+
|
542
|
+
case "cs": // Czech
|
543
|
+
return {
|
544
|
+
content: "Obsah",
|
545
|
+
headers: "Hlavičky",
|
546
|
+
};
|
547
|
+
|
548
|
+
default:
|
549
|
+
return {
|
550
|
+
content: "Content",
|
551
|
+
headers: "Headers",
|
552
|
+
};
|
553
|
+
}
|
554
|
+
}
|
555
|
+
|
556
|
+
/**
|
557
|
+
* @private
|
558
|
+
* @param index
|
559
|
+
* @param attachments
|
560
|
+
*/
|
561
|
+
function downloadAttachmentByIndex(index, attachments) {
|
562
|
+
const part = attachments[index];
|
563
|
+
if (!part) {
|
564
|
+
console.error(`Attachment mit Index ${index} nicht gefunden.`);
|
565
|
+
return;
|
566
|
+
}
|
567
|
+
|
568
|
+
const { filename, contentType, content } = part;
|
569
|
+
|
570
|
+
try {
|
571
|
+
let decodedContent;
|
572
|
+
if (contentType.startsWith("text/")) {
|
573
|
+
// Check if it's a text type
|
574
|
+
decodedContent = content; // Content is plain text
|
575
|
+
} else {
|
576
|
+
decodedContent = atob(content); // Assume base64 for other types
|
577
|
+
}
|
578
|
+
|
579
|
+
const len = decodedContent.length;
|
580
|
+
const bytes = new Uint8Array(len);
|
581
|
+
for (let i = 0; i < len; i++) {
|
582
|
+
bytes[i] = decodedContent.charCodeAt(i);
|
583
|
+
}
|
584
|
+
|
585
|
+
const blob = new Blob([bytes], { type: contentType });
|
586
|
+
const dataUrl = URL.createObjectURL(blob);
|
587
|
+
|
588
|
+
const a = document.createElement("a");
|
589
|
+
a.style.display = "none";
|
590
|
+
a.href = dataUrl;
|
591
|
+
a.download = filename;
|
592
|
+
document.body.appendChild(a);
|
593
|
+
a.click();
|
594
|
+
document.body.removeChild(a);
|
595
|
+
|
596
|
+
URL.revokeObjectURL(dataUrl);
|
597
|
+
} catch (e) {
|
598
|
+
console.error("Error downloading attachment:", e);
|
599
|
+
}
|
600
|
+
}
|
601
|
+
|
602
|
+
/**
|
603
|
+
* Initializes the event handler for the MessageContent component.
|
604
|
+
* @private
|
605
|
+
* @returns {initEventHandler}
|
606
|
+
*/
|
607
|
+
function initEventHandler() {
|
608
|
+
this[containerElementSymbol].addEventListener("click", (event) => {
|
609
|
+
const card = findTargetElementFromEvent(
|
610
|
+
event,
|
611
|
+
"data-monster-role",
|
612
|
+
"attachment",
|
613
|
+
);
|
614
|
+
if (card) {
|
615
|
+
const index = card.getAttribute("data-monster-index");
|
616
|
+
const attachments = this.getOption("message.attachments");
|
617
|
+
if (
|
618
|
+
index !== null &&
|
619
|
+
index !== undefined &&
|
620
|
+
attachments &&
|
621
|
+
Array.isArray(attachments)
|
622
|
+
) {
|
623
|
+
const parsedIndex = parseInt(index, 10);
|
624
|
+
if (
|
625
|
+
!isNaN(parsedIndex) &&
|
626
|
+
parsedIndex >= 0 &&
|
627
|
+
parsedIndex < attachments.length
|
628
|
+
) {
|
629
|
+
downloadAttachmentByIndex(parsedIndex, attachments);
|
630
|
+
} else {
|
631
|
+
this.dispatchEvent(
|
632
|
+
new CustomEvent("error", {
|
633
|
+
detail: {
|
634
|
+
message: `Invalid attachment index: ${index}. Must be a number between 0 and ${attachments.length - 1}.`,
|
635
|
+
},
|
636
|
+
}),
|
637
|
+
);
|
638
|
+
}
|
639
|
+
}
|
640
|
+
}
|
641
|
+
});
|
642
|
+
return this;
|
643
|
+
}
|
644
|
+
|
645
|
+
/**
|
646
|
+
* @private
|
647
|
+
* @return {string}
|
648
|
+
*/
|
649
|
+
function getTemplate() {
|
650
|
+
// language=HTML
|
651
|
+
return `
|
652
|
+
|
653
|
+
<template id="attachments">
|
654
|
+
<div class="attachments">
|
655
|
+
<div class="attachment-card"
|
656
|
+
data-monster-role="attachment"
|
657
|
+
data-monster-attributes="data-monster-index path:attachments.index">
|
658
|
+
<div class="attachment-icon downloadIcon" title="Download"></div>
|
659
|
+
<div class="attachment-info">
|
660
|
+
<strong class="attachment-filename"
|
661
|
+
data-monster-attributes="title path:attachments.filename | default:—"
|
662
|
+
data-monster-replace="path:attachments.filename | default:—"></strong>
|
663
|
+
<span class="attachment-size"
|
664
|
+
data-monster-replace="path:attachments.humanReadableSize | default:—"></span>
|
665
|
+
</div>
|
666
|
+
</div>
|
667
|
+
</div>
|
668
|
+
</template>
|
669
|
+
|
670
|
+
<template id="headers">
|
671
|
+
<div data-monster-replace="path:headers.key">
|
672
|
+
</div>
|
673
|
+
<div class="header-value" data-monster-replace="path:headers.value | default:—"></div>
|
674
|
+
</template>
|
675
|
+
|
676
|
+
|
677
|
+
<div data-monster-role="container" part="container">
|
678
|
+
<div class="emailContainer">
|
679
|
+
<div class="emailHeader">
|
680
|
+
<div><strong><span data-monster-replace="path:message.from.name | default:—"></span> <span
|
681
|
+
class="reduced" data-monster-replace="path:message.from.address"></span></strong></div>
|
682
|
+
<div class="emailDate" data-monster-replace="path:message.date | default:—"></div>
|
683
|
+
<div class="emailSubject" data-monster-replace="path:message.subject | default:—"></div>
|
684
|
+
</div>
|
685
|
+
|
686
|
+
<monster-tabs>
|
687
|
+
<div data-monster-button-label="i18n{content}">
|
688
|
+
<monster-html-content
|
689
|
+
class="emailContent" data-monster-role="content-container">
|
690
|
+
</monster-html-content>
|
691
|
+
<div data-monster-role="attachments"
|
692
|
+
data-monster-insert="attachments path:message.attachments"></div>
|
693
|
+
</div>
|
694
|
+
<div data-monster-button-label="i18n{headers}">
|
695
|
+
<div data-monster-insert="headers path:message.headers" data-monster-role="headers-container"></div>
|
696
|
+
</div>
|
697
|
+
</monster-tabs>
|
698
|
+
|
699
|
+
</div>
|
700
|
+
</div>
|
701
|
+
`;
|
702
|
+
}
|
703
|
+
|
704
|
+
registerCustomElement(MessageContent);
|