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