Skip to content
Snippets Groups Projects
Select Git revision
  • ab6e8c26bde583bbca87f30c6a9ad4bf1f33bc3a
  • master default protected
  • 1.31
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
  • 4.17.0
23 results

global.html#Monster

Blame
  • api-button.mjs 13.79 KiB
    /**
     * 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 { instanceSymbol } from "../../constants.mjs";
    import { buildMap } from "../../data/buildmap.mjs";
    import { Pathfinder } from "../../data/pathfinder.mjs";
    import { addAttributeToken } from "../../dom/attributes.mjs";
    import {
    	ATTRIBUTE_ERRORMESSAGE,
    	ATTRIBUTE_ROLE,
    } from "../../dom/constants.mjs";
    import {
    	assembleMethodSymbol,
    	registerCustomElement,
    } from "../../dom/customelement.mjs";
    import {
    	findTargetElementFromEvent,
    	fireCustomEvent,
    } from "../../dom/events.mjs";
    import { isArray, isPrimitive, isIterable } from "../../types/is.mjs";
    import { validateString } from "../../types/validate.mjs";
    import { ActionButton } from "./action-button.mjs";
    import { ApiButtonStyleSheet } from "./stylesheet/api-button.mjs";
    import { isObject, isFunction } from "../../types/is.mjs";
    import { getGlobal } from "../../types/global.mjs";
    import { Formatter } from "../../text/formatter.mjs";
    
    export { ApiButton };
    
    /**
     * @private
     * @type {symbol}
     */
    const containerElementSymbol = Symbol("containerElement");
    
    /**
     * The ApiButton is a button that opens a popper element with possible actions.
     *
     * <img src="./images/api-button.png">
     *
     * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
     * as well as [pooperjs](https://popper.js.org/docs/v2/).
     *
     * You can create this control either by specifying the HTML tag <monster-action-button />` directly in the HTML or using
     * Javascript via the `document.createElement('monster-action-button');` method.
     *
     * ```html
     * <monster-action-button></monster-action-button>
     * ```
     *
     * Or you can create this CustomControl directly in Javascript:
     *
     * ```js
     * import {PopperButton} from '@schukai/component-form/source/action-button.js';
     * document.createElement('monster-action-button');
     * ```
     *
     * The `data-monster-button-class` attribute can be used to change the CSS class of the button.
     *
     * @startuml api-button.png
     * skinparam monochrome true
     * skinparam shadowing false
     * HTMLElement <|-- CustomElement
     * CustomElement <|-- CustomControl
     * CustomControl <|-- Button
     * Button <|-- ActionButton
     * ActionButton <|-- ApiButton
     * @enduml
     *
     * @copyright schukai GmbH
     * @summary A button that opens a popper element with possible actions.
     */
    
    /**
     * An API button with icons
     *
     * @fragments /fragments/components/form/api-button/
     *
     * @example /examples/components/form/api-button-simple
     *
     * @since 3.32.0
     * @copyright schukai GmbH
     * @summary A api button control
     * @fires monster-button-set
     * @fires monster-api-button-click
     * @fires monster-api-button-successful
     * @fires monster-api-button-failed
     */
    class ApiButton extends ActionButton {
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @return {symbol}
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for("@schukai/monster/components/form/api-button@@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} mapping - The mapping object.
    	 * @property {string} mapping.selector - The selector to find the buttons in the response.
    	 * @property {string} mapping.labelSelector - The selector to find the label for the button.
    	 * @property {string} mapping.labelTemplate - The template to create the label for the button.
    	 * @property {string} mapping.apiTemplate - The template to create the api for the button.
    	 * @property {string} mapping.urlTemplate - The template to create the url for the button.
    	 * @property {function} mapping.filter - The filter function to filter the buttons.
    	 * @property {string} url - The url to fetch the data.
    	 * @property {object} api - The api options.
    	 * @property {object} api.fetch - The fetch options.
    	 * @property {string} api.body - The body template.
    	 * @property {object} callbacks - The callbacks object.
    	 * @property {function} callbacks.beforeApi - The beforeApi callback.
    	 * @property {object} fetch - The fetch options.
    	 * @property {string} fetch.redirect - The redirect option.
    	 * @property {string} fetch.method - The method option.
    	 * @property {string} fetch.mode - The mode option.
    	 * @property {string} fetch.credentials - The credentials option.
    	 * @property {object} fetch.headers - The headers option.
    	 * @property {string} fetch.headers.accept - The accept option.
    	 * @extends {ActionButton.defaults}
    	 */
    	get defaults() {
    		const opts = Object.assign({}, super.defaults, {
    			mapping: {
    				selector: "*",
    				labelSelector: "",
    				labelTemplate: "",
    				apiTemplate: "",
    				urlTemplate: "",
    				filter: "",
    			},
    			api: {
    				fetch: {
    					method: "POST",
    					redirect: "error",
    					mode: "same-origin",
    					credentials: "same-origin",
    					headers: {
    						accept: "application/json",
    					},
    				},
    				body: {},
    			},
    			url: "",
    			callbacks: {
    				beforeApi: null,
    			},
    			fetch: {
    				redirect: "error",
    				method: "GET",
    				mode: "same-origin",
    				credentials: "same-origin",
    				headers: {
    					accept: "application/json",
    				},
    			},
    		});
    
    		opts["actions"]["execute"] = executeAPIButton.bind(self);
    
    		return opts;
    	}
    
    	/**
    	 *
    	 * @return {Promise}
    	 */
    	fetch(url) {
    		if (url instanceof URL) {
    			url = url.toString();
    		}
    
    		if (url !== undefined) {
    			url = validateString(url);
    		}
    
    		return fetchData.call(this, url).then((map) => {
    			if (
    				isObject(map) ||
    				isArray(map) | (map instanceof Set) ||
    				map instanceof Map
    			) {
    				this.importButtons(map);
    			}
    		});
    	}
    
    	/**
    	 * Import buttons from a map.
    	 *
    	 * @param {array|object|Map|Set} data
    	 * @return {Monster.Components.Form.ApiButton}
    	 * @throws {Error} map is not iterable
    	 * @throws {Error} missing label configuration
    	 */
    	importButtons(data) {
    		const mappingOptions = this.getOption("mapping", {});
    		const selector = mappingOptions?.["selector"];
    		const labelSelector = mappingOptions?.["labelSelector"];
    		const labelTemplate = mappingOptions?.["labelTemplate"];
    		const apiTemplate = mappingOptions?.["apiTemplate"];
    		let urlTemplate = mappingOptions?.["urlTemplate"];
    		const filter = mappingOptions?.["filter"];
    
    		let flag = false;
    		let apiEqualUrl = false;
    		if (labelTemplate === "") {
    			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template");
    			flag = true;
    		}
    
    		if (apiTemplate === "") {
    			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty api template");
    			flag = true;
    		}
    
    		if (urlTemplate === "") {
    			urlTemplate = apiTemplate;
    			apiEqualUrl = true;
    		}
    
    		if (flag === true) {
    			throw new Error("missing label or api configuration");
    		}
    
    		if (isPrimitive(labelSelector) && labelSelector !== "") {
    			const finder = new Pathfinder(data);
    			const label = finder.getVia(labelSelector);
    			this.setOption("labels.button", label);
    			this.value = label;
    		}
    
    		let labelMap;
    		const urlMap = buildMap(data, selector, urlTemplate, apiTemplate, filter);
    		if (apiEqualUrl === true) {
    			labelMap = urlMap;
    		} else {
    			labelMap = buildMap(data, selector, labelTemplate, apiTemplate, filter);
    		}
    
    		const buttons = [];
    		if (!isIterable(urlMap)) {
    			throw new Error("map is not iterable");
    		}
    
    		for (const [iterKey] of urlMap) {
    			const vmUrl = urlMap.get(iterKey);
    			const vmLabel = labelMap.get(iterKey);
    			buttons.push({
    				label: vmLabel,
    				class: "monster-button-outline-primary monster-border-0",
    				action: this.getOption("actions.execute"),
    				url: vmUrl,
    				cmd: iterKey,
    			});
    		}
    
    		try {
    			this.updateI18n();
    		} catch (e) {
    			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
    		}
    
    		this.setOption("buttons", buttons);
    
    		fireCustomEvent(this, "monster-button-set", {
    			buttons: buttons,
    		});
    
    		return this;
    	}
    
    	/**
    	 *
    	 * @return {Monster.Components.Form.Popper}
    	 */
    	[assembleMethodSymbol]() {
    		super[assembleMethodSymbol]();
    		initControlReferences.call(this);
    		initEventHandler.call(this);
    
    		return this;
    	}
    
    	/**
    	 * @return {string}
    	 */
    	static getTag() {
    		return "monster-api-button";
    	}
    
    	/**
    	 * @return {Array<CSSStyleSheet>}
    	 */
    	static getCSSStyleSheet() {
    		const styles = super.getCSSStyleSheet();
    		styles.push(ApiButtonStyleSheet);
    		return styles;
    	}
    }
    
    /**
     *
     * @param {Event} event
     * @param {HTMLElement} button
     * @param {Monster.Components.Form.ApiButton} element
     */
    function executeAPIButton(event, button, element) {
    	const self = element;
    
    	const fetchOptions = self.getOption("api.fetch", {});
    
    	const callback = self.getOption("callbacks.beforeApi");
    	if (isFunction(callback)) {
    		callback.call(self, fetchOptions);
    	}
    
    	let url = undefined;
    	let label = undefined;
    	let key = undefined;
    
    	const attr = button.getAttribute("data-monster-insert-reference");
    	if (attr) {
    		const index = attr.split("-")[1];
    		const b = self.getOption("buttons." + index);
    
    		url = b?.["url"];
    		label = b?.["label"];
    		key = b?.["cmd"];
    	}
    
    	const body = self.getOption("api.body");
    	if (isObject(body)) {
    		const bodyString = JSON.stringify(body);
    
    		const obj = {
    			url: url,
    			label: label,
    			value: self.getOption("value"),
    			key: key,
    			id: self.getOption("id"),
    		};
    
    		fetchOptions.body = new Formatter(obj, {}).format(bodyString);
    	}
    
    	if (button instanceof HTMLElement) {
    		button.setState("activity");
    	}
    
    	fireCustomEvent(self, "monster-api-button-click", {
    		button,
    	});
    
    	const global = getGlobal();
    	global
    		.fetch(url, fetchOptions)
    		.then((response) => {
    			if (!response.ok) {
    				if (button instanceof HTMLElement) {
    					button.setState("successful", 4000);
    				}
    				return Promise.reject(response);
    			}
    
    			const contentType = response?.headers?.get("content-type");
    			if (contentType && contentType.indexOf("application/json") !== -1) {
    				return response
    					.text()
    					.then((text) => {
    						try {
    							const data = JSON.parse(text); // Try to parse the response as JSON
    
    							if (button instanceof HTMLElement) {
    								button.setState("successful", 4000);
    							}
    
    							fireCustomEvent(self, "monster-api-button-successful", {
    								button,
    								data,
    								response,
    								contentType: response.headers.get("Content-Type"),
    							});
    						} catch (error) {
    							if (button instanceof HTMLElement) {
    								button.setState("failed", 4000);
    								button.setMessage(error.message).showMessage(2000);
    							}
    
    							fireCustomEvent(self, "monster-api-button-failed", {
    								button,
    								error,
    								response,
    								contentType: response.headers.get("Content-Type"),
    							});
    						}
    					})
    					.catch((error) => {
    						if (button instanceof HTMLElement) {
    							button.setState("failed", 4000);
    							button.setMessage("request failed").showMessage(2000);
    						}
    
    						fireCustomEvent(self, "monster-api-button-failed", {
    							button,
    							error,
    							response,
    							contentType: response.headers.get("Content-Type"),
    						});
    					});
    			} else {
    				return response
    					.blob()
    					.then((data) => {
    						fireCustomEvent(self, "monster-api-button-successful", {
    							button,
    							data,
    							response,
    							contentType: response.headers.get("Content-Type"),
    						});
    					})
    					.catch((error) => {
    						if (button instanceof HTMLElement) {
    							button.setState("failed", 4000);
    							button.setMessage("request failed").showMessage(2000);
    						}
    
    						fireCustomEvent(self, "monster-api-button-failed", {
    							button,
    							error,
    							response,
    							contentType: response.headers.get("Content-Type"),
    						});
    					});
    			}
    		})
    		.catch((error) => {
    			if (button instanceof HTMLElement) {
    				button.setState("failed", 4000);
    				button.setMessage(error.message).showMessage(2000);
    			}
    
    			fireCustomEvent(self, "monster-api-button-failed", {
    				button,
    				error,
    			});
    		});
    }
    
    /**
     * @private
     * @param {string} url
     * @return {Promise}
     * @throws {TypeError} the result cannot be parsed
     * @throws {TypeError} unsupported response
     */
    function fetchData(url) {
    	if (!url) url = this.getOption("url");
    	if (!url) return Promise.resolve();
    
    	const fetchOptions = this.getOption("fetch", {});
    
    	const global = getGlobal();
    	return global
    		.fetch(url, fetchOptions)
    		.then((response) => {
    			const contentType = response.headers.get("content-type");
    			if (contentType && contentType.indexOf("application/json") !== -1) {
    				return response.text();
    			}
    
    			throw new TypeError(`unsupported response ${contentType}`);
    		})
    		.then((text) => {
    			try {
    				return Promise.resolve(JSON.parse(text));
    			} catch (e) {
    				throw new TypeError("the result cannot be parsed");
    			}
    		});
    }
    
    /**
     * @private
     * @return {Monster.Components.Form.Popper}
     */
    function initEventHandler() {
    	this[containerElementSymbol].addEventListener("click", (event) => {
    		const element = findTargetElementFromEvent(
    			event,
    			"data-monster-insert-reference",
    		);
    		const attr = element.getAttribute("data-monster-insert-reference");
    		if (attr) {
    			const index = attr.split("-")[1];
    			const b = this.getOption("buttons." + index);
    			if (isObject(b) && isFunction(b?.action)) {
    				b.action(event, element, this);
    			}
    		}
    	});
    
    	return this;
    }
    
    /**
     * @private
     * @return {Monster.Components.Form.ApiButton}
     */
    function initControlReferences() {
    	this[containerElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}=container]`,
    	);
    	return this;
    }
    
    registerCustomElement(ApiButton);