diff --git a/development/issues/closed/309.mjs b/development/issues/closed/309.mjs index 31890c133475a945dbb7ac2f77d1de5036adfb3a..a873a39da87b15d4dced94e1e656958e2b38b6c9 100644 --- a/development/issues/closed/309.mjs +++ b/development/issues/closed/309.mjs @@ -14,6 +14,7 @@ import "../../../source/components/style/typography.pcss"; import "../../../source/components/content/viewer.mjs"; + const v = document.getElementById("mainer"); console.log(v) // @@ -33,6 +34,10 @@ console.log(v) // const url = new URL(`data:text/markdown;base64,${btoa(markdownText)}`); // v.setContent(url, "text/markdown", "base64"); +const content = "zpl-demo" +v.setContent(content, "application/oct33et-stream", "base64"); + + //const url = new URL("data:text/plain;base64,SGVsbG8gV29ybGQh"); // base64 encoded string for "Hello World!" //v.setContent(url, "text/plain", "base64"); @@ -83,10 +88,10 @@ console.log(v) // Simple MP3 audio file encoded in base64 // Minimal base64-encoded test video // Base64 encoded simple ping sound (44.1kHz, 16-bit, mono) -const miniAudioBase64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjIwLjEwMAAAAAAAAAAAAAAA//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA////////////AYCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID////////////////////////8AAAAZTGF2YzU4LjM1AAAAAAAAAAAAAAAAJAYAAAAAAAAAAnHyhqwaAAAAAAD/+7DEAAAHHA8iABBwALkNH1AiMmYiIn+JwQgkCYP4fwfBAMcEAQDgQg/B8//y4Pv//gg/+CAIcHwQDg+D4IAAAAD4IBgYGFAIUC4Pg+CAIAgCAIAuD//8HwQf//Ag/YEAwgwLhQDAoGIAz+MUzmX2bOuGicww1HUErAYcESGDJEKeLWg0Kw4EhwsShwWFDBgNB0BQoDgqGEwOAwIAYEQ0mOlI1JyBJI4/k6lQqEBYYgw8QkQzGAmPmyMpGR6RHCUBDgCGg4FwsJBYLBYBQ0RQGi0iJCwvNBdKnR4bwEXFxcXBMLhYLhcKhUJhITExISScTSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//sQxNYDwAABpBwAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="; - -const url = new URL(`data:audio/mp3;base64,${miniAudioBase64}`); -v.setContent(url, "audio/mp3", "base64"); +// const miniAudioBase64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjIwLjEwMAAAAAAAAAAAAAAA//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA////////////AYCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID////////////////////////8AAAAZTGF2YzU4LjM1AAAAAAAAAAAAAAAAJAYAAAAAAAAAAnHyhqwaAAAAAAD/+7DEAAAHHA8iABBwALkNH1AiMmYiIn+JwQgkCYP4fwfBAMcEAQDgQg/B8//y4Pv//gg/+CAIcHwQDg+D4IAAAAD4IBgYGFAIUC4Pg+CAIAgCAIAuD//8HwQf//Ag/YEAwgwLhQDAoGIAz+MUzmX2bOuGicww1HUErAYcESGDJEKeLWg0Kw4EhwsShwWFDBgNB0BQoDgqGEwOAwIAYEQ0mOlI1JyBJI4/k6lQqEBYYgw8QkQzGAmPmyMpGR6RHCUBDgCGg4FwsJBYLBYBQ0RQGi0iJCwvNBdKnR4bwEXFxcXBMLhYLhcKhUJhITExISScTSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//sQxNYDwAABpBwAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="; +// +// const url = new URL(`data:audio/mp3;base64,${miniAudioBase64}`); +// v.setContent(url, "audio/mp3", "base64"); // const minimalVideoBase64 = "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACoAYF//+c3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0MiByMjQ3OSBkZDc5YTYxIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTYgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAKYWVpegAVpD//+e5VrHBPqGP9IjQ1ErQWdyI5BQAAAAMAAAMAAANiAAA/AAADAAA6xQS1AAFr6v7oCKDAAAADOwAAAwAAAwB7AAA9AAAAAwAACywAAAAABowDGBHE84cFC6BnGJObADwQ6JCSNs5wOEMFLFNXBALRqRK33vh/+kRBagRa/qE/YqRQ+kRCAUW1ZUB/q2Qb+kR2uP8I/0h2vxj5oJAxM94/hYtpIFoAAAADAAADAAlYABO0AAADAAADABRWN3xTBGgTAAAABgAAAwUYLJwAAQBxtSxpJ1CZT4dCZ/6RUgRo9IiAAAAABgCAAAAXc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAABAsAAAACAAAAFHN0Y28AAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHNzAAAAAAAAAAEAAAABAAAAEHNzaWEAAAAAABBzc2QAAAAAAAAAAQAAAA8AAAACbWRpYQAAAABtZGhkAAAAABdoZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAmxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAIsc3RibAAAALRzdHNkAAAAAAAAAAEAAACkYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAEYAcAASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADNhdmNDAWQACv/hABlnZAAKrNlCmXv4wRAAAAAMAEAAAAMBA8SJmQEABGjxIf+AAAEaAAAADCBAKG5iKQABAAVo8SH/gAABGgAAAA5AQChOYilIAAAGaPEh/4AAARoAAAAPAEAoTmIpAAAAGj8IIAIQEBYQAAAAGHhwYWMAAAAAAAAAAgAAAAEAABAOAAAAABxhdmMxAAAAAAAAAgAAAAEAABAsAAAAAAEYAXAAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAzYXZjQwFkAAr/4QAZZmQACqzZQpl7+MEQAAAACXwAAAgIDC9mAAABGjxIf+AAAEaAAAAMEEAobmIpAAEABWjxIf+AAAEaAAAADkBAKE5iKUgAAAZo8SH/gAABGgAAAA8AQChOYikAAAAaPwggAhAQFhAAAAAYeHBhYwAAAAAAAAACAAAAAwAAEA4AAAAAHGaDLZVzJGhE"; // diff --git a/source/components/content/viewer.mjs b/source/components/content/viewer.mjs index 039ae12227fe8fa7c7c5d669cd968137c6c6b787..9c6066a488b2933fc683075014ae7018047c4441 100644 --- a/source/components/content/viewer.mjs +++ b/source/components/content/viewer.mjs @@ -13,21 +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"; -export { Viewer }; +export {Viewer}; /** * @private @@ -48,604 +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: "", - }, - renderers: { - image: this.setImage, - html: this.setHTML, - pdf: this.setPDF, - 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: - 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 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]; + } } /** @@ -654,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; + } } /** @@ -668,7 +724,7 @@ function isURL(variable) { * @return {boolean} */ function isBlob(variable) { - return variable instanceof Blob; + return variable instanceof Blob; } /** @@ -677,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); + }); } /** @@ -691,18 +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", + }; + } } /** @@ -710,8 +870,8 @@ function initEventHandler() { * @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">