@schukai/monster 4.23.6 → 4.24.1

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/source/components/content/viewer/html.mjs +171 -0
  4. package/source/components/content/viewer/message.mjs +704 -0
  5. package/source/components/content/viewer/style/html.pcss +10 -0
  6. package/source/components/content/viewer/style/message.pcss +148 -0
  7. package/source/components/content/viewer/stylesheet/html.mjs +38 -0
  8. package/source/components/content/viewer/stylesheet/message.mjs +38 -0
  9. package/source/components/content/viewer.mjs +626 -522
  10. package/source/components/form/digits.mjs +0 -1
  11. package/source/components/form/select.mjs +2787 -2845
  12. package/source/components/form/style/select.pcss +0 -4
  13. package/source/components/form/stylesheet/select.mjs +14 -7
  14. package/source/components/form/util/floating-ui.mjs +2 -1
  15. package/source/components/layout/board.mjs +0 -5
  16. package/source/components/layout/panel.mjs +1 -1
  17. package/source/components/layout/popper.mjs +19 -10
  18. package/source/components/layout/tabs.mjs +17 -13
  19. package/source/components/navigation/table-of-content.mjs +0 -1
  20. package/source/components/tree-menu/style/tree-menu.pcss +1 -0
  21. package/source/components/tree-menu/stylesheet/tree-menu.mjs +1 -1
  22. package/source/dom/sanitize-html.mjs +54 -0
  23. package/source/monster.mjs +3 -0
  24. package/source/text/markdown-parser.mjs +253 -241
  25. package/source/types/version.mjs +1 -1
  26. package/test/cases/monster.mjs +1 -1
  27. package/test/web/import.js +1 -0
  28. package/test/web/test.html +2 -2
  29. package/test/web/tests.js +555 -149
@@ -13,19 +13,21 @@
13
13
  */
14
14
 
15
15
  import {
16
- assembleMethodSymbol,
17
- CustomElement,
18
- registerCustomElement,
16
+ assembleMethodSymbol,
17
+ CustomElement,
18
+ registerCustomElement,
19
19
  } from "../../dom/customelement.mjs";
20
20
  import "../notify/notify.mjs";
21
- import {ViewerStyleSheet} from "./stylesheet/viewer.mjs";
22
- import {instanceSymbol} from "../../constants.mjs";
23
- import {isString} from "../../types/is.mjs";
24
- import {getGlobal} from "../../types/global.mjs";
25
- import {MediaType, parseMediaType} from "../../types/mediatype.mjs";
26
- import {MarkdownToHTML} from "../../text/markdown-parser.mjs";
21
+ import { ViewerStyleSheet } from "./stylesheet/viewer.mjs";
22
+ import { instanceSymbol } from "../../constants.mjs";
23
+ import { isString } from "../../types/is.mjs";
24
+ import { getGlobal } from "../../types/global.mjs";
25
+ import { MediaType, parseMediaType } from "../../types/mediatype.mjs";
26
+ import { MarkdownToHTML } from "../../text/markdown-parser.mjs";
27
+ import "../layout/tabs.mjs";
28
+ import "./viewer/message.mjs";
27
29
 
28
- export {Viewer};
30
+ export { Viewer };
29
31
 
30
32
  /**
31
33
  * @private
@@ -46,502 +48,604 @@ const viewerElementSymbol = Symbol("viewerElement");
46
48
  * @summary A simple viewer component for PDF, HTML, and images.
47
49
  */
