Skip to content
Snippets Groups Projects
Select Git revision
  • 783084c60b1ee40eacbc8711adc5ea26863dc08c
  • master default protected
  • 1.31
  • 4.24.3
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
23 results

datatable.mjs

Blame
  • collapse.mjs 12.81 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.
     */
    
    import {
    	assembleMethodSymbol,
    	CustomElement,
    	getSlottedElements,
    	registerCustomElement,
    } from "../../dom/customelement.mjs";
    import { CollapseStyleSheet } from "./stylesheet/collapse.mjs";
    import { fireCustomEvent } from "../../dom/events.mjs";
    import { getDocument } from "../../dom/util.mjs";
    import { addAttributeToken } from "../../dom/attributes.mjs";
    import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
    import { Host } from "../host/host.mjs";
    import { generateUniqueConfigKey } from "../host/util.mjs";
    import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
    import { instanceSymbol } from "../../constants.mjs";
    
    export { Collapse, nameSymbol };
    
    /**
     * @private
     * @type {symbol}
     */
    const timerCallbackSymbol = Symbol("timerCallback");
    
    /**
     * @private
     * @type {symbol}
     */
    const detailsElementSymbol = Symbol("detailsElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const controlElementSymbol = Symbol("controlElement");
    
    /**
     * local symbol
     * @private
     * @type {symbol}
     */
    const resizeObserverSymbol = Symbol("resizeObserver");
    
    /**
     * @private
     * @type {symbol}
     */
    const detailsSlotElementSymbol = Symbol("detailsSlotElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const detailsContainerElementSymbol = Symbol("detailsContainerElement");
    /**
    
     * @private
     * @type {symbol}
     */
    const detailsDecoElementSymbol = Symbol("detailsDecoElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const nameSymbol = Symbol("name");
    
    /**
     * The Collapse component is used to show a details.
     *
     * <img src="./images/collapse.png">
     *
     * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
     *
     * You can create this control either by specifying the HTML tag <monster-collapse />` directly in the HTML or using
     * Javascript via the `document.createElement('monster-collapse');` method.
     *
     * ```html
     * <monster-collapse></monster-collapse>
     * ```
     *
     * Or you can create this CustomControl directly in Javascript:
     *
     * ```js
     * import '@schukai/monster/source/components/host/collapse.mjs';
     * document.createElement('monster-collapse');
     * ```
     *
     * The Body should have a class "hidden" to ensure that the styles are applied correctly.
     *
     * ```css
     * body.hidden {
     *    visibility: hidden;
     * }
     * ```
     *
     * @startuml collapse.png
     * skinparam monochrome true
     * skinparam shadowing false
     * HTMLElement <|-- CustomElement
     * CustomElement <|-- Collapse
     * @enduml
     *
     * @copyright schukai GmbH
     * @memberOf Monster.Components.Host
     * @summary A simple collapse component
     * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-open
     * @fires Monster.Components.Host.Collapse.event:monster-collapse-open
     * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-close
     * @fires Monster.Components.Host.Collapse.event:monster-collapse-closed
     * @fires Monster.Components.Host.Collapse.event:monster-collapse-adjust-height
     */
    class Collapse extends CustomElement {
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @returns {symbol}
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for("@schukai/monster/components/layout/collapse@@instance");
    	}
    
    	/**
    	 *
    	 */
    	constructor() {
    		super();
    		// the name is only used for the host config and the event name
    		this[nameSymbol] = "collapse";
    	}
    
    	/**
    	 * 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} classes CSS classes
    	 * @property {string} classes.container CSS class for the container
    	 * @property {Object} features Feature configuration
    	 * @property {boolean} features.accordion Enable accordion mode
    	 * @property {boolean} features.persistState Enable persist state (Host and Config-Manager required)
    	 * @property {boolean} features.useScrollValues Use scroll values (scrollHeight) instead of clientHeight for the height calculation
    	 * @property {boolean} openByDefault Open the details by default
    	 */
    	get defaults() {
    		return Object.assign({}, super.defaults, {
    			templates: {
    				main: getTemplate(),
    			},
    			classes: {
    				container: "padding",
    			},
    			features: {
    				accordion: true,
    				persistState: true,
    				useScrollValues: false,
    			},
    			openByDefault: false,
    		});
    	}
    
    	/**
    	 *
    	 * @returns {Monster.Components.Host.Collapse}
    	 */
    	[assembleMethodSymbol]() {
    		super[assembleMethodSymbol]();
    		initControlReferences.call(this);
    		initStateFromHostConfig.call(this);
    		initResizeObserver.call(this);
    		initEventHandler.call(this);
    
    		if (this.getOption("openByDefault")) {
    			this.open();
    		}
    	}
    
    	/**
    	 *
    	 */
    	connectedCallback() {
    		super.connectedCallback();
    		updateResizeObserverObservation.call(this);
    		//  this[resizeObserverSymbol].observe(getDocument().body);
    	}
    
    	/**
    	 *
    	 */
    	disconnectedCallback() {
    		super.disconnectedCallback();
    		//this[resizeObserverSymbol].disconnect();
    	}
    
    	/**
    	 *
    	 * @returns {Monster.Components.Host.Collapse}
    	 */
    	toggle() {
    		if (this[detailsElementSymbol].classList.contains("active")) {
    			this.close();
    		} else {
    			this.open();
    		}
    		return this;
    	}
    
    	/**
    	 *
    	 * @returns {boolean}
    	 */
    	isClosed() {
    		return !this[detailsElementSymbol].classList.contains("active");
    	}
    
    	/**
    	 *
    	 * @returns {boolean}
    	 */
    	isOpen() {
    		return !this.isClosed();
    	}
    
    	/**
    	 *
    	 * @returns {Monster.Components.Host.Collapse}
    	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-open
    	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-open
    	 */
    	open() {
    		let node;
    		if (this[detailsElementSymbol].classList.contains("active")) {
    			return this;
    		}
    
    		fireCustomEvent(this, "monster-" + this[nameSymbol] + "-before-open", {});
    
    		adjustHeight.call(this);
    		this[detailsElementSymbol].classList.add("active");
    
    		if (this.getOption("features.accordion") === true) {
    			node = this;
    			while (node.nextElementSibling instanceof Collapse) {
    				node = node.nextElementSibling;
    				node.close();
    			}
    
    			node = this;
    			while (node.previousElementSibling instanceof Collapse) {
    				node = node.previousElementSibling;
    				node.close();
    			}
    		}
    
    		setTimeout(() => {
    			setTimeout(() => {
    				updateStateConfig.call(this);
    				fireCustomEvent(this, "monster-" + this[nameSymbol] + "-open", {});
    				setTimeout(() => {
    					this[controlElementSymbol].classList.remove("overflow-hidden");
    				}, 500);
    			}, 0);
    		}, 0);
    
    		return this;
    	}
    
    	/**
    	 *
    	 * @returns {Monster.Components.Host.Collapse}
    	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-close
    	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-closed
    	 */
    	close() {
    		if (!this[detailsElementSymbol].classList.contains("active")) {
    			return this;
    		}
    
    		fireCustomEvent(this, "monster-" + this[nameSymbol] + "-before-close", {});
    		this[controlElementSymbol].classList.add("overflow-hidden");
    
    		setTimeout(() => {
    			this[detailsElementSymbol].classList.remove("active");
    			setTimeout(() => {
    				updateStateConfig.call(this);
    				fireCustomEvent(this, "monster-" + this[nameSymbol] + "-closed", {});
    			}, 0);
    		}, 0);
    
    		return this;
    	}
    
    	/**
    	 *
    	 * @return {string}
    	 */
    	static getTag() {
    		return "monster-collapse";
    	}
    
    	/**
    	 * @return {Array<CSSStyleSheet>}
    	 */
    	static getCSSStyleSheet() {
    		return [CollapseStyleSheet];
    	}
    
    	/**
    	 * This method is called when the element is inserted into a document, including into a shadow tree.
    	 * @return {Monster.Components.Host.Collapse}
    	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-adjust-height
    	 */
    	adjustHeight() {
    		adjustHeight.call(this);
    		return this;
    	}
    }
    
    function adjustHeight() {
    	let height = 0;
    
    	if (this[detailsContainerElementSymbol]) {
    		if (this.getOption("features.useScrollValues")) {
    			height += this[detailsContainerElementSymbol].scrollHeight;
    		} else {
    			height += this[detailsContainerElementSymbol].clientHeight;
    		}
    	}
    
    	if (this[detailsDecoElementSymbol]) {
    		if (this.getOption("features.useScrollValues")) {
    			height += this[detailsDecoElementSymbol].scrollHeight;
    		} else {
    			height += this[detailsDecoElementSymbol].clientHeight + 1;
    		}
    	}
    
    	if (height === 0) {
    		if (this.getOption("features.useScrollValues")) {
    			height = this[detailsElementSymbol].scrollHeight;
    		} else {
    			height = this[detailsElementSymbol].clientHeight;
    		}
    
    		if (height === 0) {
    			height = "auto";
    		}
    	} else {
    		height += "px";
    	}
    
    	this[detailsElementSymbol].style.setProperty(
    		"--monster-height",
    		height,
    		"important",
    	);
    	fireCustomEvent(this, "monster-" + this[nameSymbol] + "-adjust-height", {});
    }
    
    function updateResizeObserverObservation() {
    	this[resizeObserverSymbol].disconnect();
    
    	const slottedNodes = getSlottedElements.call(this);
    	slottedNodes.forEach((node) => {
    		this[resizeObserverSymbol].observe(node);
    	});
    
    	if (this[detailsContainerElementSymbol]) {
    		this[resizeObserverSymbol].observe(this[detailsContainerElementSymbol]);
    	}
    
    	this.adjustHeight();
    }
    
    /**
     * @private
     */
    function initEventHandler() {
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	initSlotChangedHandler.call(this);
    	return this;
    }
    
    function initSlotChangedHandler() {
    	this[detailsSlotElementSymbol].addEventListener("slotchange", () => {
    		updateResizeObserverObservation.call(this);
    	});
    }
    
    /**
     * @private
     * @return {Select}
     * @throws {Error} no shadow-root is defined
     */
    function initControlReferences() {
    	if (!this.shadowRoot) {
    		throw new Error("no shadow-root is defined");
    	}
    
    	this[controlElementSymbol] = this.shadowRoot.querySelector(
    		"[data-monster-role=control]",
    	);
    	this[detailsElementSymbol] = this.shadowRoot.querySelector(
    		"[data-monster-role=detail]",
    	);
    	this[detailsSlotElementSymbol] = this.shadowRoot.querySelector("slot");
    	this[detailsContainerElementSymbol] = this.shadowRoot.querySelector(
    		"[data-monster-role=container]",
    	);
    	this[detailsDecoElementSymbol] = this.shadowRoot.querySelector(
    		"[data-monster-role=deco]",
    	);
    }
    
    /**
     * @private
     * @returns {string}
     */
    function getConfigKey() {
    	return generateUniqueConfigKey(this[nameSymbol], this.id, "state");
    }
    
    /**
     * @private
     */
    function updateStateConfig() {
    	if (!this.getOption("features.persistState")) {
    		return;
    	}
    
    	if (!this[detailsElementSymbol]) {
    		return;
    	}
    
    	const document = getDocument();
    	const host = document.querySelector("monster-host");
    	if (!(host && this.id)) {
    		return;
    	}
    
    	const configKey = getConfigKey.call(this);
    
    	try {
    		host.setConfig(configKey, this.isOpen());
    	} catch (error) {
    		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
    	}
    }
    
    /**
     * @private
     * @returns {Promise}
     */
    function initStateFromHostConfig() {
    	if (!this.getOption("features.persistState")) {
    		return Promise.resolve({});
    	}
    
    	const document = getDocument();
    	const host = document.querySelector("monster-host");
    
    	if (!(host && this.id)) {
    		return Promise.resolve({});
    	}
    
    	const configKey = getConfigKey.call(this);
    	return host
    		.getConfig(configKey)
    		.then((state) => {
    			if (state === true) {
    				this.open();
    			} else {
    				this.close();
    			}
    		})
    		.catch((error) => {
    			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
    		});
    }
    
    /**
     * @private
     */
    function initResizeObserver() {
    	// against flickering
    	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
    		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
    			try {
    				this[timerCallbackSymbol].touch();
    				return;
    			} catch (e) {
    				delete this[timerCallbackSymbol];
    			}
    		}
    
    		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
    			checkAndRearrangeContent.call(this);
    		});
    	});
    }
    
    function checkAndRearrangeContent() {
    	this.adjustHeight();
    }
    
    /**
     * @private
     * @return {string}
     */
    function getTemplate() {
    	// language=HTML
    	return `
            <div data-monster-role="control" part="control" class="overflow-hidden">
                <div data-monster-role="detail">
                    <div data-monster-attributes="class path:classes.container" part="container"
                         data-monster-role="container">
                        <slot></slot>
                    </div>
                    <div class="deco-line" data-monster-role="deco" part="deco"></div>
                </div>
            </div>`;
    }
    
    registerCustomElement(Collapse);