UNPKG

@schukai/monster

Version:

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

311 lines (278 loc) 8.24 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 { instanceSymbol } from "../../constants.mjs"; import { addAttributeToken } from "../../dom/attributes.mjs"; import { ATTRIBUTE_ERRORMESSAGE, ATTRIBUTE_ROLE, } from "../../dom/constants.mjs"; import { assembleMethodSymbol, attributeObserverSymbol, CustomElement, initMethodSymbol, registerCustomElement, } from "../../dom/customelement.mjs"; import { isString } from "../../types/is.mjs"; import { ATTRIBUTE_FORM_RELOAD, ATTRIBUTE_FORM_URL } from "./constants.mjs"; import { loadAndAssignContent } from "./util/fetch.mjs"; import { addErrorAttribute } from "../../dom/error.mjs"; export { Reload }; /** * @private * @type {symbol} */ const intersectionObserverWasInitialized = Symbol("wasInitialized"); /** * This CustomControl reloads the content of an url and embeds it into the dom. * * @fragments /fragments/components/form/reload/ * * @example /examples/components/form/reload-simple * * @since 1.13.0 * @copyright Volker Schukai * @summary A beautiful reload control * @fires monster-fetched */ class Reload extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} * @since 2.1.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/form/reload"); } /** * 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} url url to fetch * @property {string} reload onshow, always (onshow is default and means that the content is loaded when the element is visible, always means that the content is always loaded) * @property {string} filter css selector * @property {Object} fetch fetch options for the request * @property {string} fetch.redirect error, follow, manual * @property {string} fetch.method GET, POST, PUT, DELETE * @property {string} fetch.mode same-origin, cors, no-cors, navigate * @property {string} fetch.credentials omit, same-origin, include * @property {Object} fetch.headers * @property {string} fetch.headers.accept text/html, application/json */ get defaults() { return Object.assign( {}, super.defaults, { templates: { main: getTemplate.call(this), }, shadowMode: false, url: null, reload: "onshow", filter: null, fetch: { redirect: "error", method: "GET", mode: "same-origin", credentials: "same-origin", headers: { accept: "text/html", }, }, }, initOptionsFromArguments.call(this), ); } /** * This method determines which attributes are to be monitored by `attributeChangedCallback()`. * * @return {string[]} */ static get observedAttributes() { const list = super.observedAttributes; list.push(ATTRIBUTE_FORM_URL); return list; } /** * @return {void} */ [initMethodSymbol]() { super[initMethodSymbol](); // data-monster-options this[attributeObserverSymbol][ATTRIBUTE_FORM_URL] = (url) => { if (this.hasAttribute(ATTRIBUTE_FORM_URL)) { this.setOption("url", new URL(url, document.location).toString()); } else { this.setOption("url", undefined); } }; } /** * This method is called internal and should not be called directly. * @throws {Error} missing default slot * @throws {Error} no shadow-root is defined * @throws {Error} missing url * @throws {Error} we won't be able to read the data * @throws {Error} request failed * @throws {Error} not found * @throws {Error} undefined status or type * @fires monster-fetched * @return {Monster.Components.Form.Form} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initIntersectionObserver.call(this); } /** * This method is called internal and should not be called directly. * * @return {string} */ static getTag() { return "monster-reload"; } /** * load content from url * * It is important to know that with this function the loading is executed * directly. it is loaded as well when the element is not visible. * * @param {string|undefined} url */ fetch(url) { if (isString(url) || url instanceof URL) { this.setAttribute(ATTRIBUTE_FORM_URL, `${url}`); } try { return loadContent.call(this); } catch (e) { addErrorAttribute(this, e); return Promise.reject(e); } } } /** * @private * @return {object} */ function initOptionsFromArguments() { const options = {}; const url = this.getAttribute(ATTRIBUTE_FORM_URL); if (isString(url)) { options["url"] = new URL(url, document.location).toString(); } if (this.hasAttribute(ATTRIBUTE_FORM_RELOAD)) { options["reload"] = this.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase(); } return options; } /** * @private * @throws {Error} missing default slot * @throws {Error} no shadow-root is defined * @throws {Error} missing url * @throws {Error} we won't be able to read the data * @throws {Error} request failed * @throws {Error} not found * @throws {Error} undefined status or type * @fires monster-fetched */ function initIntersectionObserver() { if (this[intersectionObserverWasInitialized] === true) { return; } this[intersectionObserverWasInitialized] = true; const options = { threshold: [0.5], }; const callback = (entries, observer) => { for (const [, entry] of entries.entries()) { if (entry.isIntersecting === true) { // undefined or always do the same if (this.getOption("reload") === "onshow") { observer.disconnect(); } try { loadContent.call(this).catch((e) => { addErrorAttribute(this, e); }); } catch (e) { addErrorAttribute(this, e); } } } }; const observer = new IntersectionObserver(callback, options); observer.observe(this); } /** * @private * @throws {Error} missing default slot * @throws {Error} no shadow-root is defined * @throws {Error} missing url * @throws {Error} we won't be able to read the data * @throws {Error} request failed * @throws {Error} not found * @throws {Error} undefined status or type * @throws {Error} client error * @throws {Error} undefined status or type * @throws {TypeError} value is not an instance of * @throws {TypeError} value is not a string * @fires monster-fetched * @return {Promise} */ function loadContent() { const url = this.getOption("url", undefined); if (!isString(url) || url === "") { throw new Error("missing url"); } const options = this.getOption("fetch", {}); let parentNode = this; if (this.shadowRoot) { parentNode = this.shadowRoot; } let container = parentNode.querySelector(`[${ATTRIBUTE_ROLE}=container]`); let currentDisplayMode = container?.style?.display; if (currentDisplayMode === undefined) { currentDisplayMode = "inherit"; } if (!(container instanceof HTMLElement)) { container = document.createElement("div"); container.style.display = "none"; container.setAttribute(ATTRIBUTE_ROLE, "container"); parentNode.appendChild(container); } return loadAndAssignContent(container, url, options, this.getOption("filter")) .then(() => { if (currentDisplayMode !== undefined) { container.style.display = currentDisplayMode; } }) .catch((e) => { addErrorAttribute(this, e); }); } /** * @private * @return {string} */ function getTemplate() { return this.innerHTML; } registerCustomElement(Reload);