Something went wrong on our end
Select Git revision
datatable.mjs
-
Volker Schukai authoredVolker Schukai authored
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);