Skip to content
Snippets Groups Projects
Select Git revision
  • e7d3c49cd98e46fc84204c03ea7564b8117f2fb3
  • master default protected
  • 1.31
  • 4.38.3
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
23 results

reload.mjs

Blame
  • reload.mjs 9.57 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 { 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";
    
    export { Reload };
    
    /**
     * @private
     * @type {symbol}
     */
    const intersectionObserverWasInitialized = Symbol("wasInitialized");
    
    /**
     * This CustomControl reloads the content of a url and embeds it into the dom.
     *
     * <img src="./images/reload.png">
     *
     * You can create this control either by specifying the HTML tag `<monster-reload />` directly in the HTML or using
     * Javascript via the `document.createElement('monster-reload');` method.
     *
     * ```html
     * <monster-reload></monster-reload>
     *
     * <script type="module">
     * import {Reload} from '@schukai/component-form/source/reload.js';
     * document.createElement('monster-reload');
     * </script>
     * ```
     *
     * A simple configuration can look like this
     *
     * ```html
     * <script id="config"
     *         type="application/json">
     *     {
     *         "url": "./content.html",
     *     }
     * </script>
     *
     * <monster-reload data-monster-options-selector="#config">
     * </monster-reload>
     * ```
     *
     * If you want to display a loader, you can insert a div with the attribute `data-monster-role="container"`.
     * The content of this div will be replaced by the loaded code.
     *
     * ```html
     * <monster-reload data-monster-options-selector="#config">
     * <div data-monster-role="container">
     * LOADER ...
     * </div>
     * </monster-reload>
     * ```
     *
     * If you need additional structure, you can simply specify it.
     *
     * ```html
     * <monster-reload data-monster-options-selector="#config">
     * <div class="row">
     *   <div class="col" data-monster-role="container">
     *       LOADER ...
     *   </div>
     * </div>
     * </monster-reload>
     * ```
     *
     * @fragments /fragments/components/form/reload/
     *
     * @example /examples/components/form/reload-simple
     *
     * @since 1.13.0
     * @copyright schukai GmbH
     * @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=undefined
    	 * @property {string} reload=undefined currently the values defined are `onshow` and `always`. The default `onshow` removes the IntersectionObserver. This means that the content is only loaded once. reloading of the content does not occur.
    	 * @property {string} filter=undefined dom selectors to search for elements, if undefined then everything is taken
    	 * @property {Monster.Components.Form.Processor[]} processors
    	 * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
    	 * @property {String} fetch.redirect=error
    	 * @property {String} fetch.method=GET
    	 * @property {String} fetch.mode=same-origin
    	 * @property {String} fetch.credentials=same-origin
    	 * @property {Object} fetch.headers={"accept":"text/html"}}
    	 */
    	get defaults() {
    		return Object.assign(
    			{},
    			super.defaults,
    			{
    				templates: {
    					main: getTemplate.call(this),
    				},
    				shadowMode: null,
    				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}`);
    		}
    
    		return loadContent.call(this);
    	}
    }
    
    /**
     * This attribute can be used to pass a URL to this select.
     *
     * ```
     * <monster-select data-monster-url="https://example.com/"></monster-select>
     * ```
     *
     * @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) => {
    						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString());
    					});
    				} catch (e) {
    					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString());
    				}
    			}
    		}
    	};
    
    	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) => {
    			throw e;
    		});
    }
    
    /**
     * @private
     * @return {string}
     */
    function getTemplate() {
    	return this.innerHTML;
    }
    
    registerCustomElement(Reload);