@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
399 lines (360 loc) • 10.4 kB
JavaScript
/**
* 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);