Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
  • 1.31
  • 4.15.1
  • 4.15.0
  • 4.14.0
  • 4.13.1
  • 4.13.0
  • 4.12.0
  • 4.11.1
  • 4.11.0
  • 4.10.4
  • 4.10.3
  • 4.10.2
  • 4.10.1
  • 4.10.0
  • 4.9.0
  • 4.8.0
  • 4.7.0
  • 4.6.1
  • 4.6.0
  • 4.5.1
  • 4.5.0
22 results

reload.mjs

Blame
  • reload.mjs 9.37 KiB
    /**
     * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
     * Node module: @schukai/monster
     * This file is licensed under the AGPLv3 License.
     * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
     */
    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>
     * ```
     *
     * @startuml reload.png
     * skinparam monochrome true
     * skinparam shadowing false
     * HTMLElement <|-- CustomElement
     * CustomElement <|-- CustomControl
     * CustomControl <|-- Reload
     * @enduml
     *
     * @since 1.13.0
     * @copyright schukai GmbH
     * @memberOf Monster.Components.Form
     * @summary A reload control
     * @fires Monster.Components.Form.event:monster-fetched
     */
    class Reload extends CustomElement {
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @returns {symbol}
    	 * @since 2.1.0
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for("@schukai/component-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: false,
    				url: undefined,
    				reload: "onshow",
    				filter: undefined,
    				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;
    	}
    
    	/**
    	 *
    	 */
    	[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.Components.Form.event: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.Components.Form.event: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.Components.Form.event: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);