From 71cb32c8830835951afbb561e5a63d71cb511a90 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Tue, 1 Jul 2025 12:20:21 +0200 Subject: [PATCH] fix: Improve code formatting and readability in viewer component - Reformatted import statements in viewer.mjs to maintain consistent spacing and style. - Enhanced method and comment formatting for better clarity and understanding throughout the Viewer class. - Unified the error handling and event dispatch mechanisms to streamline the process and improve maintainability. --- source/components/content/viewer.mjs | 1558 +++++++++++++------------- 1 file changed, 779 insertions(+), 779 deletions(-) diff --git a/source/components/content/viewer.mjs b/source/components/content/viewer.mjs index 9c6066a4..81ffc8a2 100644 --- a/source/components/content/viewer.mjs +++ b/source/components/content/viewer.mjs @@ -13,24 +13,24 @@ */ import { - assembleMethodSymbol, - CustomElement, - registerCustomElement, + assembleMethodSymbol, + CustomElement, + registerCustomElement, } from "../../dom/customelement.mjs"; import "../notify/notify.mjs"; -import {ViewerStyleSheet} from "./stylesheet/viewer.mjs"; -import {instanceSymbol} from "../../constants.mjs"; -import {isString} from "../../types/is.mjs"; -import {getGlobal} from "../../types/global.mjs"; -import {MediaType, parseMediaType} from "../../types/mediatype.mjs"; -import {MarkdownToHTML} from "../../text/markdown-parser.mjs"; +import { ViewerStyleSheet } from "./stylesheet/viewer.mjs"; +import { instanceSymbol } from "../../constants.mjs"; +import { isString } from "../../types/is.mjs"; +import { getGlobal } from "../../types/global.mjs"; +import { MediaType, parseMediaType } from "../../types/mediatype.mjs"; +import { MarkdownToHTML } from "../../text/markdown-parser.mjs"; import "../layout/tabs.mjs"; import "./viewer/message.mjs"; -import {getLocaleOfDocument} from "../../dom/locale.mjs"; -import {Button} from "../form/button.mjs"; -import {findTargetElementFromEvent} from "../../dom/events.mjs"; +import { getLocaleOfDocument } from "../../dom/locale.mjs"; +import { Button } from "../form/button.mjs"; +import { findTargetElementFromEvent } from "../../dom/events.mjs"; -export {Viewer}; +export { Viewer }; /** * @private @@ -51,657 +51,657 @@ const viewerElementSymbol = Symbol("viewerElement"); * @summary A simple viewer component for PDF, HTML, and images. */ class Viewer extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/content/viewer@@instance"); - } - - /** - * To set the options via the HTML tag, the attribute `data-monster-options` must be used. - * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} - * - * The individual configuration values can be found in the table. - * - * @property {Object} templates Template definitions - * @property {string} templates.main Main template - * @property {string} content Content to be displayed in the viewer - * @property {Object} classes Css classes - * @property {string} classes.viewer Css class for the viewer - * @property {Object} renderers Renderers for different media types - * @property {function} renderers.image Function to render image content - * @property {function} renderers.html Function to render HTML content - * @property {function} renderers.pdf Function to render PDF content - * @property {function} renderers.plaintext Function to render plain text content - * @property {function} renderers.markdown Function to render Markdown content - */ - get defaults() { - return Object.assign({}, super.defaults, { - templates: { - main: getTemplate(), - }, - content: "<slot></slot>", - classes: { - viewer: "", - }, - labels: getLabels(), - renderers: { - image: this.setImage, - html: this.setHTML, - pdf: this.setPDF, - download: this.setDownload, - plaintext: this.setPlainText, - markdown: this.setMarkdown, - audio: this.setAudio, - video: this.setVideo, - message: this.setMessage, - }, - - }); - } - - /** - * Sets the content of an element based on the provided content and media type. - * - * @param {string} content - The content to be set. - * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified. - * @return {void} This method does not return a value. - * @throws {Error} Throws an error if shadowRoot is not defined. - */ - setContent(content, mediaType = "text/plain") { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const renderers = this.getOption("renderers"); - - const isDataURL = (value) => { - return ( - (typeof value === "string" && value.startsWith("data:")) || - (value instanceof URL && value.protocol === "data:") - ); - }; - - if (isDataURL(content)) { - try { - const dataUrl = content.toString(); - const [header] = dataUrl.split(","); - const [typeSegment] = header.split(";"); - mediaType = typeSegment.replace("data:", "") || "text/plain"; - } catch (error) { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: "Invalid data URL format", - }), - ); - return; - } - } - - if (mediaType === undefined || mediaType === null || mediaType === "") { - mediaType = "text/plain"; - } - - let mediaTypeObject; - - try { - mediaTypeObject = new parseMediaType(mediaType); - if (!(mediaTypeObject instanceof MediaType)) { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Invalid MediaType"}), - ); - return; - } - } catch (error) { - this.dispatchEvent(new CustomEvent("viewer-error", {detail: error})); - return; - } - - const checkRenderer = (renderer, contentType) => { - if (renderers && typeof renderers[renderer] === "function") { - return true; - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: `Renderer for ${contentType} not found`, - }), - ); - return false; - } - }; - - switch (mediaTypeObject.type) { - case "text": - switch (mediaTypeObject.subtype) { - case "html": - if (checkRenderer("html", mediaTypeObject.toString())) { - renderers.html.call(this, content); - } - break; - case "plain": - if (checkRenderer("plaintext", mediaTypeObject.toString())) { - renderers.plaintext.call(this, content); - } - break; - case "markdown": - if (checkRenderer("markdown", mediaTypeObject.toString())) { - this.setMarkdown(content); - } - break; - default: - if (checkRenderer("plaintext", mediaTypeObject.toString())) { - renderers.plaintext.call(this, content); - } - break; - } - break; - - case "application": - switch (mediaTypeObject.subtype) { - case "pdf": - if (checkRenderer("pdf", mediaTypeObject.toString())) { - renderers.pdf.call(this, content); - } - break; - - default: - - // Handle octet-stream as a generic binary data type - if (checkRenderer("download", mediaTypeObject.toString())) { - renderers.download.call(this, content); - break; - } - - this.setOption("content", content + "!!"); - break; - } - break; - - case "message": - switch (mediaTypeObject.subtype) { - case "rfc822": - if (checkRenderer("message", mediaTypeObject.toString())) { - renderers.message.call(this, content); - } - break; - - default: - this.setOption("content", content); - break; - } - break; - - case "audio": - if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) { - renderers[mediaTypeObject.type].call(this, content); - } - break; - - case "video": - if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) { - renderers[mediaTypeObject.type].call(this, content); - } - break; - - case "image": - if (checkRenderer("image", mediaTypeObject.toString())) { - renderers.image.call(this, content); - } - break; - - default: - this.setOption("content", content); - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: `Unsupported media type: ${mediaTypeObject.toString()}`, - }), - ); // Notify about unsupported media type - return; - } - } - - /** - * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it - * to configure audio playback within the viewer. Throws an error if the input type is invalid. - * - * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string. - * @return {void} No return value. - */ - setAudio(data) { - if (isBlob(data)) { - data = URL.createObjectURL(data); - } else if (isURL(data)) { - // nothing to do - } else if (isString(data)) { - // nothing to do - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or URL expected"}), - ); - throw new Error("Blob or URL expected"); - } - - this.setOption( - "content", - ` + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/content/viewer@@instance"); + } + + /** + * To set the options via the HTML tag, the attribute `data-monster-options` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {Object} templates Template definitions + * @property {string} templates.main Main template + * @property {string} content Content to be displayed in the viewer + * @property {Object} classes Css classes + * @property {string} classes.viewer Css class for the viewer + * @property {Object} renderers Renderers for different media types + * @property {function} renderers.image Function to render image content + * @property {function} renderers.html Function to render HTML content + * @property {function} renderers.pdf Function to render PDF content + * @property {function} renderers.plaintext Function to render plain text content + * @property {function} renderers.markdown Function to render Markdown content + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + content: "<slot></slot>", + classes: { + viewer: "", + }, + labels: getLabels(), + renderers: { + image: this.setImage, + html: this.setHTML, + pdf: this.setPDF, + download: this.setDownload, + plaintext: this.setPlainText, + markdown: this.setMarkdown, + audio: this.setAudio, + video: this.setVideo, + message: this.setMessage, + }, + }); + } + + /** + * Sets the content of an element based on the provided content and media type. + * + * @param {string} content - The content to be set. + * @param {string} [mediaType="text/plain"] - The media type of the content. Defaults to "text/plain" if not specified. + * @return {void} This method does not return a value. + * @throws {Error} Throws an error if shadowRoot is not defined. + */ + setContent(content, mediaType = "text/plain") { + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + + const renderers = this.getOption("renderers"); + + const isDataURL = (value) => { + return ( + (typeof value === "string" && value.startsWith("data:")) || + (value instanceof URL && value.protocol === "data:") + ); + }; + + if (isDataURL(content)) { + try { + const dataUrl = content.toString(); + const [header] = dataUrl.split(","); + const [typeSegment] = header.split(";"); + mediaType = typeSegment.replace("data:", "") || "text/plain"; + } catch (error) { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: "Invalid data URL format", + }), + ); + return; + } + } + + if (mediaType === undefined || mediaType === null || mediaType === "") { + mediaType = "text/plain"; + } + + let mediaTypeObject; + + try { + mediaTypeObject = new parseMediaType(mediaType); + if (!(mediaTypeObject instanceof MediaType)) { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Invalid MediaType" }), + ); + return; + } + } catch (error) { + this.dispatchEvent(new CustomEvent("viewer-error", { detail: error })); + return; + } + + const checkRenderer = (renderer, contentType) => { + if (renderers && typeof renderers[renderer] === "function") { + return true; + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: `Renderer for ${contentType} not found`, + }), + ); + return false; + } + }; + + switch (mediaTypeObject.type) { + case "text": + switch (mediaTypeObject.subtype) { + case "html": + if (checkRenderer("html", mediaTypeObject.toString())) { + renderers.html.call(this, content); + } + break; + case "plain": + if (checkRenderer("plaintext", mediaTypeObject.toString())) { + renderers.plaintext.call(this, content); + } + break; + case "markdown": + if (checkRenderer("markdown", mediaTypeObject.toString())) { + this.setMarkdown(content); + } + break; + default: + if (checkRenderer("plaintext", mediaTypeObject.toString())) { + renderers.plaintext.call(this, content); + } + break; + } + break; + + case "application": + switch (mediaTypeObject.subtype) { + case "pdf": + if (checkRenderer("pdf", mediaTypeObject.toString())) { + renderers.pdf.call(this, content); + } + break; + + default: + // Handle octet-stream as a generic binary data type + if (checkRenderer("download", mediaTypeObject.toString())) { + renderers.download.call(this, content); + break; + } + + this.setOption("content", content); + break; + } + break; + + case "message": + switch (mediaTypeObject.subtype) { + case "rfc822": + if (checkRenderer("message", mediaTypeObject.toString())) { + renderers.message.call(this, content); + } + break; + + default: + this.setOption("content", content); + break; + } + break; + + case "audio": + if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) { + renderers[mediaTypeObject.type].call(this, content); + } + break; + + case "video": + if (checkRenderer(mediaTypeObject.type, mediaTypeObject.toString())) { + renderers[mediaTypeObject.type].call(this, content); + } + break; + + case "image": + if (checkRenderer("image", mediaTypeObject.toString())) { + renderers.image.call(this, content); + } + break; + + default: + this.setOption("content", content); + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: `Unsupported media type: ${mediaTypeObject.toString()}`, + }), + ); // Notify about unsupported media type + return; + } + } + + /** + * Sets the audio content for the viewer. Accepts a Blob, URL, or string and processes it + * to configure audio playback within the viewer. Throws an error if the input type is invalid. + * + * @param {Blob|string} data - The audio content. This can be a Blob, a URL, or a string. + * @return {void} No return value. + */ + setAudio(data) { + if (isBlob(data)) { + data = URL.createObjectURL(data); + } else if (isURL(data)) { + // nothing to do + } else if (isString(data)) { + // nothing to do + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or URL expected" }), + ); + throw new Error("Blob or URL expected"); + } + + this.setOption( + "content", + ` <audio controls part="audio" style="max-width: 100%"> <source src="${data}"> </audio>`, - ); - } - - /** - * Sets the video content for the viewer. The method accepts a Blob, URL, or string, - * verifies its type, and updates the viewer's content accordingly. - * - * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string. - * @return {void} This method does not return a value. It updates the viewer's state. - * @throws {Error} Throws an error if the provided data is not a Blob or URL. - */ - setVideo(data) { - if (isBlob(data)) { - data = URL.createObjectURL(data); - } else if (isURL(data)) { - // nothing to do - } else if (isString(data)) { - // nothing to do - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or URL expected"}), - ); - throw new Error("Blob or URL expected"); - } - - this.setOption( - "content", - ` + ); + } + + /** + * Sets the video content for the viewer. The method accepts a Blob, URL, or string, + * verifies its type, and updates the viewer's content accordingly. + * + * @param {Blob|string} data - The video data to set. It can be a Blob, URL, or string. + * @return {void} This method does not return a value. It updates the viewer's state. + * @throws {Error} Throws an error if the provided data is not a Blob or URL. + */ + setVideo(data) { + if (isBlob(data)) { + data = URL.createObjectURL(data); + } else if (isURL(data)) { + // nothing to do + } else if (isString(data)) { + // nothing to do + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or URL expected" }), + ); + throw new Error("Blob or URL expected"); + } + + this.setOption( + "content", + ` <video controls part="video" style="max-width: 100%"> <source src="${data}"> </video>`, - ); - } - - /** - * Renders Markdown content using built-in or custom Markdown parser. - * Overrideable via `customRenderers['text/markdown']`. - * - * @param {string|Blob} data - */ - setMarkdown(data) { - if (isBlob(data)) { - blobToText(data) - .then((markdownText) => { - try { - const html = MarkdownToHTML.convert(markdownText); - this.setHTML(html); - } catch (error) { - this.setPlainText(markdownText); // Fallback to plain text if conversion fails - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - } - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - return; - } else if (isURL(data)) { - getGlobal() - .fetch(data) - .then((response) => { - return response.text(); - }) - .then((markdownText) => { - try { - const html = MarkdownToHTML.convert(markdownText); - this.setHTML(html); - } catch (error) { - this.setPlainText(markdownText); // Fallback to plain text if conversion fails - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - } - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - return; - } else if (isString(data)) { - try { - const html = MarkdownToHTML.convert(data); - this.setHTML(html); - } catch (error) { - this.setPlainText(data); // Fallback to plain text if conversion fails - this.dispatchEvent(new CustomEvent("viewer-error", {detail: error})); - } - return; - } - - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or string expected"}), - ); - throw new Error("Blob or string expected"); - } - - /** - * Configures and embeds a PDF document into the application with customizable display settings. - * - * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string. - * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer. - * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer. - * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer. - * @return {void} This method returns nothing but sets the embedded PDF as the content. - */ - setPDF(data, navigation = true, toolbar = true, scrollbar = false) { - const hashes = - "#toolbar=" + - (toolbar ? "1" : "0") + - "&navpanes=" + - (navigation ? "1" : "0") + - "&scrollbar=" + - (scrollbar ? "1" : "0"); - - let pdfURL = ""; - if (isBlob(data)) { - pdfURL = URL.createObjectURL(data); - pdfURL += hashes; - } else if (isURL(data)) { - // check if the url already contains the hashes - if (data?.hash?.indexOf("#") === -1) { - pdfURL = data.toString() + hashes; - } else { - pdfURL = data.toString(); - } - } else if (isString(data)) { - //URL.createObjectURL(data); - const blobObj = new Blob([atob(data)], {type: "application/pdf"}); - const url = window.URL.createObjectURL(blobObj); - - pdfURL = data; - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or URL expected"}), - ); - throw new Error("Blob or URL expected"); - } - - const html = - '<object part="pdf" data="' + - pdfURL + - '" width="100%" height="100%" type="application/pdf"></object>'; - - this.setOption("content", html); - } - - /** - * Sets the download functionality for the viewer. - * @param data - * @param filename - */ - setDownload(data, filename = "download") { - - const rawData = data; - - if (isBlob(data)) { - data = URL.createObjectURL(data); - } else if (isURL(data)) { - // nothing to do - } else if (isString(data)) { - // nothing to do - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or URL expected"}), - ); - throw new Error("Blob or URL expected"); - } - - const button = `<monster-button data-monster-role="download">` + this.getOption('labels.download') + `</monster-button>`; - this.setOption("content", button); - - this.addEventListener("click", (event) => { - - const element = findTargetElementFromEvent(event, "data-monster-role", "download"); - if (element instanceof Button) { - const anchor = document.createElement("a"); - anchor.href = URL.createObjectURL(new Blob([rawData])) - anchor.download = filename; - anchor.style.display = "none"; - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); - } - - - }) - - } - - /** - * Sets the content for displaying an email message. - * The data is expected to be an object with a structure containing - * 'to', 'from', 'subject', 'parts', and 'headers'. - * The parts are processed to display plain text and HTML in separate tabs, - * and attachments are listed. - * - * @param {object} emailData - The structured email data. - */ - setMessage(emailData) { - if (!emailData || typeof emailData !== "object") { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: "Invalid email data provided", - }), - ); - return; - } - - this.setOption( - "content", - '<monster-message-content part="message"></monster-message-content>', - ); - - setTimeout(() => { - const messageContent = this.shadowRoot.querySelector( - "monster-message-content", - ); - if (!messageContent) { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: "Message content element not found", - }), - ); - return; - } - - messageContent.setMessage(emailData); - }, 100); - } - - /** - * Sets an image for the target by accepting a blob, URL, or string representation of the image. - * - * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image. - * @return {void} Does not return a value. - */ - setImage(data) { - if (isBlob(data)) { - data = URL.createObjectURL(data); - } else if (isURL(data)) { - // nothing to do - } else if (isString(data)) { - // nothing to do - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: "Blob or URL expected"}), - ); - throw new Error("Blob or URL expected"); - } - - this.setOption( - "content", - `<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`, - ); - } - - /** - * - * if the data is a string, it is interpreted as HTML. - * if the data is a URL, the HTML is loaded from the url and set as content. - * if the data is an HTMLElement, the outerHTML is used as content. - * - * @param {HTMLElement|URL|string|Blob} data - */ - setHTML(data) { - if (data instanceof Blob) { - blobToText(data) - .then((html) => { - this.setOption("content", html); - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - - return; - } else if (data instanceof HTMLElement) { - data = data.outerHTML; - } else if (isString(data)) { - // nothing to do - } else if (isURL(data)) { - // fetch element - getGlobal() - .fetch(data) - .then((response) => { - return response.text(); - }) - .then((html) => { - this.setOption("content", html); - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: "HTMLElement or string expected", - }), - ); - throw new Error("HTMLElement or string expected"); - } - - this.setOption("content", data); - } - - /** - * Sets the plain text content by processing the input data, which can be of various types, including Blob, - * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option. - * - * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement, - * a plain string, or a string formatted as a valid URL. The method determines - * the data type and processes it accordingly. - * @return {void} - This method does not return any value. It processes the content and updates the relevant option - * property. - */ - setPlainText(data) { - const mkPreSpan = (text) => { - const pre = document.createElement("pre"); - pre.innerText = text; - pre.setAttribute("part", "text"); - return pre.outerHTML; - }; - - if (data instanceof Blob) { - blobToText(data) - .then((text) => { - const div = document.createElement("div"); - div.innerHTML = text; - text = div.innerText; - - this.setOption("content", mkPreSpan(text)); - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - - return; - } else if (data instanceof HTMLElement) { - data = data.outerText; - } else if (isString(data)) { - const div = document.createElement("div"); - div.innerHTML = data; - data = div.innerText; - } else if (isURL(data)) { - getGlobal() - .fetch(data) - .then((response) => { - return response.text(); - }) - .then((text) => { - const div = document.createElement("div"); - div.innerHTML = text; - text = div.innerText; - - this.setOption("content", mkPreSpan(text)); - }) - .catch((error) => { - this.dispatchEvent( - new CustomEvent("viewer-error", {detail: error}), - ); - throw new Error(error); - }); - } else { - this.dispatchEvent( - new CustomEvent("viewer-error", { - detail: "HTMLElement or string expected", - }), - ); - throw new Error("HTMLElement or string expected"); - } - - this.setOption("content", mkPreSpan(data)); - } - - /** - * - * @return {Viewer} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initControlReferences.call(this); - initEventHandler.call(this); - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-viewer"; - } - - /** - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [ViewerStyleSheet]; - } + ); + } + + /** + * Renders Markdown content using built-in or custom Markdown parser. + * Overrideable via `customRenderers['text/markdown']`. + * + * @param {string|Blob} data + */ + setMarkdown(data) { + if (isBlob(data)) { + blobToText(data) + .then((markdownText) => { + try { + const html = MarkdownToHTML.convert(markdownText); + this.setHTML(html); + } catch (error) { + this.setPlainText(markdownText); // Fallback to plain text if conversion fails + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + } + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + return; + } else if (isURL(data)) { + getGlobal() + .fetch(data) + .then((response) => { + return response.text(); + }) + .then((markdownText) => { + try { + const html = MarkdownToHTML.convert(markdownText); + this.setHTML(html); + } catch (error) { + this.setPlainText(markdownText); // Fallback to plain text if conversion fails + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + } + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + return; + } else if (isString(data)) { + try { + const html = MarkdownToHTML.convert(data); + this.setHTML(html); + } catch (error) { + this.setPlainText(data); // Fallback to plain text if conversion fails + this.dispatchEvent(new CustomEvent("viewer-error", { detail: error })); + } + return; + } + + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or string expected" }), + ); + throw new Error("Blob or string expected"); + } + + /** + * Configures and embeds a PDF document into the application with customizable display settings. + * + * @param {Blob|URL|string} data The PDF data to be embedded. Can be provided as a Blob, URL, or base64 string. + * @param {boolean} [navigation=true] Determines whether the navigation pane is displayed in the PDF viewer. + * @param {boolean} [toolbar=true] Controls the visibility of the toolbar in the PDF viewer. + * @param {boolean} [scrollbar=false] Configures the display of the scrollbar in the PDF viewer. + * @return {void} This method returns nothing but sets the embedded PDF as the content. + */ + setPDF(data, navigation = true, toolbar = true, scrollbar = false) { + const hashes = + "#toolbar=" + + (toolbar ? "1" : "0") + + "&navpanes=" + + (navigation ? "1" : "0") + + "&scrollbar=" + + (scrollbar ? "1" : "0"); + + let pdfURL = ""; + if (isBlob(data)) { + pdfURL = URL.createObjectURL(data); + pdfURL += hashes; + } else if (isURL(data)) { + // check if the url already contains the hashes + if (data?.hash?.indexOf("#") === -1) { + pdfURL = data.toString() + hashes; + } else { + pdfURL = data.toString(); + } + } else if (isString(data)) { + //URL.createObjectURL(data); + const blobObj = new Blob([atob(data)], { type: "application/pdf" }); + const url = window.URL.createObjectURL(blobObj); + + pdfURL = data; + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or URL expected" }), + ); + throw new Error("Blob or URL expected"); + } + + const html = + '<object part="pdf" data="' + + pdfURL + + '" width="100%" height="100%" type="application/pdf"></object>'; + + this.setOption("content", html); + } + + /** + * Sets the download functionality for the viewer. + * @param data + * @param filename + */ + setDownload(data, filename = "download") { + const rawData = data; + + if (isBlob(data)) { + data = URL.createObjectURL(data); + } else if (isURL(data)) { + // nothing to do + } else if (isString(data)) { + // nothing to do + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or URL expected" }), + ); + throw new Error("Blob or URL expected"); + } + + const button = + `<monster-button data-monster-role="download">` + + this.getOption("labels.download") + + `</monster-button>`; + this.setOption("content", button); + + this.addEventListener("click", (event) => { + const element = findTargetElementFromEvent( + event, + "data-monster-role", + "download", + ); + if (element instanceof Button) { + const anchor = document.createElement("a"); + anchor.href = URL.createObjectURL(new Blob([rawData])); + anchor.download = filename; + anchor.style.display = "none"; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + } + }); + } + + /** + * Sets the content for displaying an email message. + * The data is expected to be an object with a structure containing + * 'to', 'from', 'subject', 'parts', and 'headers'. + * The parts are processed to display plain text and HTML in separate tabs, + * and attachments are listed. + * + * @param {object} emailData - The structured email data. + */ + setMessage(emailData) { + if (!emailData || typeof emailData !== "object") { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: "Invalid email data provided", + }), + ); + return; + } + + this.setOption( + "content", + '<monster-message-content part="message"></monster-message-content>', + ); + + setTimeout(() => { + const messageContent = this.shadowRoot.querySelector( + "monster-message-content", + ); + if (!messageContent) { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: "Message content element not found", + }), + ); + return; + } + + messageContent.setMessage(emailData); + }, 100); + } + + /** + * Sets an image for the target by accepting a blob, URL, or string representation of the image. + * + * @param {(Blob|string)} data - The image data, which can be a Blob, a valid URL, or a string representation of the image. + * @return {void} Does not return a value. + */ + setImage(data) { + if (isBlob(data)) { + data = URL.createObjectURL(data); + } else if (isURL(data)) { + // nothing to do + } else if (isString(data)) { + // nothing to do + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: "Blob or URL expected" }), + ); + throw new Error("Blob or URL expected"); + } + + this.setOption( + "content", + `<img style="max-width: 100%" src="${data}" alt="image" part="image" onerror="this.dispatchEvent(new CustomEvent('viewer-error', {detail: 'Image loading error'}));">`, + ); + } + + /** + * + * if the data is a string, it is interpreted as HTML. + * if the data is a URL, the HTML is loaded from the url and set as content. + * if the data is an HTMLElement, the outerHTML is used as content. + * + * @param {HTMLElement|URL|string|Blob} data + */ + setHTML(data) { + if (data instanceof Blob) { + blobToText(data) + .then((html) => { + this.setOption("content", html); + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + + return; + } else if (data instanceof HTMLElement) { + data = data.outerHTML; + } else if (isString(data)) { + // nothing to do + } else if (isURL(data)) { + // fetch element + getGlobal() + .fetch(data) + .then((response) => { + return response.text(); + }) + .then((html) => { + this.setOption("content", html); + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: "HTMLElement or string expected", + }), + ); + throw new Error("HTMLElement or string expected"); + } + + this.setOption("content", data); + } + + /** + * Sets the plain text content by processing the input data, which can be of various types, including Blob, + * HTMLElement, string, or a valid URL. The method extracts and sets the raw text content into a predefined option. + * + * @param {Blob|HTMLElement|string} data - The input data to be processed. It can be a Blob object, an HTMLElement, + * a plain string, or a string formatted as a valid URL. The method determines + * the data type and processes it accordingly. + * @return {void} - This method does not return any value. It processes the content and updates the relevant option + * property. + */ + setPlainText(data) { + const mkPreSpan = (text) => { + const pre = document.createElement("pre"); + pre.innerText = text; + pre.setAttribute("part", "text"); + return pre.outerHTML; + }; + + if (data instanceof Blob) { + blobToText(data) + .then((text) => { + const div = document.createElement("div"); + div.innerHTML = text; + text = div.innerText; + + this.setOption("content", mkPreSpan(text)); + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + + return; + } else if (data instanceof HTMLElement) { + data = data.outerText; + } else if (isString(data)) { + const div = document.createElement("div"); + div.innerHTML = data; + data = div.innerText; + } else if (isURL(data)) { + getGlobal() + .fetch(data) + .then((response) => { + return response.text(); + }) + .then((text) => { + const div = document.createElement("div"); + div.innerHTML = text; + text = div.innerText; + + this.setOption("content", mkPreSpan(text)); + }) + .catch((error) => { + this.dispatchEvent( + new CustomEvent("viewer-error", { detail: error }), + ); + throw new Error(error); + }); + } else { + this.dispatchEvent( + new CustomEvent("viewer-error", { + detail: "HTMLElement or string expected", + }), + ); + throw new Error("HTMLElement or string expected"); + } + + this.setOption("content", mkPreSpan(data)); + } + + /** + * + * @return {Viewer} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + initControlReferences.call(this); + initEventHandler.call(this); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-viewer"; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [ViewerStyleSheet]; + } } /** @@ -710,12 +710,12 @@ class Viewer extends CustomElement { * @return {boolean} */ function isURL(variable) { - try { - new URL(variable); - return true; - } catch (error) { - return false; - } + try { + new URL(variable); + return true; + } catch (error) { + return false; + } } /** @@ -724,7 +724,7 @@ function isURL(variable) { * @return {boolean} */ function isBlob(variable) { - return variable instanceof Blob; + return variable instanceof Blob; } /** @@ -733,12 +733,12 @@ function isBlob(variable) { * @return {Promise<unknown>} */ function blobToText(blob) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.onerror = reject; - reader.readAsText(blob); - }); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsText(blob); + }); } /** @@ -747,122 +747,122 @@ function blobToText(blob) { * @throws {Error} no shadow-root is defined */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } - this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer"); + this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer"); } /** * @private */ function initEventHandler() { - return this; + return this; } function getLabels() { - switch (getLocaleOfDocument().language) { - case "de": // German - return { - download: "Herunterladen", - }; - - case "es": // Spanish - return { - download: "Descargar", - }; - - case "zh": // Mandarin - return { - download: "下载", - }; - - case "hi": // Hindi - return { - download: "下载", - }; - - case "bn": // Bengali - return { - download: "ডাউনলোড", - }; - - case "pt": // Portuguese - return { - download: "Baixar", - }; - - case "ru": // Russian - return { - download: "Скачать", - }; - - case "ja": // Japanese - return { - download: "ダウンロード", - }; - - case "pa": // Western Punjabi - return { - download: "ਡਾਊਨਲੋਡ", - }; - - case "mr": // Marathi - return { - download: "डाउनलोड", - }; - - case "fr": // French - return { - download: "Télécharger", - }; - - case "it": // Italian - return { - download: "Scarica", - }; - - case "nl": // Dutch - return { - download: "Downloaden", - }; - - case "sv": // Swedish - return { - download: "Ladda ner", - }; - - case "pl": // Polish - return { - download: "Ściągnij", - }; - - case "da": // Danish - return { - download: "Lad ned", - }; - - case "fi": // Finnish - return { - download: "Lataa", - }; - - case "no": // Norwegian - return { - download: "Laste ned", - }; - - case "cs": // Czech - return { - download: "Stáhnout", - }; - - default: - return { - download: "Download", - }; - } + switch (getLocaleOfDocument().language) { + case "de": // German + return { + download: "Herunterladen", + }; + + case "es": // Spanish + return { + download: "Descargar", + }; + + case "zh": // Mandarin + return { + download: "下载", + }; + + case "hi": // Hindi + return { + download: "下载", + }; + + case "bn": // Bengali + return { + download: "ডাউনলোড", + }; + + case "pt": // Portuguese + return { + download: "Baixar", + }; + + case "ru": // Russian + return { + download: "Скачать", + }; + + case "ja": // Japanese + return { + download: "ダウンロード", + }; + + case "pa": // Western Punjabi + return { + download: "ਡਾਊਨਲੋਡ", + }; + + case "mr": // Marathi + return { + download: "डाउनलोड", + }; + + case "fr": // French + return { + download: "Télécharger", + }; + + case "it": // Italian + return { + download: "Scarica", + }; + + case "nl": // Dutch + return { + download: "Downloaden", + }; + + case "sv": // Swedish + return { + download: "Ladda ner", + }; + + case "pl": // Polish + return { + download: "Ściągnij", + }; + + case "da": // Danish + return { + download: "Lad ned", + }; + + case "fi": // Finnish + return { + download: "Lataa", + }; + + case "no": // Norwegian + return { + download: "Laste ned", + }; + + case "cs": // Czech + return { + download: "Stáhnout", + }; + + default: + return { + download: "Download", + }; + } } /** @@ -870,8 +870,8 @@ function getLabels() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div id="viewer" data-monster-role="viewer" part="viewer" data-monster-replace="path:content" data-monster-attributes="class path:classes.viewer"> -- GitLab