Skip to content
Snippets Groups Projects
Select Git revision
  • ba4ce5b99c3136e97f35a8e65adccc286db7244b
  • master default protected
  • 1.31
  • 4.24.3
  • 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
23 results

log.mjs

Blame
  • api-bar.mjs 17.56 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.
     */
    
    import {instanceSymbol} from "../../constants.mjs";
    import {addAttributeToken} from "../../dom/attributes.mjs";
    import {
        ATTRIBUTE_ERRORMESSAGE,
        ATTRIBUTE_ROLE,
    } from "../../dom/constants.mjs";
    import {
        assembleMethodSymbol,
        registerCustomElement,
    } from "../../dom/customelement.mjs";
    
    import {isArray, isFunction, isString, isIterable, isObject, isPrimitive} from "../../types/is.mjs";
    import {fireCustomEvent} from "../../dom/events.mjs";
    import {ButtonBar} from "./button-bar.mjs";
    import {validateString} from "../../types/validate.mjs";
    import {Pathfinder} from "../../data/pathfinder.mjs";
    import {buildMap} from "../../data/buildmap.mjs";
    import {ApiButtonStyleSheet} from "./stylesheet/api-button.mjs";
    import {Formatter} from "../../text/formatter.mjs";
    import {getGlobal} from "../../types/global.mjs";
    
    import "./button.mjs";
    import "./message-state-button.mjs";
    import "./state-button.mjs";
    import {MessageStateButton} from "./message-state-button.mjs";
    import {StateButton} from "./state-button.mjs";
    
    export {ApiBar};
    
    
    /**
     * A ApiBar
     *
     * @fragments /fragments/components/form/api-bar/
     *
     * @example /examples/components/form/api-bar-simple
     *
     * @since 3.90.0
     * @copyright schukai GmbH
     * @summary A beautiful ApiBar that can make your life easier and also looks good.
     */
    class ApiBar extends ButtonBar {
        /**
         * This method is called by the `instanceof` operator.
         * @returns {symbol}
         */
        static get [instanceSymbol]() {
            return Symbol.for("@schukai/monster/components/form/api-bar@@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 key to find the api value in the response.
         * @property {string} mapping.urlTemplate - The key to find the url value in the response, if empty the api value is used.
         * @property {function} mapping.filter - The filter function to filter the buttons.
         * @property {string} url - The url to fetch the data.
         * @property {string} buttonTag - The tag name of the button
         * @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.beforeApiCall - The beforeApiCall callback called before the api request is made.
         * @property {function} callbacks.failedApiCall - The failedApiCall callback called when the api request failed.
         * @property {function} callbacks.successfulApiCall - The successfulApiCall callback called when the api request was successful.
         * @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 acceptance option.
         * @property {object} actions - The actions object.
         * @property {function} actions.execute - The execute action.
         * @property {object} data - The data object, this can be used to store some data and send it with the request.
         * @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: null,
                },
                url: "",
                buttonTag: "monster-message-state-button",
                callbacks: {
                    beforeApiCal: null,
                    failedApiCall: null,
                    successfulApiCall: null,
                },
                fetch: {
                    redirect: "error",
                    method: "GET",
                    mode: "same-origin",
                    credentials: "same-origin",
                    headers: {
                        accept: "application/json",
                    },
                },
                actions: {
                    execute: executeAPIButton
                },
                data: null
            });
    
            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 {ApiButton}
         * @throws {Error} map is not iterable
         * @throws {Error} missing label configuration
         */
        importButtons(data) {
    
            const self = this;
    
            const currentButtons = self.querySelectorAll(`[${ATTRIBUTE_ROLE}="api-button"]`);
            for (const btnElement of currentButtons) {
                btnElement.remove();
            }
    
            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, check the error attribute");
            }
    
            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");
            }
    
            const buttonTag = this.getOption("buttonTag");
            const executerCallback = this.getOption("actions.execute");
    
            for (const [iterKey] of urlMap) {
                const vmUrl = urlMap.get(iterKey);
                const vmLabel = labelMap.get(iterKey);
    
                const button = getGlobal().document.createElement(buttonTag);
                button.setAttribute(ATTRIBUTE_ROLE, `api-button`);
                button.setOption("labels.button", vmLabel);
                button.setOption("actions.click", (event) => {
    
                    if (isFunction(executerCallback)) {
    
                        executerCallback.call(this, event, {
                            key: iterKey,
                            url: vmUrl,
                            label: vmLabel,
                            button: button,
                        })
    
                    }
                });
    
                this.appendChild(button);
            }
    
            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 {void}
         */
        [assembleMethodSymbol]() {
            super[assembleMethodSymbol]();
        }
    
        /**
         * @return {string}
         */
        static getTag() {
            return "monster-api-bar";
        }
    
        /**
         * @return {Array<CSSStyleSheet>}
         */
        static getCSSStyleSheet() {
            const styles = super.getCSSStyleSheet();
            styles.push(ApiButtonStyleSheet);
            return styles;
        }
    }
    
    /**
     *
     * @param {Event} event
     * @param {object} opts
     */
    function executeAPIButton(event, opts) {
        const self = this;
    
        if (!isObject(opts)) {
            opts = {};
        }
    
        const button = opts?.["button"];
        const fetchOptions = self.getOption("api.fetch", {});
    
        const callback = self.getOption("callbacks.beforeApiCall");
        if (isFunction(callback)) {
            callback.call(self, fetchOptions);
        }
    
        const successfulApiCall = self.getOption("callbacks.successfulApiCall");
        const failedApiCall = self.getOption("callbacks.failedApiCall");
    
        let url = opts?.["url"];
        let label = opts?.["label"];
        let key = opts?.["key"];
    
        let body = self.getOption("api.body");
    
        if (isString(body)) {
            try {
                body = JSON.parse(body);
            } catch (e) {
                body = {};
            }
        }
    
        if (isObject(body)) {
            const bodyString = JSON.stringify(body);
    
            const obj = {
                url: url,
                label: label,
                key: key,
                data: self.getOption("data"),
            };
    
            fetchOptions.body = new Formatter(obj, {}).format(bodyString);
        }
    
        if (button instanceof HTMLElement) {
            button?.setState("activity");
        }
    
        fireCustomEvent(self, "monster-api-bar-click", {
            button,
        });
    
        const global = getGlobal();
        global
            .fetch(url, fetchOptions)
            .then((response) => {
    
                if (!response.ok) {
                    if (button instanceof MessageStateButton || button instanceof StateButton) {
                        button.setState("failed", 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 MessageStateButton || button instanceof StateButton) {
                                    button.setState("successful", 4000);
                                }
    
                                fireCustomEvent(self, "monster-api-bar-successful", {
                                    button,
                                    data,
                                    response,
                                    contentType: response.headers.get("Content-Type"),
                                });
    
                                if (isFunction(successfulApiCall)) {
                                    successfulApiCall.call(self, data, response);
                                }
    
                            } catch (error) {
                                if (button instanceof HTMLElement) {
                                    button.setState("failed", 4000);
                                    button.setMessage(error.message).showMessage(2000);
                                }
    
                                fireCustomEvent(self, "monster-api-bar-failed", {
                                    button,
                                    error,
                                    response,
                                    contentType: response.headers.get("Content-Type"),
                                });
                            }
                        })
                        .catch((error) => {
    
                            button.setState("failed", 4000);
    
                            if (isFunction(failedApiCall)) {
                                failedApiCall.call(self, error, response);
                            } else if (button instanceof MessageStateButton || button instanceof StateButton) {
                                button.setMessage("request failed").showMessage(2000);
                            }
    
                            fireCustomEvent(self, "monster-api-bar-failed", {
                                button,
                                error,
                                response,
                                contentType: response.headers.get("Content-Type"),
                            });
                        });
                } else {
                    return response
                        .blob()
                        .then((data) => {
    
                            if (button instanceof MessageStateButton || button instanceof StateButton) {
                                button.setState("successful", 4000);
                            }
    
                            fireCustomEvent(self, "monster-api-bar-successful", {
                                button,
                                data,
                                response,
                                contentType: response.headers.get("Content-Type"),
                            });
    
                            if (isFunction(successfulApiCall)) {
                                successfulApiCall.call(self, data, response);
                            }
    
                        })
                        .catch((error) => {
    
                            if (button instanceof MessageStateButton || button instanceof StateButton) {
                                button.setState("failed", 4000);
                            }
    
                            if (isFunction(failedApiCall)) {
                                failedApiCall.call(self, error, response);
                            } else if (button instanceof MessageStateButton || button instanceof StateButton) {
    
                                if (error instanceof Response) {
                                    error = new Error(error.statusText);
                                }
    
                                button.setMessage("request failed").showMessage(2000);
                            }
    
                            fireCustomEvent(self, "monster-api-bar-failed", {
                                button,
                                error,
                                response,
                                contentType: response.headers.get("Content-Type"),
                            });
                        });
                }
            })
            .catch((error) => {
    
                if (button instanceof MessageStateButton || button instanceof StateButton) {
                    button.setState("failed", 4000);
                }
    
                if (isFunction(failedApiCall)) {
                    failedApiCall.call(self, button, error, response);
                } else if (button instanceof MessageStateButton || button instanceof StateButton) {
    
                    if (error instanceof Response) {
                        error = new Error(error.statusText);
                    }
    
                    button?.setMessage(error.message).showMessage(3000);
                }
    
                fireCustomEvent(self, "monster-api-bar-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");
                }
            });
    }
    
    registerCustomElement(ApiBar);