UNPKG

@schukai/monster

Version:

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

273 lines (235 loc) 6.88 kB
/** * 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 { assembleMethodSymbol, CustomElement, registerCustomElement, } from "../../dom/customelement.mjs"; import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; import { PanelStyleSheet } from "./stylesheet/panel.mjs"; import { instanceSymbol } from "../../constants.mjs"; export { Panel }; /** * @private * @type {symbol} */ const PanelElementSymbol = Symbol("PanelElement"); /** * local symbol * @private * @type {symbol} */ const resizeObserverSymbol = Symbol("resizeObserver"); /** * @private * @type {symbol} */ const timerCallbackSymbol = Symbol("timerCallback"); /** * A Panel is a container that can hold other elements and is used to display content in a structured way. * * @fragments /fragments/components/layout/panel/ * * @example /examples/components/layout/panel-simple * * @since 3.54.0 * @copyright schukai GmbH * @summary The Panel component is used to display a panel, isn't that cool? */ class Panel extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/layout/panel"); } /** * 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} heightAdjustment Height adjustment */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, heightAdjustment: 4, }); } /** * * @return {Panel} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); calcHeight.call(this); } /** * This method is called by the dom and should not be called directly. * * @return {void} */ connectedCallback() { super.connectedCallback(); attachResizeObserver.call(this); // disable scrolling in parent node if (this.parentNode && this.parentNode instanceof HTMLElement) { this.parentNode.style.overflow = "hidden"; } } /** * This method is called by the dom and should not be called directly. * * @return {void} */ disconnectedCallback() { super.disconnectedCallback(); disconnectResizeObserver.call(this); } /** * @return {string} */ static getTag() { return "monster-panel"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [PanelStyleSheet]; } } /** * @private */ function calcHeight() { this.style.boxSizing = "border-box"; const height = calculateMaximumHeight.call(this, this); if (height < 0) { return; } this.style.height = `${height}px`; } /** * Calculate the maximum height of an element based on the window's inner height * @param element * @return {*} */ function calculateMaximumHeight(element) { let totalBottomBorder = 0; let totalBottomPadding = 0; let totalBottomMargin = 0; let totalOutlineHeight = 0; let totalBoxShadowHeight = 0; let currentElement = element; // Get the distance from the top of the element to the top of the viewport const distanceFromTop = element.getBoundingClientRect().top; // Loop through the elements up to the body to sum up the bottom borders, padding, and margin while (currentElement && currentElement !== document.body) { const style = window.getComputedStyle(currentElement); // Box sizing const boxSizing = style.boxSizing; // Borders, padding, and margin const borderBottomWidth = parseFloat(style.borderBottomWidth); const paddingBottom = parseFloat(style.paddingBottom); const marginBottom = parseFloat(style.marginBottom); // Outline and box-shadow const outlineHeight = parseFloat(style.outlineWidth); // This is a simplification; box-shadow is more complex to parse const boxShadowVertical = parseFloat(style.boxShadow.split(" ")[3] || 0); // Accumulate values totalBottomBorder += isNaN(borderBottomWidth) ? 0 : borderBottomWidth; totalBottomPadding += isNaN(paddingBottom) || boxSizing === "border-box" ? 0 : paddingBottom; totalBottomMargin += isNaN(marginBottom) ? 0 : marginBottom; totalOutlineHeight += isNaN(outlineHeight) ? 0 : outlineHeight; totalBoxShadowHeight += isNaN(boxShadowVertical) ? 0 : boxShadowVertical; currentElement = currentElement.parentNode || currentElement.host; } // Calculate the maximum height by subtracting the distance, borders, padding, margin, outline, and box-shadow from the window's inner height const maximumHeight = window.innerHeight - distanceFromTop - totalBottomBorder - totalBottomPadding - totalBottomMargin - totalOutlineHeight - totalBoxShadowHeight; return maximumHeight + this.getOption("heightAdjustment"); } /** * @private */ function attachResizeObserver() { // against flickering this[resizeObserverSymbol] = new ResizeObserver(() => { if (this[timerCallbackSymbol] instanceof DeadMansSwitch) { try { this[timerCallbackSymbol].touch(); return; } catch (e) { delete this[timerCallbackSymbol]; } } this[timerCallbackSymbol] = new DeadMansSwitch(200, () => { calcHeight.call(this); }); }); this[resizeObserverSymbol].observe(this.ownerDocument.body); this[resizeObserverSymbol].observe(document.scrollingElement); } function disconnectResizeObserver() { if (this[resizeObserverSymbol] instanceof ResizeObserver) { this[resizeObserverSymbol].disconnect(); } } /** * @private * @return {Panel} * @throws {Error} no shadow-root is defined */ function initControlReferences() { if (!this.shadowRoot) { throw new Error("no shadow-root is defined"); } this[PanelElementSymbol] = this.shadowRoot.querySelector( "[data-monster-role=control]", ); } /** * @private */ function initEventHandler() { return this; } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <slot></slot> </div>`; } registerCustomElement(Panel);