48
50
  class Viewer extends CustomElement {
49
- /**
50
- * This method is called by the `instanceof` operator.
51
- * @return {symbol}
52
- */
53
- static get [instanceSymbol]() {
54
- return Symbol.for("@schukai/monster/components/content/viewer@@instance");
55
- }
56
-
57
- /**
58
- * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
59
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
60
- *
61
- * The individual configuration values can be found in the table.
62
- *
63
- * @property {Object} templates Template definitions
64
- * @property {string} templates.main Main template
65
- * @property {string} content Content to be displayed in the viewer
66
- * @property {Object} classes Css classes
67
- * @property {string} classes.viewer Css class for the viewer
68
- * @property {Object} renderers Renderers for different media types
69
- * @property {function} renderers.image Function to render image content
70
- * @property {function} renderers.html Function to render HTML content
71
- * @property {function} renderers.pdf Function to render PDF content
72
- * @property {function} renderers.plaintext Function to render plain text content
73
- * @property {function} renderers.markdown Function to render Markdown content
74
- */
75
- get defaults() {
76
- return Object.assign({}, super.defaults, {
77
- templates: {
78
- main: getTemplate(),
79
- },
80
- content: "<slot></slot>",
81
- classes: {
82
- viewer: "",
83
- },
84
- renderers: {
85
- image: this.setImage,
86
- html: this.setHTML,
87
- pdf: this.setPDF,
88
- plaintext: this.setPlainText,
89
- markdown: this.setMarkdown,
90
- audio: this.setAudio,
91
- video: this.setVideo,
92
- }
93
- });
94
- }
95
-
96
- /**
97
- * Sets the content of an element based on the provided content and media type.
98
- *
99
- * @param {string} content - The content to be set.
100
- * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
101
- * @return {void} This method does not return a value.
102
- * @throws {Error} Throws an error if shadowRoot is not defined.
103
- */
104
- setContent(content, mediaType = "text/plain") {
105
- if (!this.shadowRoot) {
106
- throw new Error("no shadow-root is defined");
107
- }
108
-
109
- const renderers = this.getOption("renderers");
110
-
111
- const isDataURL = (value) => {
112
- return (typeof value === "string" && value.startsWith("data:")) ||
113
- (value instanceof URL && value.protocol === "data:");
114
- };
115
-
116
- if (isDataURL(content)) {
117
- try {
118
- const dataUrl = content.toString();
119
- const [header] = dataUrl.split(",");
120
- const [typeSegment] = header.split(";");
121
- mediaType = typeSegment.replace("data:", "") || "text/plain";
122
- } catch (error) {
123
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Invalid data URL format"}));
124
- return;
125
- }
126
- }
127
-
128
- if (mediaType === undefined || mediaType === null || mediaType === "") {
129
- mediaType = "text/plain";
130
- }
131
-
132
- let mediaTypeObject;
133
-
134
- try {
135
- mediaTypeObject = new parseMediaType(mediaType);
136
- if (!(mediaTypeObject instanceof MediaType)) {
137
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Invalid MediaType"}));
138
- return;
139
- }
140
-
141
- } catch (error) {
142
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
143
- return
144
- }
145
-
146
- const checkRenderer = (renderer, contentType) => {
147
- if (renderers && typeof renderers[renderer] === "function") {
148
- return true;
149
- } else {
150
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: `Renderer for ${contentType} not found`}));
151
- return false;
152
- }
153
- }
154
-
155
- switch (mediaTypeObject.type) {
156
- case "text":
157
- switch (mediaTypeObject.subtype) {
158
- case "html":
159
- if (checkRenderer("html", mediaTypeObject.toString())) {
160
- renderers.html.call(this, content);
161
- }
162
- break;
163
- case "plain":
164
- if (checkRenderer("plaintext", mediaTypeObject.toString())) {
165
- renderers.plaintext.call(this, content);
166
- }
167
- break;
168
- case "markdown":
169
- if (checkRenderer("markdown", mediaTypeObject.toString())) {
170
- this.setMarkdown(content);
171
- }
172
- break;
173
- default:
174
- if (checkRenderer("plaintext", mediaTypeObject.toString())) {
175
- renderers.plaintext.call(this, content);
176
- }
177
- break;
178
- }
179
- break;
180
-
181
- case "application":
182
- switch (mediaTypeObject.subtype) {
183
- case "pdf":
184
- if (checkRenderer("pdf", mediaTypeObject.toString())) {
185
- renderers.pdf.call(this, content);
186
- }
187
- break;
188
- default:
189
- this.setOption("content", content);
190
- break;
191
- }
192
- break;
193
-
194
- case "audio":
195
- if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
196
- renderers[mediaTypeObject.type].call(this, content);
197
- }
198
- break;
199
-
200
- case "video":
201
- if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
202
- renderers[mediaTypeObject.type].call(this, content);
203
- }
204
- break;
205
-
206
- case "image":
207
- if (checkRenderer("image", mediaTypeObject.toString())) {
208
- renderers.image.call(this, content);
209
- }
210
- break;
211
-
212
- default:
213
- this.setOption("content", content);
214
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: `Unsupported media type: ${mediaTypeObject.toString()}`})); // Notify about unsupported media type
215
- return;
216
- }
217
- }
218
-
219
- /**
220
- * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it
221
- * to configure audio playback within the viewer. Throws an error if the input type is invalid.
222
- *
223
- * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string.
224
- * @return {void} No return value.
225
- */
226
- setAudio(data) {
227
- if (isBlob(data)) {
228
- data = URL.createObjectURL(data);
229
- } else if (isURL(data)) {
230
- // nothing to do
231
- } else if (isString(data)) {
232
- // nothing to do
233
- } else {
234
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Blob or URL expected"}));
235
- throw new Error("Blob or URL expected");
236
- }
237
-
238
- this.setOption(
239
- "content",
240
- `
51
+ /**
52
+ * This method is called by the `instanceof` operator.
53
+ * @return {symbol}
54
+ */
55
+ static get [instanceSymbol]() {
56
+ return Symbol.for("@schukai/monster/components/content/viewer@@instance");
57
+ }
58
+
59
+ /**
60
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
61
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
62
+ *
63
+ * The individual configuration values can be found in the table.
64
+ *
65
+ * @property {Object} templates Template definitions
66
+ * @property {string} templates.main Main template
67
+ * @property {string} content Content to be displayed in the viewer
68
+ * @property {Object} classes Css classes
69
+ * @property {string} classes.viewer Css class for the viewer
70
+ * @property {Object} renderers Renderers for different media types
71
+ * @property {function} renderers.image Function to render image content
72
+ * @property {function} renderers.html Function to render HTML content
73
+ * @property {function} renderers.pdf Function to render PDF content
74
+ * @property {function} renderers.plaintext Function to render plain text content
75
+ * @property {function} renderers.markdown Function to render Markdown content
76
+ */
77
+ get defaults() {
78
+ return Object.assign({}, super.defaults, {
79
+ templates: {
80
+ main: getTemplate(),
81
+ },
82
+ content: "<slot></slot>",
83
+ classes: {
84
+ viewer: "",
85
+ },
86
+ renderers: {
87
+ image: this.setImage,
88
+ html: this.setHTML,
89
+ pdf: this.setPDF,
90
+ plaintext: this.setPlainText,
91
+ markdown: this.setMarkdown,
92
+ audio: this.setAudio,
93
+ video: this.setVideo,
94
+ message: this.setMessage,
95
+ },
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Sets the content of an element based on the provided content and media type.
101
+ *
102
+ * @param {string} content - The content to be set.
103
+ * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified.
104
+ * @return {void} This method does not return a value.
105
+ * @throws {Error} Throws an error if shadowRoot is not defined.
106
+ */
107
+ setContent(content, mediaType = "text/plain") {
108
+ if (!this.shadowRoot) {
109
+ throw new Error("no shadow-root is defined");
110
+ }
111
+
112
+ const renderers = this.getOption("renderers");
113
+
114
+ const isDataURL = (value) => {
115
+ return (
116
+ (typeof value === "string" && value.startsWith("data:")) ||
117
+ (value instanceof URL && value.protocol === "data:")
118
+ );
119
+ };
120
+
121
+ if (isDataURL(content)) {
122
+ try {
123
+ const dataUrl = content.toString();
124
+ const [header] = dataUrl.split(",");
125
+ const [typeSegment] = header.split(";");
126
+ mediaType = typeSegment.replace("data:", "") || "text/plain";
127
+ } catch (error) {
128
+ this.dispatchEvent(
129
+ new CustomEvent("viewer-error", {
130
+ detail: "Invalid data URL format",
131
+ }),
132
+ );
133
+ return;
134
+ }
135
+ }
136
+
137
+ if (mediaType === undefined || mediaType === null || mediaType === "") {
138
+ mediaType = "text/plain";
139
+ }
140
+
141
+ let mediaTypeObject;
142
+
143
+ try {
144
+ mediaTypeObject = new parseMediaType(mediaType);
145
+ if (!(mediaTypeObject instanceof MediaType)) {
146
+ this.dispatchEvent(
147
+ new CustomEvent("viewer-error", { detail: "Invalid MediaType" }),
148
+ );
149
+ return;
150
+ }
151
+ } catch (error) {
152
+ this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
153
+ return;
154
+ }
155
+
156
+ const checkRenderer = (renderer, contentType) => {
157
+ if (renderers && typeof renderers[renderer] === "function") {
158
+ return true;
159
+ } else {
160
+ this.dispatchEvent(
161
+ new CustomEvent("viewer-error", {
162
+ detail: `Renderer for ${contentType} not found`,
163
+ }),
164
+ );
165
+ return false;
166
+ }
167
+ };
168
+
169
+ switch (mediaTypeObject.type) {
170
+ case "text":
171
+ switch (mediaTypeObject.subtype) {
172
+ case "html":
173
+ if (checkRenderer("html", mediaTypeObject.toString())) {
174
+ renderers.html.call(this, content);
175
+ }
176
+ break;
177
+ case "plain":
178
+ if (checkRenderer("plaintext", mediaTypeObject.toString())) {
179
+ renderers.plaintext.call(this, content);
180
+ }
181
+ break;
182
+ case "markdown":
183
+ if (checkRenderer("markdown", mediaTypeObject.toString())) {
184
+ this.setMarkdown(content);
185
+ }
186
+ break;
187
+ default:
188
+ if (checkRenderer("plaintext", mediaTypeObject.toString())) {
189
+ renderers.plaintext.call(this, content);
190
+ }
191
+ break;
192
+ }
193
+ break;
194
+
195
+ case "application":
196
+ switch (mediaTypeObject.subtype) {
197
+ case "pdf":
198
+ if (checkRenderer("pdf", mediaTypeObject.toString())) {
199
+ renderers.pdf.call(this, content);
200
+ }
201
+ break;
202
+
203
+ default:
204
+ this.setOption("content", content);
205
+ break;
206
+ }
207
+ break;
208
+
209
+ case "message":
210
+ switch (mediaTypeObject.subtype) {
211
+ case "rfc822":
212
+ if (checkRenderer("message", mediaTypeObject.toString())) {
213
+ renderers.message.call(this, content);
214
+ }
215
+ break;
216
+
217
+ default:
218
+ this.setOption("content", content);
219
+ break;
220
+ }
221
+ break;
222
+
223
+ case "audio":
224
+ if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
225
+ renderers[mediaTypeObject.type].call(this, content);
226
+ }
227
+ break;
228
+
229
+ case "video":
230
+ if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) {
231
+ renderers[mediaTypeObject.type].call(this, content);
232
+ }
233
+ break;
234
+
235
+ case "image":
236
+ if (checkRenderer("image", mediaTypeObject.toString())) {
237
+ renderers.image.call(this, content);
238
+ }
239
+ break;
240
+
241
+ default:
242
+ this.setOption("content", content);
243
+ this.dispatchEvent(
244
+ new CustomEvent("viewer-error", {
245
+ detail: `Unsupported media type: ${mediaTypeObject.toString()}`,
246
+ }),
247
+ ); // Notify about unsupported media type
248
+ return;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it
254
+ * to configure audio playback within the viewer. Throws an error if the input type is invalid.
255
+ *
256
+ * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string.
257
+ * @return {void} No return value.
258
+ */
259
+ setAudio(data) {
260
+ if (isBlob(data)) {
261
+ data = URL.createObjectURL(data);
262
+ } else if (isURL(data)) {
263
+ // nothing to do
264
+ } else if (isString(data)) {
265
+ // nothing to do
266
+ } else {
267
+ this.dispatchEvent(
268
+ new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
269
+ );
270
+ throw new Error("Blob or URL expected");
271
+ }
272
+
273
+ this.setOption(
274
+ "content",
275
+ `
241
276
  <audio controls part="audio" style="max-width: 100%">
242
277
  <source src="${data}">
243
- </audio>`
244
- );
245
- }
246
-
247
- /**
248
- * Sets the video content for the viewer. The method accepts a Blob, URL, or string,
249
- * verifies its type, and updates the viewer's content accordingly.
250
- *
251
- * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string.
252
- * @return {void} This method does not return a value. It updates the viewer's state.
253
- * @throws {Error} Throws an error if the provided data is not a Blob or URL.
254
- */
255
- setVideo(data) {
256
- if (isBlob(data)) {
257
- data = URL.createObjectURL(data);
258
- } else if (isURL(data)) {
259
- // nothing to do
260
- } else if (isString(data)) {
261
- // nothing to do
262
- } else {
263
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Blob or URL expected"}));
264
- throw new Error("Blob or URL expected");
265
- }
266
-
267
- this.setOption(
268
- "content",
269
- `
278
+ </audio>`,
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Sets the video content for the viewer. The method accepts a Blob, URL, or string,
284
+ * verifies its type, and updates the viewer's content accordingly.
285
+ *
286
+ * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string.
287
+ * @return {void} This method does not return a value. It updates the viewer's state.
288
+ * @throws {Error} Throws an error if the provided data is not a Blob or URL.
289
+ */
290
+ setVideo(data) {
291
+ if (isBlob(data)) {
292
+ data = URL.createObjectURL(data);
293
+ } else if (isURL(data)) {
294
+ // nothing to do
295
+ } else if (isString(data)) {
296
+ // nothing to do
297
+ } else {
298
+ this.dispatchEvent(
299
+ new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
300
+ );
301
+ throw new Error("Blob or URL expected");
302
+ }
303
+
304
+ this.setOption(
305
+ "content",
306
+ `
270
307
  <video controls part="video" style="max-width: 100%">
271
308
  <source src="${data}">
272
- </video>`
273
- );
274
- }
275
-
276
- /**
277
- * Renders Markdown content using built-in or custom Markdown parser.
278
- * Overrideable via `customRenderers['text/markdown']`.
279
- *
280
- * @param {string|Blob} data
281
- */
282
- setMarkdown(data) {
283
-
284
- if (isBlob(data)) {
285
- blobToText(data)
286
- .then((markdownText) => {
287
- try {
288
- const html = MarkdownToHTML.convert(markdownText);
289
- this.setHTML(html);
290
- } catch (error) {
291
- this.setPlainText(markdownText); // Fallback to plain text if conversion fails
292
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
293
- }
294
- })
295
- .catch((error) => {
296
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
297
- throw new Error(error);
298
- });
299
- return;
300
- } else if (isURL(data)) {
301
- getGlobal()
302
- .fetch(data)
303
- .then((response) => {
304
- return response.text();
305
- })
306
- .then((markdownText) => {
307
- try {
308
- const html = MarkdownToHTML.convert(markdownText);
309
- this.setHTML(html);
310
- } catch (error) {
311
- this.setPlainText(markdownText); // Fallback to plain text if conversion fails
312
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
313
- }
314
- })
315
- .catch((error) => {
316
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
317
- throw new Error(error);
318
- });
319
- return;
320
- } else if (isString(data)) {
321
- try {
322
- const html = MarkdownToHTML.convert(data);
323
- this.setHTML(html);
324
- } catch (error) {
325
- this.setPlainText(data); // Fallback to plain text if conversion fails
326
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
327
- }
328
- return;
329
- }
330
-
331
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Blob or string expected"}));
332
- throw new Error("Blob or string expected");
333
-
334
- }
335
-
336
- /**
337
- * Configures and embeds a PDF document into the application with customizable display settings.
338
- *
339
- * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string.
340
- * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
341
- * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
342
- * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
343
- * @return {void} This method returns nothing but sets the embedded PDF as the content.
344
- */
345
- setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
346
- const hashes =
347
- "#toolbar=" +
348
- (toolbar ? "1" : "0") +
349
- "&navpanes=" +
350
- (navigation ? "1" : "0") +
351
- "&scrollbar=" +
352
- (scrollbar ? "1" : "0");
353
-
354
- let pdfURL = "";
355
- if (isBlob(data)) {
356
- pdfURL = URL.createObjectURL(data);
357
- pdfURL += hashes;
358
- } else if (isURL(data)) {
359
- // check if the url already contains the hashes
360
- if (data?.hash?.indexOf("#") === -1) {
361
- pdfURL = data.toString() + hashes;
362
- } else {
363
- pdfURL = data.toString();
364
- }
365
- } else if (isString(data)) {
366
- //URL.createObjectURL(data);
367
- const blobObj = new Blob([atob(data)], {type: "application/pdf"});
368
- const url = window.URL.createObjectURL(blobObj);
369
-
370
- pdfURL = data;
371
- } else {
372
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Blob or URL expected"}));
373
- throw new Error("Blob or URL expected");
374
- }
375
-
376
- const html =
377
- '<object part="pdf" data="' +
378
- pdfURL +
379
- '" width="100%" height="100%" type="application/pdf"></object>';
380
-
381
- this.setOption("content", html);
382
- }
383
-
384
- /**
385
- * Sets an image for the target by accepting a blob, URL, or string representation of the image.
386
- *
387
- * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image.
388
- * @return {void} Does not return a value.
389
- */
390
- setImage(data) {
391
- if (isBlob(data)) {
392
- data = URL.createObjectURL(data);
393
- } else if (isURL(data)) {
394
- // nothing to do
395
- } else if (isString(data)) {
396
- // nothing to do
397
- } else {
398
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "Blob or URL expected"}));
399
- throw new Error("Blob or URL expected");
400
- }
401
-
402
- const onloaderror = `onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));"`;
403
-
404
- this.setOption(
405
- "content",
406
- `<img style="max-width: 100%" src="${data}" alt="image" part="image" ${onloaderror}>`
407
- );
408
- }
409
-
410
- /**
411
- *
412
- * if the data is a string, it is interpreted as HTML.
413
- * if the data is a URL, the HTML is loaded from the url and set as content.
414
- * if the data is an HTMLElement, the outerHTML is used as content.
415
- *
416
- * @param {HTMLElement|URL|string|Blob} data
417
- */
418
- setHTML(data) {
419
- if (data instanceof Blob) {
420
- blobToText(data)
421
- .then((html) => {
422
- this.setOption("content", html);
423
- })
424
- .catch((error) => {
425
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
426
- throw new Error(error);
427
- });
428
-
429
- return;
430
- } else if (data instanceof HTMLElement) {
431
- data = data.outerHTML;
432
- } else if (isString(data)) {
433
- // nothing to do
434
- } else if (isURL(data)) {
435
- // fetch element
436
- getGlobal()
437
- .fetch(data)
438
- .then((response) => {
439
- return response.text();
440
- })
441
- .then((html) => {
442
- this.setOption("content", html);
443
- })
444
- .catch((error) => {
445
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
446
- throw new Error(error);
447
- });
448
- } else {
449
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "HTMLElement or string expected"}));
450
- throw new Error("HTMLElement or string expected");
451
- }
452
-
453
- this.setOption("content", data);
454
- }
455
-
456
- /**
457
- * Sets the plain text content by processing the input data, which can be of various types, including Blob,
458
- * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option.
459
- *
460
- * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement,
461
- * a plain string, or a string formatted as a valid URL. The method determines
462
- * the data type and processes it accordingly.
463
- * @return {void} - This method does not return any value. It processes the content and updates the relevant option
464
- * property.
465
- */
466
- setPlainText(data) {
467
- const mkPreSpan = (text) => {
468
- const pre = document.createElement("pre");
469
- pre.innerText = text;
470
- pre.setAttribute("part", "text");
471
- return pre.outerHTML;
472
- };
473
-
474
- if (data instanceof Blob) {
475
- blobToText(data)
476
- .then((text) => {
477
- const div = document.createElement("div");
478
- div.innerHTML = test;
479
- text = div.innerText;
480
-
481
- this.setOption("content", mkPreSpan(text));
482
- })
483
- .catch((error) => {
484
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
485
- throw new Error(error);
486
- });
487
-
488
- return;
489
- } else if (data instanceof HTMLElement) {
490
- data = data.outerText;
491
- } else if (isString(data)) {
492
- const div = document.createElement("div");
493
- div.innerHTML = data;
494
- data = div.innerText;
495
- } else if (isURL(data)) {
496
- getGlobal()
497
- .fetch(data)
498
- .then((response) => {
499
- return response.text();
500
- })
501
- .then((text) => {
502
- const div = document.createElement("div");
503
- div.innerHTML = text;
504
- text = div.innerText;
505
-
506
- this.setOption("content", mkPreSpan(text));
507
- })
508
- .catch((error) => {
509
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: error}));
510
- throw new Error(error);
511
- });
512
- } else {
513
- this.dispatchEvent(new CustomEvent("viewer-error", {detail: "HTMLElement or string expected"}));
514
- throw new Error("HTMLElement or string expected");
515
- }
516
-
517
- this.setOption("content", mkPreSpan(data));
518
- }
519
-
520
- /**
521
- *
522
- * @return {Viewer}
523
- */
524
- [assembleMethodSymbol]() {
525
- super[assembleMethodSymbol]();
526
-
527
- initControlReferences.call(this);
528
- initEventHandler.call(this);
529
- }
530
-
531
- /**
532
- *
533
- * @return {string}
534
- */
535
- static getTag() {
536
- return "monster-viewer";
537
- }
538
-
539
- /**
540
- * @return {CSSStyleSheet[]}
541
- */
542
- static getCSSStyleSheet() {
543
- return [ViewerStyleSheet];
544
- }
309
+ </video>`,
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Renders Markdown content using built-in or custom Markdown parser.
315
+ * Overrideable via `customRenderers['text/markdown']`.
316
+ *
317
+ * @param {string|Blob} data
318
+ */
319
+ setMarkdown(data) {
320
+ if (isBlob(data)) {
321
+ blobToText(data)
322
+ .then((markdownText) => {
323
+ try {
324
+ const html = MarkdownToHTML.convert(markdownText);
325
+ this.setHTML(html);
326
+ } catch (error) {
327
+ this.setPlainText(markdownText); // Fallback to plain text if conversion fails
328
+ this.dispatchEvent(
329
+ new CustomEvent("viewer-error", { detail: error }),
330
+ );
331
+ }
332
+ })
333
+ .catch((error) => {
334
+ this.dispatchEvent(
335
+ new CustomEvent("viewer-error", { detail: error }),
336
+ );
337
+ throw new Error(error);
338
+ });
339
+ return;
340
+ } else if (isURL(data)) {
341
+ getGlobal()
342
+ .fetch(data)
343
+ .then((response) => {
344
+ return response.text();
345
+ })
346
+ .then((markdownText) => {
347
+ try {
348
+ const html = MarkdownToHTML.convert(markdownText);
349
+ this.setHTML(html);
350
+ } catch (error) {
351
+ this.setPlainText(markdownText); // Fallback to plain text if conversion fails
352
+ this.dispatchEvent(
353
+ new CustomEvent("viewer-error", { detail: error }),
354
+ );
355
+ }
356
+ })
357
+ .catch((error) => {
358
+ this.dispatchEvent(
359
+ new CustomEvent("viewer-error", { detail: error }),
360
+ );
361
+ throw new Error(error);
362
+ });
363
+ return;
364
+ } else if (isString(data)) {
365
+ try {
366
+ const html = MarkdownToHTML.convert(data);
367
+ this.setHTML(html);
368
+ } catch (error) {
369
+ this.setPlainText(data); // Fallback to plain text if conversion fails
370
+ this.dispatchEvent(new CustomEvent("viewer-error", { detail: error }));
371
+ }
372
+ return;
373
+ }
374
+
375
+ this.dispatchEvent(
376
+ new CustomEvent("viewer-error", { detail: "Blob or string expected" }),
377
+ );
378
+ throw new Error("Blob or string expected");
379
+ }
380
+
381
+ /**
382
+ * Configures and embeds a PDF document into the application with customizable display settings.
383
+ *
384
+ * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string.
385
+ * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer.
386
+ * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer.
387
+ * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer.
388
+ * @return {void} This method returns nothing but sets the embedded PDF as the content.
389
+ */
390
+ setPDF(data, navigation = true, toolbar = true, scrollbar = false) {
391
+ const hashes =
392
+ "#toolbar=" +
393
+ (toolbar ? "1" : "0") +
394
+ "&navpanes=" +
395
+ (navigation ? "1" : "0") +
396
+ "&scrollbar=" +
397
+ (scrollbar ? "1" : "0");
398
+
399
+ let pdfURL = "";
400
+ if (isBlob(data)) {
401
+ pdfURL = URL.createObjectURL(data);
402
+ pdfURL += hashes;
403
+ } else if (isURL(data)) {
404
+ // check if the url already contains the hashes
405
+ if (data?.hash?.indexOf("#") === -1) {
406
+ pdfURL = data.toString() + hashes;
407
+ } else {
408
+ pdfURL = data.toString();
409
+ }
410
+ } else if (isString(data)) {
411
+ //URL.createObjectURL(data);
412
+ const blobObj = new Blob([atob(data)], { type: "application/pdf" });
413
+ const url = window.URL.createObjectURL(blobObj);
414
+
415
+ pdfURL = data;
416
+ } else {
417
+ this.dispatchEvent(
418
+ new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
419
+ );
420
+ throw new Error("Blob or URL expected");
421
+ }
422
+
423
+ const html =
424
+ '<object part="pdf" data="' +
425
+ pdfURL +
426
+ '" width="100%" height="100%" type="application/pdf"></object>';
427
+
428
+ this.setOption("content", html);
429
+ }
430
+
431
+ /**
432
+ * Sets the content for displaying an email message.
433
+ * The data is expected to be an object with a structure containing
434
+ * 'to', 'from', 'subject', 'parts', and 'headers'.
435
+ * The parts are processed to display plain text and HTML in separate tabs,
436
+ * and attachments are listed.
437
+ *
438
+ * @param {object} emailData - The structured email data.
439
+ */
440
+ setMessage(emailData) {
441
+ if (!emailData || typeof emailData !== "object") {
442
+ this.dispatchEvent(
443
+ new CustomEvent("viewer-error", {
444
+ detail: "Invalid email data provided",
445
+ }),
446
+ );
447
+ return;
448
+ }
449
+
450
+ this.setOption(
451
+ "content",
452
+ '<monster-message-content part="message"></monster-message-content>',
453
+ );
454
+
455
+ setTimeout(() => {
456
+ const messageContent = this.shadowRoot.querySelector(
457
+ "monster-message-content",
458
+ );
459
+ if (!messageContent) {
460
+ this.dispatchEvent(
461
+ new CustomEvent("viewer-error", {
462
+ detail: "Message content element not found",
463
+ }),
464
+ );
465
+ return;
466
+ }
467
+
468
+ messageContent.setMessage(emailData);
469
+ }, 100);
470
+ }
471
+
472
+ /**
473
+ * Sets an image for the target by accepting a blob, URL, or string representation of the image.
474
+ *
475
+ * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image.
476
+ * @return {void} Does not return a value.
477
+ */
478
+ setImage(data) {
479
+ if (isBlob(data)) {
480
+ data = URL.createObjectURL(data);
481
+ } else if (isURL(data)) {
482
+ // nothing to do
483
+ } else if (isString(data)) {
484
+ // nothing to do
485
+ } else {
486
+ this.dispatchEvent(
487
+ new CustomEvent("viewer-error", { detail: "Blob or URL expected" }),
488
+ );
489
+ throw new Error("Blob or URL expected");
490
+ }
491
+
492
+ this.setOption(
493
+ "content",
494
+ `<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`,
495
+ );
496
+ }
497
+
498
+ /**
499
+ *
500
+ * if the data is a string, it is interpreted as HTML.
501
+ * if the data is a URL, the HTML is loaded from the url and set as content.
502
+ * if the data is an HTMLElement, the outerHTML is used as content.
503
+ *
504
+ * @param {HTMLElement|URL|string|Blob} data
505
+ */
506
+ setHTML(data) {
507
+ if (data instanceof Blob) {
508
+ blobToText(data)
509
+ .then((html) => {
510
+ this.setOption("content", html);
511
+ })
512
+ .catch((error) => {
513
+ this.dispatchEvent(
514
+ new CustomEvent("viewer-error", { detail: error }),
515
+ );
516
+ throw new Error(error);
517
+ });
518
+
519
+ return;
520
+ } else if (data instanceof HTMLElement) {
521
+ data = data.outerHTML;
522
+ } else if (isString(data)) {
523
+ // nothing to do
524
+ } else if (isURL(data)) {
525
+ // fetch element
526
+ getGlobal()
527
+ .fetch(data)
528
+ .then((response) => {
529
+ return response.text();
530
+ })
531
+ .then((html) => {
532
+ this.setOption("content", html);
533
+ })
534
+ .catch((error) => {
535
+ this.dispatchEvent(
536
+ new CustomEvent("viewer-error", { detail: error }),
537
+ );
538
+ throw new Error(error);
539
+ });
540
+ } else {
541
+ this.dispatchEvent(
542
+ new CustomEvent("viewer-error", {
543
+ detail: "HTMLElement or string expected",
544
+ }),
545
+ );
546
+ throw new Error("HTMLElement or string expected");
547
+ }
548
+
549
+ this.setOption("content", data);
550
+ }
551
+
552
+ /**
553
+ * Sets the plain text content by processing the input data, which can be of various types, including Blob,
554
+ * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option.
555
+ *
556
+ * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement,
557
+ * a plain string, or a string formatted as a valid URL. The method determines
558
+ * the data type and processes it accordingly.
559
+ * @return {void} - This method does not return any value. It processes the content and updates the relevant option
560
+ * property.
561
+ */
562
+ setPlainText(data) {
563
+ const mkPreSpan = (text) => {
564
+ const pre = document.createElement("pre");
565
+ pre.innerText = text;
566
+ pre.setAttribute("part", "text");
567
+ return pre.outerHTML;
568
+ };
569
+
570
+ if (data instanceof Blob) {
571
+ blobToText(data)
572
+ .then((text) => {
573
+ const div = document.createElement("div");
574
+ div.innerHTML = text;
575
+ text = div.innerText;
576
+
577
+ this.setOption("content", mkPreSpan(text));
578
+ })
579
+ .catch((error) => {
580
+ this.dispatchEvent(
581
+ new CustomEvent("viewer-error", { detail: error }),
582
+ );
583
+ throw new Error(error);
584
+ });
585
+
586
+ return;
587
+ } else if (data instanceof HTMLElement) {
588
+ data = data.outerText;
589
+ } else if (isString(data)) {
590
+ const div = document.createElement("div");
591
+ div.innerHTML = data;
592
+ data = div.innerText;
593
+ } else if (isURL(data)) {
594
+ getGlobal()
595
+ .fetch(data)
596
+ .then((response) => {
597
+ return response.text();
598
+ })
599
+ .then((text) => {
600
+ const div = document.createElement("div");
601
+ div.innerHTML = text;
602
+ text = div.innerText;
603
+
604
+ this.setOption("content", mkPreSpan(text));
605
+ })
606
+ .catch((error) => {
607
+ this.dispatchEvent(
608
+ new CustomEvent("viewer-error", { detail: error }),
609
+ );
610
+ throw new Error(error);
611
+ });
612
+ } else {
613
+ this.dispatchEvent(
614
+ new CustomEvent("viewer-error", {
615
+ detail: "HTMLElement or string expected",
616
+ }),
617
+ );
618
+ throw new Error("HTMLElement or string expected");
619
+ }
620
+
621
+ this.setOption("content", mkPreSpan(data));
622
+ }
623
+
624
+ /**
625
+ *
626
+ * @return {Viewer}
627
+ */
628
+ [assembleMethodSymbol]() {
629
+ super[assembleMethodSymbol]();
630
+
631
+ initControlReferences.call(this);
632
+ initEventHandler.call(this);
633
+ }
634
+
635
+ /**
636
+ *
637
+ * @return {string}
638
+ */
639
+ static getTag() {
640
+ return "monster-viewer";
641
+ }
642
+
643
+ /**
644
+ * @return {CSSStyleSheet[]}
645
+ */
646
+ static getCSSStyleSheet() {
647
+ return [ViewerStyleSheet];
648
+ }
545
649
  }
546
650
 
547
651
  /**
@@ -550,12 +654,12 @@ class Viewer extends CustomElement {
550
654
  * @return {boolean}
551
655
  */
552
656
  function isURL(variable) {
553
- try {
554
- new URL(variable);
555
- return true;
556
- } catch (error) {
557
- return false;
558
- }
657
+ try {
658
+ new URL(variable);
659
+ return true;
660
+ } catch (error) {
661
+ return false;
662
+ }
559
663
  }
560
664
 
561
665
  /**
@@ -564,7 +668,7 @@ function isURL(variable) {
564
668
  * @return {boolean}
565
669
  */
566
670
  function isBlob(variable) {
567
- return variable instanceof Blob;
671
+ return variable instanceof Blob;
568
672
  }
569
673
 
570
674
  /**
@@ -573,12 +677,12 @@ function isBlob(variable) {
573
677
  * @return {Promise<unknown>}
574
678
  */
575
679
  function blobToText(blob) {
576
- return new Promise((resolve, reject) => {
577
- const reader = new FileReader();
578
- reader.onloadend = () => resolve(reader.result);
579
- reader.onerror = reject;
580
- reader.readAsText(blob);
581
- });
680
+ return new Promise((resolve, reject) => {
681
+ const reader = new FileReader();
682
+ reader.onloadend = () => resolve(reader.result);
683
+ reader.onerror = reject;
684
+ reader.readAsText(blob);
685
+ });
582
686
  }
583
687
 
584
688
  /**
@@ -587,18 +691,18 @@ function blobToText(blob) {
587
691
  * @throws {Error} no shadow-root is defined
588
692
  */
589
693
  function initControlReferences() {
590
- if (!this.shadowRoot) {
591
- throw new Error("no shadow-root is defined");
592
- }
694
+ if (!this.shadowRoot) {
695
+ throw new Error("no shadow-root is defined");
696
+ }
593
697
 
594
- this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
698
+ this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer");
595
699
  }
596
700
 
597
701
  /**
598
702
  * @private
599
703
  */
600
704
  function initEventHandler() {
601
- return this;
705
+ return this;
602
706
  }
603
707
 
604
708
  /**
@@ -606,8 +710,8 @@ function initEventHandler() {
606
710
  * @return {string}
607
711
  */
608
712
  function getTemplate() {
609
- // language=HTML
610
- return `
713
+ // language=HTML
714
+ return `
611
715
  <div id="viewer" data-monster-role="viewer" part="viewer"
612
716
  data-monster-replace="path:content"
613
717
  data-monster-attributes="class path:classes.viewer">