UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

399 lines (360 loc) 10.4 kB
/** * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact schukai GmbH. * * SPDX-License-Identifier: AGPL-3.0 */ import { 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"; export { Viewer }; /** * @private * @type {symbol} */ const viewerElementSymbol = Symbol("viewerElement"); /** * The Viewer component is used to show a PDF, HTML or Image. * * @fragments /fragments/components/content/viewer * * @example /examples/components/content/pdf-viewer with a PDF * @example /examples/components/content/image-viewer with an image * @example /examples/components/content/html-viewer with HTML content * * @copyright schukai GmbH * @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 */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, content: "<slot></slot>", classes: { viewer: "", }, }); } /** * 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"); } let type; try { const m = new parseMediaType(mediaType); switch (m.type) { case "image": return this.setImage(content); } mediaType = m.toString(); } catch (error) { type = null; } if (mediaType === undefined || mediaType === null || mediaType === "") { mediaType = "text/plain"; } switch (mediaType) { case "text/html": this.setHTML(content); break; case "text/plain": this.setPlainText(content); break; case "application/pdf": this.setPDF(content); break; case "image/png": case "image/jpeg": case "image/gif": this.setImage(content); break; default: this.setOption("content", content); } } /** * 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 { 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 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 { throw new Error("Blob or URL expected"); } this.setOption("content", '<img src="' + data + '" alt="image" />'); } /** * * if the data is a string, it is interpreted as HTML. * if the data is an 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) => { 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) => { throw new Error(error); }); } else { 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 = test; text = div.innerText; this.setOption("content", mkPreSpan(text)); }) .catch((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) => { throw new Error(error); }); } else { 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]; } } /** * @private * @param variable * @return {boolean} */ function isURL(variable) { try { new URL(variable); return true; } catch (error) { return false; } } /** * @private * @param variable * @return {boolean} */ function isBlob(variable) { return variable instanceof Blob; } /** * @private * @param blob * @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); }); } /** * @private * @return {Select} * @throws {Error} no shadow-root is defined */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[viewerElementSymbol] = this.shadowRoot.getElementById("viewer"); } /** * @private */ function initEventHandler() { return this; } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div id="viewer" data-monster-role="viewer" part="viewer" data-monster-replace="path:content" data-monster-attributes="class path:classes.viewer"> </div>`; } registerCustomElement(Viewer);