Something went wrong on our end
Select Git revision
scheduler-event.go
-
Volker Schukai authoredVolker Schukai authored
datatable.mjs 24.43 KiB
/**
* Copyright 2023 schukai GmbH
* SPDX-License-Identifier: AGPL-3.0
*/
import {Datasource} from "./datasource.mjs";
import {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
getSlottedElements,
} from "../../dom/customelement.mjs";
import {findTargetElementFromEvent} from "../../dom/events.mjs";
import {
isString,
isFunction,
isInstance,
isObject,
isArray,
} from "../../types/is.mjs";
import {Observer} from "../../types/observer.mjs";
import {
ATTRIBUTE_DATATABLE_HEAD,
ATTRIBUTE_DATATABLE_GRID_TEMPLATE,
ATTRIBUTE_DATASOURCE_SELECTOR,
ATTRIBUTE_DATATABLE_ALIGN,
ATTRIBUTE_DATATABLE_SORTABLE,
ATTRIBUTE_DATATABLE_MODE,
ATTRIBUTE_DATATABLE_INDEX,
ATTRIBUTE_DATATABLE_MODE_HIDDEN,
ATTRIBUTE_DATATABLE_MODE_VISIBLE,
ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
ATTRIBUTE_DATATABLE_MODE_FIXED,
} from "./constants.mjs";
import {instanceSymbol} from "../../constants.mjs";
import {
Header,
createOrderStatement,
DIRECTION_ASC,
DIRECTION_DESC,
DIRECTION_NONE,
} from "./datatable/header.mjs";
import {getStoredFilterConfigKey} from "./filter/util.mjs";
import {DatatableStyleSheet} from "./stylesheet/datatable.mjs";
import {
handleDataSourceChanges,
datasourceLinkedElementSymbol,
} from "./util.mjs";
import "./columnbar.mjs";
import "./filter-button.mjs";
import {getDocument, getWindow} from "../../dom/util.mjs";
import {addAttributeToken} from "../../dom/attributes.mjs";
import {ATTRIBUTE_ERRORMESSAGE} from "../../dom/constants.mjs";
import {getDocumentTranslations} from "../../i18n/translations.mjs";
import "../state/state.mjs";
import "../host/collapse.mjs";
import {generateUniqueConfigKey} from "../host/util.mjs";
import "./datasource/dom.mjs";
import "./datasource/rest.mjs";
export {DataTable};
/**
* @private
* @type {symbol}
*/
const gridElementSymbol = Symbol("gridElement");
/**
* @private
* @type {symbol}
*/
const gridHeadersElementSymbol = Symbol("gridHeadersElement");
/**
* @private
* @type {symbol}
*/
const columnBarElementSymbol = Symbol("columnBarElement");
/**
* The DataTable component is used to show the data from a data source.
*
* <img src="./images/datatable.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-datatable />` directly in the HTML or using
* Javascript via the `document.createElement('monster-datatable');` method.
*
* ```html
* <monster-datatable></monster-datatable>
* ```
*
* Or you can create this CustomControl directly in Javascript:
*
* ```js
* import '@schukai/component-datatable/source/datatable.mjs';
* document.createElement('monster-datatable');
* ```
*
* The Body should have a class "hidden" to ensure that the styles are applied correctly.
*
* ```css
* body.hidden {
* visibility: hidden;
* }
* ```
*
* @startuml datatable.png
* skinparam monochrome true
* skinparam shadowing false
* HTMLElement <|-- CustomElement
* CustomElement <|-- Datatable
* @enduml
*
* @copyright schukai GmbH
* @memberOf Monster.Components.Datatable
* @summary A data table
*/
class DataTable extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/datatable@@instance");
}
/**
* 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} datasource Datasource configuration
* @property {string} datasource.selector Selector for the datasource
* @property {Object} mapping Mapping configuration
* @property {string} mapping.data Data mapping
* @property {Array} data Data
* @property {Array} headers Headers
* @property {Object} responsive Responsive configuration
* @property {number} responsive.breakpoint Breakpoint for responsive mode
* @property {Object} labels Labels
* @property {string} labels.theListContainsNoEntries Label for empty state
* @property {Object} features Features
* @property {boolean} features.settings Settings feature
* @property {boolean} features.footer Footer feature
* @property {boolean} features.autoInit Auto init feature (init datasource automatically)
* @property {Object} templateMapping Template mapping
* @property {string} templateMapping.row-key Row key
* @property {string} templateMapping.filter-id Filter id
**/
get defaults() {
return Object.assign(
{},
super.defaults,
{
templates: {
main: getTemplate(),
emptyState: getEmptyTemplate(),
},
datasource: {
selector: null,
},
mapping: {
data: "dataset",
},
data: [],
headers: [],
responsive: {
breakpoint: 800,
},
labels: {
theListContainsNoEntries: "The list contains no entries",
},
features: {
settings: true,
footer: true,
autoInit: true,
},
templateMapping: {
"row-key": null,
"filter-id": null,
},
},
initOptionsFromArguments.call(this),
);
}
/**
*
* @param {string} selector
* @returns {NodeListOf<*>}
*/
getGridElements(selector) {
return this[gridElementSymbol].querySelectorAll(selector);
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datatable";
}
/**
*
* @return {Monster.Components.Form.Form}
*/
[assembleMethodSymbol]() {
const rawKey = this.getOption("templateMapping.row-key");
if (rawKey === null) {
if (this.id !== null && this.id !== "") {
const rawKey = this.getOption("templateMapping.row-key");
if (rawKey === null) {
this.setOption("templateMapping.row-key", this.id + "-row");
}
} else {
this.setOption("templateMapping.row-key", "row");
}
}
if (this.id !== null && this.id !== "") {
this.setOption("templateMapping.filter-id", "" + this.id + "-filter");
} else {
this.setOption("templateMapping.filter-id", "filter");
}
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
const selector = this.getOption("datasource.selector");
if (isString(selector)) {
const elements = document.querySelectorAll(selector);
if (elements.length !== 1) {
throw new Error("the selector must match exactly one element");
}
const element = elements[0];
if (!isInstance(element, Datasource)) {
throw new TypeError("the element must be a datasource");
}
this[datasourceLinkedElementSymbol] = element;
setTimeout(() => {
handleDataSourceChanges.call(this);
element.datasource.attachObserver(
new Observer(handleDataSourceChanges.bind(this)),
);
}, 0);
}
getHostConfig
.call(this, getColumnVisibilityConfigKey)
.then((config) => {
const headerOrderMap = new Map();
getHostConfig
.call(this, getStoredOrderConfigKey)
.then((orderConfig) => {
if (isArray(orderConfig) || orderConfig.length > 0) {
for (let i = 0; i < orderConfig.length; i++) {
const item = orderConfig[i];
const parts = item.split(" ");
const field = parts[0];
const direction = parts[1] || DIRECTION_ASC;
headerOrderMap.set(field, direction);
}
}
})
.then(() => {
try {
initGridAndStructs.call(this, config, headerOrderMap);
} catch (error) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error?.message || error.toString(),
);
}
updateColumnBar.call(this);
})
.catch((error) => {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error?.message || error.toString(),
);
});
})
.catch((error) => {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
error?.message || error.toString(),
);
});
}
/**
*
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [DatatableStyleSheet];
}
}
/**
* @private
* @returns {string}
*/
function getColumnVisibilityConfigKey() {
return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
}
/**
* @private
* @returns {string}
*/
function getFilterConfigKey() {
return generateUniqueConfigKey("datatable", this?.id, "filter");
}
/**
* @private
* @returns {Promise}
*/
function getHostConfig(callback) {
const document = getDocument();
const host = document.querySelector("monster-host");
if (!(host && this.id)) {
return Promise.resolve({});
}
if (!host || !isFunction(host?.getConfig)) {
throw new TypeError("the host must be a monster-host");
}
const configKey = callback.call(this);
return host.hasConfig(configKey).then((hasConfig) => {
if (hasConfig) {
return host.getConfig(configKey);
} else {
return {};
}
});
}
/**
* @private
*/
function updateColumnBar() {
if (!this[columnBarElementSymbol]) {
return;
}
const columns = [];
for (const header of this.getOption("headers")) {
const mode = header.getInternal("mode");
if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
continue;
}
columns.push({
visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
name: header.label,
index: header.index,
});
}
this[columnBarElementSymbol].setOption("columns", columns);
}
/**
* @private
*/
function updateHeaderFromColumnBar() {
if (!this[columnBarElementSymbol]) {
return;
}
const options = this[columnBarElementSymbol].getOption("columns");
if (!isArray(options)) return;
const invisibleMap = {};
for (let i = 0; i < options.length; i++) {
const option = options[i];
invisibleMap[option.index] = option.visible;
}
for (const header of this.getOption("headers")) {
const mode = header.getInternal("mode");
if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
continue;
}
if (invisibleMap[header.index] === false) {
header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN);
} else {
header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE);
}
}
}
/**
* @private
*/
function updateConfigColumnBar() {
if (!this[columnBarElementSymbol]) {
return;
}
const options = this[columnBarElementSymbol].getOption("columns");
if (!isArray(options)) return;
const map = {};
for (let i = 0; i < options.length; i++) {
const option = options[i];
map[option.name] = option.visible;
}
const document = getDocument();
const host = document.querySelector("monster-host");
if (!(host && this.id)) {
return;
}
const configKey = getColumnVisibilityConfigKey.call(this);
try {
host.setConfig(configKey, map);
} catch (error) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
}
}
/**
* @private
*/
function initEventHandler() {
const self = this;
getWindow().addEventListener("resize", (event) => {
updateGrid.call(self);
});
self[columnBarElementSymbol].attachObserver(
new Observer((e) => {
updateHeaderFromColumnBar.call(self);
updateGrid.call(self);
updateConfigColumnBar.call(self);
}),
);
self[gridHeadersElementSymbol].addEventListener("click", function (event) {
let element = null;
const datasource = self[datasourceLinkedElementSymbol];
if (!datasource) {
return;
}
element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE);
if (element) {
const index = element.parentNode.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
const headers = self.getOption("headers");
event.preventDefault();
headers[index].changeDirection();
setTimeout(function () {
/** hotfix, normally this should be done via the updater, no idea why this is not possible. */
element.setAttribute(
ATTRIBUTE_DATATABLE_SORTABLE,
`${headers[index].field} ${headers[index].direction}`,
);
storeOrderStatement.call(self, true);
}, 0);
}
});
}
/**
* @private
*/
function initGridAndStructs(hostConfig, headerOrderMap) {
const rowID = this.getOption("templateMapping.row-key");
if (!this[gridElementSymbol]) {
throw new Error("no grid element is defined");
}
let template;
getSlottedElements.call(this).forEach((e) => {
if (e instanceof HTMLTemplateElement && e.id === rowID) {
template = e;
}
});
if (!template) {
throw new Error("no template is defined");
}
const rowCount = template.content.children.length;
const headers = [];
for (let i = 0; i < rowCount; i++) {
let hClass = "";
const row = template.content.children[i];
let mode = "";
if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) {
mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE);
}
let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE);
if (!grid || grid === "" || grid === "auto") {
grid = "minmax(0, 1fr)";
}
let label = "";
let labelKey = "";
if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) {
label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD);
labelKey = label;
try {
if (label.startsWith("i18n:")) {
label = label.substring(5, label.length);
label = getDocumentTranslations().getText(label, label);
}
} catch (e) {
label = "i18n error " + label;
}
}
if (!label) {
label = i + 1 + "";
mode = ATTRIBUTE_DATATABLE_MODE_FIXED;
labelKey = label;
}
if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) {
if (hostConfig[label] === false) {
mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN;
} else {
mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE;
}
}
let align = "";
if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) {
align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN);
}
switch (align) {
case "center":
hClass = "flex-center";
break;
case "end":
hClass = "flex-end";
break;
case "start":
hClass = "flex-start";
break;
default:
hClass = "flex-start";
}
let field = "";
let direction = DIRECTION_NONE;
if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) {
field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim();
const parts = field.split(" ").map((item) => item.trim());
field = parts[0];
if (headerOrderMap.has(field)) {
direction = headerOrderMap.get(field);
} else if (parts.length === 2 && [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1) {
direction = parts[1];
}
}
if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
hClass += " hidden";
}
const header = new Header();
header.setInternals({
field: field,
label: label,
classes: hClass,
index: i,
mode: mode,
grid: grid,
labelKey: labelKey,
direction: direction,
});
headers.push(header);
}
this.setOption("headers", headers);
setTimeout(() => {
storeOrderStatement.call(this, this.getOption("features.autoInit"));
},
0);
}
/**
* @private
* @returns {string}
*/
export function getStoredOrderConfigKey() {
return generateUniqueConfigKey("datatable", this?.id, "stored-order");
}
/**
* @private
*/
function storeOrderStatement(doFetch) {
const headers = this.getOption("headers");
const statement = createOrderStatement(headers);
setDataSource.call(this, {orderBy: statement}, doFetch);
const document = getDocument();
const host = document.querySelector("monster-host");
if (!(host && this.id)) {
return;
}
const configKey = getStoredOrderConfigKey.call(this);
// statement explode with , and remove all empty
const list = statement.split(",").filter((item) => item.trim() !== "");
if (list.length === 0) {
// host.deleteConfig(configKey);
return;
}
host.setConfig(configKey, list);
}
/**
* @private
*/
function updateGrid() {
if (!this[gridElementSymbol]) {
throw new Error("no grid element is defined");
}
let gridTemplateColumns = "";
const headers = this.getOption("headers");
let styles = "";
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`;
styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`;
} else {
gridTemplateColumns += `${header.grid} `;
}
}
const sheet = new CSSStyleSheet();
if (styles !== "") sheet.replaceSync(styles);
this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet];
const bodyWidth = getDocument().body.getBoundingClientRect().width;
const breakpoint = this.getOption("responsive.breakpoint");
if (bodyWidth > breakpoint) {
this[
gridElementSymbol
].style.gridTemplateColumns = `${gridTemplateColumns}`;
this[
gridHeadersElementSymbol
].style.gridTemplateColumns = `${gridTemplateColumns}`;
} else {
this[gridElementSymbol].style.gridTemplateColumns = "auto";
this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto";
}
}
/**
* @private
* @param {Monster.Components.Datatable.Header[]} headers
* @param {bool} doFetch
*/
function setDataSource({orderBy}, doFetch) {
const datasource = this[datasourceLinkedElementSymbol];
if (!datasource) {
return;
}
if (isFunction(datasource?.setParameters)) {
datasource.setParameters({orderBy});
}
if (doFetch !== false && isFunction(datasource?.fetch)) {
datasource.fetch();
}
}
/**
* @private
* @return {Monster.Components.Datatable.Form}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[gridElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=datatable]",
);
this[gridHeadersElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=datatable-headers]",
);
this[columnBarElementSymbol] =
this.shadowRoot.querySelector("monster-column-bar");
return this;
}
/**
* @private
* @return {object}
* @throws {TypeError} incorrect arguments passed for the datasource
* @throws {Error} the datasource could not be initialized
*/
function initOptionsFromArguments() {
const options = {};
const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
if (selector) {
options.datasource = {selector: selector};
}
const breakpoint = this.getAttribute(
ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
);
if (breakpoint) {
options.responsive = {};
options.responsive.breakpoint = parseInt(breakpoint);
}
return options;
}
/**
* @private
* @return {string}
*/
function getEmptyTemplate() {
return `<monster-state data-monster-role="empty-without-action">
<div part="visual">
<svg width="4rem" height="4rem" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/>
<path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/>
</svg>
</div>
<div part="content" data-monster-replace="path:labels.theListContainsNoEntries">
The list contains no entries.
</div>
</monster-state>`;
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
<template id="headers-row">
<div data-monster-attributes="class path:headers-row.classname,
data-monster-index path:headers-row.index"
data-monster-replace="path:headers-row.html"></div>
</template>
<slot></slot>
<div class="table-container" part="table-container">
<div class="filter">
<slot name="filter"></slot>
</div>
<div class="bar">
<monster-column-bar
data-monster-attributes="class path:features.settings | ?::hidden"></monster-column-bar>
<slot name="bar"></slot>
</div>
<div data-monster-role="datatable-headers" data-monster-insert="headers-row path:headers"></div>
<div data-monster-replace="path:templates.emptyState"
data-monster-attributes="class path:data | has-entries | ?:hidden:empty-state-container"></div>
<div data-monster-role="datatable" data-monster-insert="\${row-key} path:data">
</div>
</div>
<div data-monster-role="footer" data-monster-select-this="true"
data-monster-attributes="class path:data | has-entries | ?::hidden">
<slot name="footer" data-monster-attributes="class path:features.footer | ?::hidden"></slot>
</div>
</div>
`;
}
registerCustomElement(DataTable);