Something went wrong on our end
Select Git revision
api-button.mjs
-
Volker Schukai authoredVolker Schukai authored
datatable.mjs 44.74 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.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import {Datasource} from "./datasource.mjs";
import {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
getSlottedElements,
} from "../../dom/customelement.mjs";
import {
findTargetElementFromEvent,
fireCustomEvent,
} from "../../dom/events.mjs";
import {clone} from "../../util/clone.mjs";
import {
isString,
isFunction,
isInstance,
isObject,
isArray,
} from "../../types/is.mjs";
import {
validateArray,
validateInteger,
validateObject,
} from "../../types/validate.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_FEATURES,
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 {DatatableStyleSheet} from "./stylesheet/datatable.mjs";
import {
handleDataSourceChanges,
datasourceLinkedElementSymbol,
} from "./util.mjs";
import "./columnbar.mjs";
import "./filter-button.mjs";
import {
findElementWithSelectorUpwards,
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";
import "../form/context-help.mjs";
import {getLocaleOfDocument} from "../../dom/locale.mjs";
export {DataTable};
/**
* @private
* @type {symbol}
*/
const gridElementSymbol = Symbol("gridElement");
/**
* @private
* @type {symbol}
*/
const dataControlElementSymbol = Symbol("dataControlElement");
/**
* @private
* @type {symbol}
*/
const gridHeadersElementSymbol = Symbol("gridHeadersElement");
/**
* @private
* @type {symbol}
*/
const columnBarElementSymbol = Symbol("columnBarElement");
/**
* @private
* @type {symbol}
*/
const copyAllElementSymbol = Symbol("copyAllElement");
/**
* @private
* @type {symbol}
*/
const resizeObserverSymbol = Symbol("resizeObserver");
/**
* The DataTable component is used to show the data from a data source.
*
* @copyright schukai GmbH
* @summary A data table
*/
/**
* A DataTable
*
* @fragments /fragments/components/datatable/datatable/
*
* @example /examples/components/datatable/empty The empty state
* @example /examples/components/datatable/data-using-javascript The data using javascript
* @example /examples/components/datatable/alignment The alignment
* @example /examples/components/datatable/row-mode The row mode
* @example /examples/components/datatable/grid-template The grid template
* @example /examples/components/datatable/overview-class The overview class
* @example /examples/components/datatable/datasource Use a datasource
* @example /examples/components/datatable/pagination Use pagination
* @example /examples/components/datatable/filter Filer the data
* @example /examples/components/datatable/ Select rows
*
* @copyright schukai GmbH
* @summary A beautiful and highly customizable data table. It can be used to display data from a data source.
* @fires monster-datatable-row-copied
* @fires monster-datatable-row-removed
* @fires monster-datatable-row-added
* @fires monster-datatable-row-selected
* @fires monster-datatable-row-deselected
* @fires monster-datatable-all-rows-selected
* @fires monster-datatable-all-rows-deselected
* @fires monster-datatable-selection-changed
**/
class DataTable extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @return {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} classes Classes
* @property {string} classes.container Container class
* @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 {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature
* @property {boolean} features.copyAll Copy all feature
* @property {boolean} features.help Help feature
* @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: 900,
},
labels: getTranslations(),
classes: {
control: "monster-theme-control-container-1",
container: "",
row: "monster-theme-control-row-1",
},
features: {
settings: true,
footer: true,
autoInit: true,
doubleClickCopyToClipboard: true,
copyAll: true,
help: true,
},
copy: {
delimiter: ";",
quoteOpen: '"',
quoteClose: '"',
rowBreak: "\n",
},
templateMapping: {
"row-key": null,
"filter-id": null,
},
},
initOptionsFromArguments.call(this),
);
}
/**
*
* @param {string} selector
* @return {NodeListOf<*>}
*/
getGridElements(selector) {
return this[gridElementSymbol].querySelectorAll(selector);
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datatable";
}
/**
* @return {void}
*/
disconnectedCallback() {
super.disconnectedCallback();
if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
this[resizeObserverSymbol].disconnect();
}
}
/**
* @return {void}
*/
connectedCallback() {
const self = this;
super.connectedCallback();
this[resizeObserverSymbol] = new ResizeObserver((entries) => {
updateGrid.call(self);
});
this[resizeObserverSymbol].observe(this.parentNode);
}
/**
* Get the row number of the selected rows as an array
*
* @returns {number[]}
*/
getSelectedRows() {
const rows = this.getGridElements(`[data-monster-role="select-row"]`);
const selectedRows = [];
rows.forEach((row) => {
if (row.checked) {
const key = row.parentNode.getAttribute("data-monster-insert-reference");
const index = key.split("-").pop();
selectedRows.push(parseInt(index, 10));
}
});
return selectedRows;
}
/**
* @return void
*/
[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 element = findElementWithSelectorUpwards(this, selector);
if (element === null) {
throw new Error("the selector must match exactly one element");
}
if (!isInstance(element, Datasource)) {
throw new TypeError("the element must be a datasource");
}
this[datasourceLinkedElementSymbol] = element;
queueMicrotask(() => {
handleDataSourceChanges.call(this);
element.datasource.attachObserver(
new Observer(handleDataSourceChanges.bind(this)),
);
});
}
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];
}
/**
* Copy a row from the datatable
*
* @param {number|string} fromIndex
* @param {number|string} toIndex
* @return {DataTable}
* @fires monster-datatable-row-copied
*/
copyRow(fromIndex, toIndex) {
const datasource = this[datasourceLinkedElementSymbol];
if (!datasource) {
return this;
}
let d = datasource.data;
let c = clone(d);
let rows = c;
const mapping = this.getOption("mapping.data");
if (mapping) {
rows = c?.[mapping];
}
if (rows === undefined || rows === null) {
rows = [];
}
if (toIndex === undefined) {
toIndex = rows.length;
}
if (isString(fromIndex)) {
fromIndex = parseInt(fromIndex);
}
if (isString(toIndex)) {
toIndex = parseInt(toIndex);
}
if (toIndex < 0 || toIndex > rows.length) {
throw new RangeError("index out of bounds");
}
validateArray(rows);
validateInteger(fromIndex);
validateInteger(toIndex);
if (fromIndex < 0 || fromIndex >= rows.length) {
throw new RangeError("index out of bounds");
}
rows.splice(toIndex, 0, clone(rows[fromIndex]));
datasource.data = c;
fireCustomEvent(this, "monster-datatable-row-copied", {
index: toIndex,
});
return this;
}
/**
* Remove a row from the datatable
*
* @param {number|string} index
* @return {DataTable}
* @fires monster-datatable-row-removed
*/
removeRow(index) {
const datasource = this[datasourceLinkedElementSymbol];
if (!datasource) {
return this;
}
let d = datasource.data;
let c = clone(d);
let rows = c;
const mapping = this.getOption("mapping.data");
if (mapping) {
rows = c?.[mapping];
}
if (rows === undefined || rows === null) {
rows = [];
}
if (isString(index)) {
index = parseInt(index);
}
validateArray(rows);
validateInteger(index);
if (index < 0 || index >= rows.length) {
throw new RangeError("index out of bounds");
}
if (mapping) {
rows = c?.[mapping];
}
rows.splice(index, 1);
datasource.data = c;
fireCustomEvent(this, "monster-datatable-row-removed", {
index: index,
});
return this;
}
/**
* Add a row to the datatable
*
* @param {Object} data
* @return {DataTable}
*
* @fires monster-datatable-row-added
**/
addRow(data) {
const datasource = this[datasourceLinkedElementSymbol];
if (!datasource) {
return this;
}
let d = datasource.data;
let c = clone(d);
let rows = c;
const mapping = this.getOption("mapping.data");
if (mapping) {
rows = c?.[mapping];
}
if (rows === undefined || rows === null) {
rows = [];
}
validateArray(rows);
validateObject(data);
rows.push(data);
datasource.data = c;
fireCustomEvent(this, "monster-datatable-row-added", {
index: rows.length - 1,
});
return this;
}
}
/**
* @private
* @return {string}
*/
function getColumnVisibilityConfigKey() {
return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
}
/**
* @private
* @return {string}
*/
function getFilterConfigKey() {
return generateUniqueConfigKey("datatable", this?.id, "filter");
}
/**
* @private
* @return {Promise}
*/
function getHostConfig(callback) {
const host = findElementWithSelectorUpwards(this, "monster-host");
if (!host) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "no host found");
return Promise.resolve({});
}
if (!this.id) {
addAttributeToken(
this,
ATTRIBUTE_ERRORMESSAGE,
"no id found; id is required for config",
);
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 host = findElementWithSelectorUpwards(this, "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;
const quoteOpenChar = this.getOption("copy.quoteOpen");
const quoteCloseChar = this.getOption("copy.quoteClose");
const delimiterChar = this.getOption("copy.delimiter");
const rowBreak = this.getOption("copy.rowBreak");
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();
queueMicrotask(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);
});
}
});
const eventHandlerDoubleClickCopyToClipboard = (event) => {
const element = findTargetElementFromEvent(event, "data-monster-head");
if (element) {
let text = "";
if (event.shiftKey) {
const index = element.getAttribute("data-monster-insert-reference");
if (index) {
const cols = self.getGridElements(
`[data-monster-insert-reference="${index}"]`,
);
const colTexts = [];
for (let i = 0; i < cols.length; i++) {
const col = cols[i];
if (
col.querySelector("monster-button-bar") ||
col.querySelector("monster-button")
) {
continue;
}
if (col.textContent) {
colTexts.push(
quoteOpenChar + col.textContent.trim() + quoteCloseChar,
);
}
}
text = colTexts.join(delimiterChar);
}
} else {
if (
element.querySelector("monster-button-bar") ||
element.querySelector("monster-button")
) {
return;
}
text = element.textContent.trim();
}
if (getWindow().navigator.clipboard && text) {
getWindow()
.navigator.clipboard.writeText(text)
.then(
() => {
},
(err) => {
},
);
}
}
};
if (self.getOption("features.doubleClickCopyToClipboard")) {
self[gridElementSymbol].addEventListener(
"dblclick",
eventHandlerDoubleClickCopyToClipboard,
);
}
if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) {
this[copyAllElementSymbol].addEventListener("click", (event) => {
event.preventDefault();
const table = [];
let currentRow = [];
let currentIndex = null;
const cols = self.getGridElements(`[data-monster-insert-reference]`);
const rowIndexes = new Map();
cols.forEach((col) => {
const index = col.getAttribute("data-monster-insert-reference");
rowIndexes.set(index, true);
});
rowIndexes.forEach((value, key) => {
const cols = self.getGridElements(
`[data-monster-insert-reference="${key}"]`,
);
for (let i = 0; i < cols.length; i++) {
const col = cols[i];
if (
col.querySelector("monster-button-bar") ||
col.querySelector("monster-button")
) {
continue;
}
if (col.textContent) {
currentRow.push(
quoteOpenChar + col.textContent.trim() + quoteCloseChar,
);
}
}
if (currentRow.length > 0) {
table.push(currentRow);
}
currentRow = [];
});
if (table.length > 0) {
const text = table.map((row) => row.join(delimiterChar)).join(rowBreak);
if (getWindow().navigator.clipboard && text) {
getWindow()
.navigator.clipboard.writeText(text)
.then(
() => {
},
(err) => {
},
);
}
}
});
}
const selectRowCallback = (event) => {
const element = findTargetElementFromEvent(event, "data-monster-role", "select-row");
if (element) {
const key = element.parentNode.getAttribute("data-monster-insert-reference");
const row = self.getGridElements(
`[data-monster-insert-reference="${key}"]`,
);
const index = key.split("-").pop();
if (element.checked) {
row.forEach((col) => {
col.classList.add("selected");
});
fireCustomEvent(self, "monster-datatable-row-selected", {
index: index
})
} else {
row.forEach((col) => {
col.classList.remove("selected");
});
fireCustomEvent(self, "monster-datatable-row-deselected", {
index: index
})
}
fireCustomEvent(this, "monster-datatable-selection-changed", {})
}
const rows = self.getGridElements(`[data-monster-role="select-row"]`);
const allSelected = Array.from(rows).every((row) => row.checked);
const selectAll = this[gridHeadersElementSymbol].querySelector(`[data-monster-role="select-all"]`);
selectAll.checked = allSelected;
}
this[gridElementSymbol].addEventListener("click", selectRowCallback);
this[gridElementSymbol].addEventListener("touch", selectRowCallback);
const selectAllCallback = (event) => {
const element = findTargetElementFromEvent(event, "data-monster-role", "select-all");
if (element) {
const mode = element.checked
const rows = this.getGridElements(`[data-monster-role="select-row"]`);
rows.forEach((row) => {
row.checked = mode;
});
if (mode) {
fireCustomEvent(this, "monster-datatable-all-rows-selected", {})
} else {
fireCustomEvent(this, "monster-datatable-all-rows-deselected", {})
}
fireCustomEvent(this, "monster-datatable-selection-changed", {})
}
}
this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback)
this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback)
}
/**
* @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 features = [];
if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) {
const features = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" ");
features.forEach((feature) => {
features.push(feature.trim());
if (feature === "select") {
label = "<input type='checkbox' data-monster-role='select-all' />";
while (row.firstChild) {
row.removeChild(row.firstChild);
}
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.setAttribute("data-monster-role", "select-row");
row.appendChild(checkbox);
}
});
}
const header = new Header();
header.setInternals({
field: field,
label: label,
classes: hClass,
index: i,
mode: mode,
grid: grid,
labelKey: labelKey,
direction: direction,
features: features,
});
headers.push(header);
}
this.setOption("headers", headers);
queueMicrotask(() => {
storeOrderStatement.call(this, this.getOption("features.autoInit"));
});
}
/**
* @private
* @returns {object}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case 'de':
return {
theListContainsNoEntries: "Die Liste enthält keine Einträge",
copyAll: "Alles kopieren",
helpText:
"<p>Sie können die Werte aus einzelnen Zeilen<br>" +
"in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" +
"<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" +
"Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>",
};
case 'fr':
return {
theListContainsNoEntries: "La liste ne contient aucune entrée",
copyAll: "Copier tout",
helpText:
"<p>Vous pouvez copier les valeurs des rangées individuelles<br>" +
"dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" +
"<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" +
"Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>",
};
case 'sp':
return {
theListContainsNoEntries: "La lista no contiene entradas",
copyAll: "Copiar todo",
helpText:
"<p>Puedes copiar los valores de filas individuales<br>" +
"al portapapeles haciendo doble clic en la columna correspondiente.</p>" +
"<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" +
"Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>",
};
case 'it':
return {
theListContainsNoEntries: "L'elenco non contiene voci",
copyAll: "Copia tutto",
helpText:
"<p>Puoi copiare i valori dalle singole righe<br>" +
"negli appunti facendo doppio clic sulla colonna relativa.</p>" +
"<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" +
"Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>",
};
case 'pl':
return {
theListContainsNoEntries: "Lista nie zawiera wpisów",
copyAll: "Kopiuj wszystko",
helpText:
"<p>Możesz skopiować wartości z poszczególnych wierszy<br>" +
"do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" +
"<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" +
"Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>",
};
case 'no':
return {
theListContainsNoEntries: "Listen inneholder ingen oppføringer",
copyAll: "Kopier alt",
helpText:
"<p>Du kan kopiere verdier fra enkeltrader<br>" +
"til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" +
"<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" +
"Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>",
};
case 'dk':
return {
theListContainsNoEntries: "Listen indeholder ingen poster",
copyAll: "Kopiér alt",
helpText:
"<p>Du kan kopiere værdier fra enkelte rækker<br>" +
"til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" +
"<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" +
"Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>",
};
case 'sw':
return {
theListContainsNoEntries: "Listan innehåller inga poster",
copyAll: "Kopiera allt",
helpText:
"<p>Du kan kopiera värden från enskilda rader<br>" +
"till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" +
"<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" +
"Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>",
};
case 'en':
default:
return {
theListContainsNoEntries: "The list contains no entries",
copyAll: "Copy all",
helpText:
"<p>You can copy the values from individual rows<br>" +
"to the clipboard by double-clicking on the relevant column.</p>" +
"<p>To copy an entire row, hold down the Shift key while clicking.<br>" +
"If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>",
};
}
}
/**
* @private
* @return {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 host = findElementWithSelectorUpwards(this, "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) {
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 = this.parentNode.clientWidth;
const breakpoint = this.getOption("responsive.breakpoint");
this[dataControlElementSymbol].classList.toggle(
"small",
bodyWidth <= 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 {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 {DataTable}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[dataControlElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=control]",
);
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");
this[copyAllElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=copy-all]",
);
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" fill="currentColor" 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" data-monster-attributes="class path:classes.control">
<template id="headers-row">
<div data-monster-attributes="class path:headers-row.classes,
data-monster-index path:headers-row.index"
data-monster-replace="path:headers-row.html"></div>
</template>
<slot></slot>
<div data-monster-attributes="class path:classes.container"
data-monster-role="table-container" part="table-container">
<div class="filter">
<slot name="filter"></slot>
</div>
<div class="bar">
<monster-context-help
data-monster-attributes="class path:features.help | ?::hidden"
data-monster-replace="path:labels.helpText"
></monster-context-help>
<a href="#" data-monster-attributes="class path:features.copyAll | ?::hidden"
data-monster-role="copy-all" data-monster-replace="path:labels.copyAll">Copy all</a>
<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);