UNPKG

@schukai/monster

Version:

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

299 lines (266 loc) 8.02 kB
/** * Copyright © Volker Schukai 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 Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { assembleMethodSymbol, CustomElement, registerCustomElement, } from "../../dom/customelement.mjs"; import { findElementWithSelectorUpwards } from "../../dom/util.mjs"; import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; import { Datasource } from "./datasource.mjs"; import { SpinnerStyleSheet } from "../stylesheet/spinner.mjs"; import { isString } from "../../types/is.mjs"; import { instanceSymbol } from "../../constants.mjs"; import "../form/select.mjs"; import "./datasource/dom.mjs"; import "./datasource/rest.mjs"; import "../form/popper.mjs"; import "../form/context-error.mjs"; import { StatusStyleSheet } from "./stylesheet/status.mjs"; import { Formatter } from "../../text/formatter.mjs"; export { DatasourceStatus }; /** * @private * @type {symbol} */ const errorElementSymbol = Symbol.for("errorElement"); /** * @private * @type {symbol} */ const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement"); const spinnerElementSymbol = Symbol("spinnerElement"); /** * A simple dataset status component * * @fragments /fragments/components/datatable/datasource-status * * @example /examples/components/datatable/datasource-status-simple Simple dataset status * * @issue https://localhost.alvine.dev:8440/development/issues/closed/274.html * * @copyright Volker Schukai * @summary The Status component is used to show the current status of a datasource. */ class DatasourceStatus extends CustomElement { /** */ constructor() { super(); } /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/datatable/status@@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 {Object} datasource Datasource configuration * @property {string} datasource.selector The selector of the datasource * @property {Object} callbacks Callbacks * @property {Function} callbacks.onError Callback function for error handling <code>function(message: string, event: Event): string</code> * @property {Object} timeouts Timeouts * @property {number} timeouts.message Timeout for the message * @property {Object} state State */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, datasource: { selector: null, }, callbacks: { onError: null, }, timeouts: { message: 4000, spinnerMin: 200, }, state: { spinner: "hide", }, }); } /** * * @return {string} */ static getTag() { return "monster-datasource-status"; } /** * @private */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); } /** * * @param message * @param timeout * @returns {DatasourceStatus} */ setErrorMessage(message, timeout) { this[errorElementSymbol].setErrorMessage(message, timeout); return this; } /** * * @return [CSSStyleSheet] */ static getCSSStyleSheet() { return [StatusStyleSheet, SpinnerStyleSheet, ThemeStyleSheet]; } } /** * @private * @return {Select} * @throws {Error} no shadow-root is defined */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[errorElementSymbol] = this.shadowRoot.querySelector( "monster-context-error", ); this[spinnerElementSymbol] = this.shadowRoot.querySelector(".monster-spinner"); } /** * @private */ function initEventHandler() { const selector = this.getOption("datasource.selector", ""); const self = this; if (isString(selector)) { const element = findElementWithSelectorUpwards(this, selector); if (element === null) { throw new Error("the selector must match exactly one element"); } if (!(element instanceof Datasource)) { throw new TypeError("the element must be a datasource"); } let hideTimer = null; let lastShowAt = 0; let requestVersion = 0; const setSpinnerState = (state) => { self.setOption("state.spinner", state); const spinner = self[spinnerElementSymbol]; if (spinner) { spinner.setAttribute("data-monster-state-loader", state); } }; const clearHideTimer = () => { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }; const getSpinnerMinTimeout = () => { const value = Number(self.getOption("timeouts.spinnerMin", 0)); return Number.isFinite(value) ? Math.max(0, value) : 0; }; const scheduleHide = (version) => { clearHideTimer(); const elapsed = Date.now() - lastShowAt; const delay = Math.max(0, getSpinnerMinTimeout() - elapsed); hideTimer = setTimeout(() => { hideTimer = null; if (version !== requestVersion) { return; } setSpinnerState("hide"); }, delay); }; const hideSpinner = () => { setSpinnerState("hide"); }; const showSpinner = () => { setSpinnerState("show"); }; this[datasourceLinkedElementSymbol] = element; hideSpinner(); element.addEventListener("monster-datasource-fetched", function () { if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") { self[errorElementSymbol].resetErrorMessage(); } scheduleHide(requestVersion); }); element.addEventListener("monster-datasource-fetch", function () { requestVersion += 1; lastShowAt = Date.now(); clearHideTimer(); if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") { self[errorElementSymbol].resetErrorMessage(); } showSpinner(); }); element.addEventListener("monster-datasource-error", function (event) { scheduleHide(requestVersion); const timeout = self.getOption("timeouts.message", 4000); let msg = "Cannot load data"; try { if (event.detail.error instanceof Error) { msg = event.detail.error.message; } else if (event.detail.error instanceof Object) { msg = JSON.stringify(event.detail.error); } else if (event.detail.error instanceof String) { msg = event.detail.error; } else if (event.detail.error instanceof Number) { msg = event.detail.error.toString(); } else { msg = event.detail.error; } } catch (e) { } finally { const callback = self.getOption("callbacks.onError", null); if (callback) { callback.call(self, msg, event); } else { self[errorElementSymbol].setErrorMessage(msg, timeout); } } }); } } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control" data-monster-attributes="disabled path:disabled | if:true"> <monster-context-error data-monster-option-classes-button="monster-theme-error-2 monster-theme-background-inherit"></monster-context-error> <div class="monster-spinner" data-monster-attributes="data-monster-state-loader path:state.spinner"></div> </div> `; } registerCustomElement(DatasourceStatus);