@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
410 lines (361 loc) • 10.6 kB
JavaScript
/**
* 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 {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { clone } from "../../util/clone.mjs";
import { ColumnBarStyleSheet } from "./stylesheet/column-bar.mjs";
import { createPopper } from "@popperjs/core";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { hasObjectLink } from "../../dom/attributes.mjs";
import { customElementUpdaterLinkSymbol } from "../../dom/constants.mjs";
import { getGlobalObject } from "../../types/global.mjs";
export { ColumnBar };
/**
* @private
* @type {symbol}
*/
const settingsButtonElementSymbol = Symbol("settingButtonElement");
/**
* @private
* @type {symbol}
*/
const settingsButtonEventHandlerSymbol = Symbol("settingsButtonEventHandler");
/**
* @private
* @type {symbol}
*/
const settingsLayerElementSymbol = Symbol("settingsLayerElement");
/**
* @private
* @type {symbol}
*/
const dotsContainerElementSymbol = Symbol("dotsContainerElement");
/**
* @private
* @type {symbol}
*/
const popperInstanceSymbol = Symbol("popperInstance");
/**
* @private
* @type {symbol}
*/
const closeEventHandlerSymbol = Symbol("closeEventHandler");
/**
* A column bar for a datatable
*
* @fragments /fragments/components/datatable/datatable/
*
* @example /examples/components/datatable/empty
*
* @copyright schukai GmbH
* @summary The ColumnBar component is used to show and configure the columns of a datatable.
**/
class ColumnBar extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/column-bar@@instance");
}
/**
* This method is called to customize the component.
* @returns {Map<unknown, unknown>}
*/
get customization() {
return new Map([...super.customization, ["templateFormatter.i18n", true]]);
}
/**
* 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} labels Locale definitions
* @property {string} locale.settings The text for the settings button
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: getTranslations(),
columns: [],
});
}
/**
* Called every time the element is added to the DOM. Useful for running initialization code.
* @return {void}
* @since 4.14.0
*/
connectedCallback() {
super.connectedCallback();
this[closeEventHandlerSymbol] = (event) => {
const path = event.composedPath();
const isOutsideElement = !path.includes(this);
const isOutsideShadow = !path.includes(this.shadowRoot);
if (
isOutsideElement &&
isOutsideShadow &&
this[settingsLayerElementSymbol]
) {
this[settingsLayerElementSymbol].classList.remove("visible");
}
};
getGlobalObject("document").addEventListener(
"click",
this[closeEventHandlerSymbol],
);
getGlobalObject("document").addEventListener(
"touch",
this[closeEventHandlerSymbol],
);
}
/**
* Called every time the element is removed from the DOM. Useful for running clean up code.
*
* @return {void}
* @since 4.14.0
*/
disconnectedCallback() {
super.disconnectedCallback();
if (this[closeEventHandlerSymbol]) {
getGlobalObject("document").removeEventListener(
"click",
this[closeEventHandlerSymbol],
);
getGlobalObject("document").removeEventListener(
"touch",
this[closeEventHandlerSymbol],
);
this[closeEventHandlerSymbol] = null;
}
}
/**
* @return {string}
*/
static getTag() {
return "monster-column-bar";
}
/**
*
* @return {void}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [ColumnBarStyleSheet];
}
}
/**
* @private
* @returns {{settings: string}}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de": // German
return { settings: "Einstellungen" };
case "fr": // French
return { settings: "Paramètres" };
case "es": // Spanish
return { settings: "Configuración" };
case "zh": // Mandarin (Chinese)
return { settings: "设置" };
case "hi": // Hindi
return { settings: "सेटिंग्स" };
case "bn": // Bengali
return { settings: "সেটিংস" };
case "pt": // Portuguese
return { settings: "Configurações" };
case "ru": // Russian
return { settings: "Настройки" };
case "ja": // Japanese
return { settings: "設定" };
case "pa": // Western Punjabi
return { settings: "ਸੈਟਿੰਗਾਂ" };
case "mr": // Marathi
return { settings: "सेटिंग्ज" };
case "it": // Italian
return { settings: "Impostazioni" };
case "nl": // Dutch
return { settings: "Instellingen" };
case "sv": // Swedish
return { settings: "Inställningar" };
case "pl": // Polish
return { settings: "Ustawienia" };
case "da": // Danish
return { settings: "Indstillinger" };
case "fi": // Finnish
return { settings: "Asetukset" };
case "no": // Norwegian
return { settings: "Innstillinger" };
case "cs": // Czech
return { settings: "Nastavení" };
default: // English fallback
case "en":
return { settings: "Settings" };
}
}
/**
* @private
* @return {ColumnBar}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[settingsButtonElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=settings-button]",
);
this[settingsLayerElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=settings-layer]",
);
this[dotsContainerElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=dots]",
);
return this;
}
/**
* @private
*/
function initEventHandler() {
const self = this;
self[popperInstanceSymbol] = createPopper(
self[settingsButtonElementSymbol],
self[settingsLayerElementSymbol],
{
placement: "auto",
modifiers: [
{
name: "offset",
options: {
offset: [10, 10],
},
},
],
},
);
self[dotsContainerElementSymbol].addEventListener("click", function (event) {
const element = findTargetElementFromEvent(
event,
"data-monster-role",
"column",
);
if (element) {
const index = element.getAttribute("data-monster-index");
event.preventDefault();
const columns = clone(self.getOption("columns"));
const column = columns.find((col) => {
return parseInt(col.index) === parseInt(index);
});
column.visible = !column.visible;
self.setOption("columns", columns);
}
});
self[settingsButtonEventHandlerSymbol] = (event) => {
const clickTarget = event.composedPath()?.[0];
if (
self[settingsLayerElementSymbol] === clickTarget ||
self[settingsLayerElementSymbol].contains(clickTarget)
) {
return;
}
document.body.removeEventListener(
"click",
self[settingsButtonEventHandlerSymbol],
);
};
self[settingsButtonElementSymbol].addEventListener("click", function (event) {
const element = findTargetElementFromEvent(
event,
"data-monster-role",
"settings-button",
);
if (element) {
self[settingsLayerElementSymbol].classList.toggle("visible");
event.preventDefault();
if (self[settingsLayerElementSymbol].classList.contains("visible")) {
self[popperInstanceSymbol].update();
queueMicrotask(() => {
document.body.addEventListener(
"click",
self[settingsButtonEventHandlerSymbol],
);
});
}
}
});
self[settingsLayerElementSymbol].addEventListener("change", function (event) {
const control = event.target;
const index = control.getAttribute("data-monster-index");
const columns = clone(self.getOption("columns"));
const column = columns.find((col) => {
return parseInt(col.index) === parseInt(index);
});
column.visible = control.checked;
self.setOption("columns", columns);
});
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<template id="column">
<div data-monster-role="column">
<label><input type="checkbox" data-monster-attributes="
data-monster-index path:column.index,
checked path:column.visible | ?:checked:"><span
data-monster-replace="path:column.name"
></span></label>
</div>
</template>
<template id="dots">
<li data-monster-insert="">
<a href="#" data-monster-role="column"
data-monster-attributes="
class path:dots.visible | ?:is-hidden:is-visible,
title path:dots.name,
data-monster-index path:dots.index">
</a>
</li>
</template>
<div data-monster-role="control" part="control" data-monster-select-this="true" data-monster-attributes="class path:columns | has-entries | ?::hidden">
<ul data-monster-insert="dots path:columns"
data-monster-role="dots"></ul>
<a href="#" data-monster-role="settings-button">i18n{settings}</a>
<div data-monster-role="settings-layer">
<div data-monster-insert="column path:columns" data-monster-role="settings-popup-list">
</div>
</div>
</div>
`;
}
registerCustomElement(ColumnBar);