From bd266eca8213587d4cbaa8a7e5edc42cb269859e Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Fri, 27 Dec 2024 12:48:17 +0100 Subject: [PATCH] fix: datatable, datasource optimisation #272 --- development/issues/open/226.html | 92 +- development/issues/open/272.html | 179 ++++ development/issues/open/272.mjs | 42 + development/mock/issue-272.js | 289 ++++++ source/components/datatable/dataset.mjs | 632 ++++++------ source/components/datatable/datasource.mjs | 38 +- .../components/datatable/datasource/dom.mjs | 38 +- .../components/datatable/datasource/rest.mjs | 930 +++++++++--------- .../datatable/embedded-pagination.mjs | 11 + source/components/datatable/pagination.mjs | 866 ++++++++-------- source/components/datatable/status.mjs | 2 +- .../datatable/style/pagination.pcss | 4 +- .../datatable/stylesheet/pagination.mjs | 21 +- source/components/datatable/util.mjs | 1 + source/components/host/config-manager.mjs | 4 +- source/data/datasource/server.mjs | 1 + source/data/datasource/server/restapi.mjs | 39 +- source/dom/customelement.mjs | 34 + source/types/has.mjs | 29 + 19 files changed, 1938 insertions(+), 1314 deletions(-) create mode 100644 development/issues/open/272.html create mode 100644 development/issues/open/272.mjs create mode 100644 development/mock/issue-272.js create mode 100644 source/types/has.mjs diff --git a/development/issues/open/226.html b/development/issues/open/226.html index 9b4547168..83878821a 100644 --- a/development/issues/open/226.html +++ b/development/issues/open/226.html @@ -5,93 +5,13 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Datatable Pagination um Max Count erweitern #226</title> <script src="./226.mjs" type="module"></script> - - <style> - - :not(:defined) { - display: none; - } - - .slide { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - } - - - - main { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - } - - monster-slider::part(prev) { - } - - monster-slider::part(control) { - - } - - .container { - padding: 10px; - width: 1200px; - height: 600px; - background-color: #cccccc; - box-sizing: border-box; - overflow: hidden; - } - - </style> </head> <body> - <h1>Datatable Pagination um Max Count erweitern #226</h1> - <p>user aborted</p> - <ul> - <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/226">Issue #226</a></li> - <li><a href="/">Back to overview</a></li> - </ul> - <main> - - <div class="container"> - <monster-slider> - <div slot="prev"><svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-arrow-left-square-fill" viewBox="0 0 16 16"> - <path d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2zm-4.5-6.5H5.707l2.147-2.146a.5.5 0 1 0-.708-.708l-3 3a.5.5 0 0 0 0 .708l3 3a.5.5 0 0 0 .708-.708L5.707 8.5H11.5a.5.5 0 0 0 0-1"/> - </svg></div> - <div class="slide" style="display:none;background-color: #ff6666;width:50px"> - <h1>SLIDE 1</h1> - </div> - <div class="slide" style="background-color: #ff6666;width:50px"> - <h1>SLIDE 1 a</h1> - </div> - <div class="slide" style="background-color: #66ff66;width:50px;"><svg xmlns="http://www.w3.org/2000/svg" - width="467" height="462"> - <rect x="80" y="60" width="250" height="250" rx="20" - style="fill:#ff0000; stroke:#000000;stroke-width:2px;" /> - - <rect x="140" y="120" width="250" height="250" rx="40" - style="fill:#0000ff; stroke:#000000; stroke-width:2px; - fill-opacity:0.7;" /> - </svg></div> - <div class="slide" style="background-color: #6666ff;width:50px;"> - - <h1>SLIDE 3</h1> - - </div> - <div slot="next"> - - <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-arrow-right-square-fill" viewBox="0 0 16 16"> - <path d="M0 14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2zm4.5-6.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5a.5.5 0 0 1 0-1"/> - </svg> - - </div> - </monster-slider> - </div> - - </main> +<h1>Datatable Pagination um Max Count erweitern #226</h1> +<ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/226">Issue #226</a></li> + <li><a href="/">Back to overview</a></li> +</ul> +<main></main> </body> </html> diff --git a/development/issues/open/272.html b/development/issues/open/272.html new file mode 100644 index 000000000..2fe8e354d --- /dev/null +++ b/development/issues/open/272.html @@ -0,0 +1,179 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>check and update data save button #272</title> + <script src="./272.mjs" type="module"></script> +</head> +<body> +<h1>check and update pagination and dataset #272</h1> +<p></p> +<ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/272">Issue #272</a></li> + <li><a href="/">Back to overview</a></li> +</ul> +<main> + + <monster-datasource-rest id="ds272-r" + data-monster-option-features-autoInit="true" + data-monster-option-write-url="/issue-272.json" + data-monster-option-write-acceptedstatus="400::200" + data-monster-option-read-url="/issue-272.json?limit=1&page=${page}" + data-x-monster-option-read-mapping-transformer="path:dataset" + ></monster-datasource-rest> + + + <monster-datasource-dom id="ds272-d"> + <script type="application/json"> + [ + { + "id": 1, + "username": "martin89", + "email": "elena.richards@domain.com", + "full_name": "Elena Richards", + "age": 29, + "country": "Greece", + "registered_date": "2019-11-23", + "status": "active" + }, + { + "id": 2, + "username": "sophiabell", + "email": "thomas.cook@gmail.com", + "full_name": "Thomas Cook", + "age": 22, + "country": "Portugal", + "registered_date": "2022-04-15", + "status": "banned" + }, + { + "id": 3, + "username": "brianm", + "email": "chloe.adams@webmail.com", + "full_name": "Chloe Adams", + "age": 37, + "country": "New Zealand", + "registered_date": "2021-06-01", + "status": "inactive" + }, + { + "id": 4, + "username": "larad", + "email": "alejandro.fuentes@yahoo.com", + "full_name": "Alejandro Fuentes", + "age": 45, + "country": "Japan", + "registered_date": "2024-01-20", + "status": "banned" + } + ] + + + </script> + + </monster-datasource-dom> + + <monster-button-bar id="bb-r"> + <monster-button>#1</monster-button> + <monster-button>#2</monster-button> + <monster-button>#3</monster-button> + <monster-button>#4</monster-button> + </monster-button-bar> + + <monster-pagination + data-monster-option-datasource-selector="#ds272-r" + data-monster-option-objectsperpage="1" + ></monster-pagination> + + <hr> + <monster-dataset + id="ds-272" + data-monster-option-datasource-selector="#ds272-r"> + + <div style="display: grid; grid-template-columns: 1fr 1fr;gap: 0.1rem;width: 600px"> + <div>ID</div> + <div data-monster-replace="path:data.id"></div> + <div>Username</div> + <div data-monster-replace="path:data.username"></div> + <div>Full Name</div> + <div data-monster-replace="path:data.full_name"></div> + <div>Email</div> + <div data-monster-replace="path:data.email"></div> + <div>Age</div> + <div data-monster-replace="path:data.age"></div> + <div>Country</div> + <div data-monster-replace="path:data.country"></div> + <div>Registered Date</div> + <div data-monster-replace="path:data.registered_date"></div> + <div>Status</div> + <div data-monster-replace="path:data.status"></div> + </div> + + </monster-dataset> + + + <monster-pagination + data-monster-option-datasource-selector="#ds272-r" + data-monster-option-objectsperpage="1" + ></monster-pagination> + + + <hr> + <hr> + <hr> + <hr> + + <monster-pagination + data-monster-option-datasource-selector="#ds272-d" + data-monster-option-objectsperpage="1" + ></monster-pagination> + + + <monster-button-bar id="bb-d"> + <monster-button>#1</monster-button> + <monster-button>#2</monster-button> + <monster-button>#3</monster-button> + <monster-button>#4</monster-button> + </monster-button-bar> + + <hr> + <monster-dataset + data-monster-option-mapping-data="" id="ds-272" + data-monster-option-datasource-selector="#ds272-d"> + + <div style="display: grid; grid-template-columns: 1fr 1fr;gap: 0.1rem;width: 600px"> + <div>ID</div> + <div data-monster-replace="path:data.id"></div> + <div>Username</div> + <div data-monster-replace="path:data.username"></div> + <div>Full Name</div> + <div data-monster-replace="path:data.full_name"></div> + <div>Email</div> + <div data-monster-replace="path:data.email"></div> + <div>Age</div> + <div data-monster-replace="path:data.age"></div> + <div>Country</div> + <div data-monster-replace="path:data.country"></div> + <div>Registered Date</div> + <div data-monster-replace="path:data.registered_date"></div> + <div>Status</div> + <div data-monster-replace="path:data.status"></div> + </div> + + </monster-dataset> + + + <monster-pagination + data-monster-option-datasource-selector="#ds272-d" + data-monster-option-objectsperpage="1" + ></monster-pagination> + + <hr> + <hr> + <hr> + <hr> + +</main> +</body> +</html> diff --git a/development/issues/open/272.mjs b/development/issues/open/272.mjs new file mode 100644 index 000000000..6a14260be --- /dev/null +++ b/development/issues/open/272.mjs @@ -0,0 +1,42 @@ +/** +* @file development/issues/open/272.mjs +* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/272 +* @description check and update data save button +* @issue 272 +*/ + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/link.pcss"; +import "../../../source/components/style/color.pcss"; +import "../../../source/components/style/theme.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; +import "../../../source/components/datatable/datasource/rest.mjs"; +import "../../../source/components/datatable/datasource/dom.mjs"; +import "../../../source/components/datatable/dataset.mjs"; +import "../../../source/components/datatable/pagination.mjs"; +import "../../../source/components/form/button.mjs"; +import "../../../source/components/form/button-bar.mjs"; + +// DOM-Elemente für die Seiten-Datensätze +const pageData1 = document.getElementById("ds272-d"); +const pageData2 = document.getElementById("ds272-r"); + +// Allgemeine Funktion zur Einstellung der Seitennummer +function setPage(dataElement, pageNum) { + dataElement.setParameters({page: pageNum}); +} + +// Initialisierung der Buttons und Event-Handler für die erste Datenquelle +const buttonBar1 = document.getElementById("bb-d"); +['first', 'second', 'third', 'fourth'].forEach((label, index) => { + const button = buttonBar1.children[index]; + button.setOption("actions.click", () => setPage(pageData1, index + 1)); +}); + +// Initialisierung der Buttons und Event-Handler für die zweite Datenquelle +const buttonBar2 = document.getElementById("bb-r"); +['first', 'second', 'third', 'fourth'].forEach((label, index) => { + const button = buttonBar2.children[index]; + button.setOption("actions.click", () => setPage(pageData2, index + 1)); +}); diff --git a/development/mock/issue-272.js b/development/mock/issue-272.js new file mode 100644 index 000000000..2c8acffdd --- /dev/null +++ b/development/mock/issue-272.js @@ -0,0 +1,289 @@ +const json = + `[{ + "id": 1, + "username": "wernerjennifer", + "email": "smithchristina@clark.biz", + "full_name": "Joshua Smith", + "age": 28, + "country": "Central African Republic", + "registered_date": "2020-01-17", + "status": "active" + }, + { + "id": 2, + "username": "elizabethmurphy", + "email": "beverlywarren@gmail.com", + "full_name": "Sarah Snyder", + "age": 18, + "country": "Tajikistan", + "registered_date": "2023-05-10", + "status": "banned" + }, + { + "id": 3, + "username": "ftaylor", + "email": "deckercassandra@conrad.com", + "full_name": "Rhonda Carroll", + "age": 34, + "country": "Australia", + "registered_date": "2022-07-31", + "status": "inactive" + }, + { + "id": 4, + "username": "antoniocastillo", + "email": "tevans@hotmail.com", + "full_name": "Brad Brown", + "age": 39, + "country": "Nepal", + "registered_date": "2023-01-09", + "status": "banned" + }, + { + "id": 5, + "username": "dylan46", + "email": "audreywalter@watts-conley.com", + "full_name": "Jacqueline Turner", + "age": 26, + "country": "United States Minor Outlying Islands", + "registered_date": "2024-10-14", + "status": "inactive" + }, + { + "id": 6, + "username": "michael27", + "email": "thompsonsydney@gmail.com", + "full_name": "Stephanie Walls", + "age": 25, + "country": "Switzerland", + "registered_date": "2020-05-20", + "status": "inactive" + }, + { + "id": 7, + "username": "vickie92", + "email": "mary35@jackson.com", + "full_name": "Colin Cohen", + "age": 23, + "country": "Saint Martin", + "registered_date": "2022-12-31", + "status": "active" + }, + { + "id": 8, + "username": "fordomar", + "email": "hernandezbrian@gmail.com", + "full_name": "Christopher Garza", + "age": 65, + "country": "Antigua and Barbuda", + "registered_date": "2022-08-15", + "status": "banned" + }, + { + "id": 9, + "username": "jerry82", + "email": "nwhite@yahoo.com", + "full_name": "Gabrielle Garza", + "age": 46, + "country": "Mauritania", + "registered_date": "2023-03-08", + "status": "inactive" + }, + { + "id": 10, + "username": "alyssa54", + "email": "kevin11@hotmail.com", + "full_name": "Andrea Williams", + "age": 43, + "country": "Western Sahara", + "registered_date": "2023-04-19", + "status": "active" + }, + { + "id": 11, + "username": "jacob30", + "email": "scottmary@yahoo.com", + "full_name": "Nicole Cunningham", + "age": 29, + "country": "French Polynesia", + "registered_date": "2022-02-21", + "status": "banned" + }, + { + "id": 12, + "username": "ftucker", + "email": "simmonsrichard@jones.com", + "full_name": "Daniel Shelton", + "age": 42, + "country": "Cameroon", + "registered_date": "2024-04-20", + "status": "active" + }, + { + "id": 13, + "username": "alvarezpaul", + "email": "ucannon@ortega.biz", + "full_name": "Matthew Poole", + "age": 44, + "country": "Chad", + "registered_date": "2021-09-22", + "status": "inactive" + }, + { + "id": 14, + "username": "sarahunter", + "email": "allisoncharles@phillips-graves.com", + "full_name": "Nathan Fernandez", + "age": 40, + "country": "French Polynesia", + "registered_date": "2023-04-11", + "status": "inactive" + }, + { + "id": 15, + "username": "sierra65", + "email": "hnicholson@gmail.com", + "full_name": "Michael Taylor", + "age": 27, + "country": "Indonesia", + "registered_date": "2021-08-15", + "status": "active" + }, + { + "id": 16, + "username": "benjamin84", + "email": "zgallegos@hale-johnson.com", + "full_name": "Molly Santana", + "age": 32, + "country": "Holy See (Vatican City State)", + "registered_date": "2023-06-09", + "status": "inactive" + }, + { + "id": 17, + "username": "peckpaul", + "email": "james03@hotmail.com", + "full_name": "Kelly Allen", + "age": 42, + "country": "Austria", + "registered_date": "2021-04-26", + "status": "active" + }, + { + "id": 18, + "username": "jill42", + "email": "williamstephenson@hotmail.com", + "full_name": "Samuel Adkins", + "age": 22, + "country": "American Samoa", + "registered_date": "2024-10-04", + "status": "active" + }, + { + "id": 19, + "username": "pottssavannah", + "email": "emily76@mckinney.com", + "full_name": "Jeremy Pearson", + "age": 42, + "country": "Lesotho", + "registered_date": "2020-06-15", + "status": "inactive" + }, + { + "id": 20, + "username": "robertsdaniel", + "email": "robinsontodd@pineda.org", + "full_name": "Francis Castaneda", + "age": 58, + "country": "British Indian Ocean Territory (Chagos Archipelago)", + "registered_date": "2020-03-01", + "status": "inactive" + } + ] +`; + + +// check if JSON is valid +const objects = JSON.parse(json) +if (!Array.isArray(objects)) { + throw new Error('Invalid JSON') +} + +const requestDelay = 10 + +function wrapJsonWithPagination(data, page, limit) { + return JSON.stringify({ + dataset: data, + sys: { + pagination: { + objectsPerPage: limit, + pages: Math.ceil(objects.length / limit), + currentPage: page + } + } + }) +} + +export default [ + { + url: '/issue-272.json', + method: 'get', + rawResponse: async (req, res) => { + res.setHeader('Content-Type', 'application/json') + res.statusCode = 200 + + const url = new URL(req.url, `http://${req.headers.host}`) + const q = Object.fromEntries(url.searchParams) + + let filtered = objects + + if (q) { + + let limit = 20 + + if (q.limit) { + limit = parseInt(q.limit) + } + + if (q.page) { + const page = parseInt(q.page) + const start = (page - 1) * limit + const end = start + limit + filtered = objects.slice(start, end) + } else { + filtered = objects.slice(0, limit) + } + + if (q.q) { + const query = q.q.toLowerCase() + filtered = objects.filter(item => item.name.toLowerCase().includes(query)) + + } + + setTimeout(function () { + res.end(wrapJsonWithPagination(filtered, q.page || 1, limit)) + }, requestDelay); + + return + } + + setTimeout(function () { + res.end(wrapJsonWithPagination(filtered, 1, 20)) + }, requestDelay); + + }, + }, + { + url: '/issue-272.json', + method: 'post', + rawResponse: async (req, res) => { + res.setHeader('Content-Type', 'application/json') + res.statusCode = 200 + + const jsonRespond = JSON.stringify({message: "Data has been successfully saved"}) + + res.end(jsonRespond) + } + } + +]; \ No newline at end of file diff --git a/source/components/datatable/dataset.mjs b/source/components/datatable/dataset.mjs index 97b8dc2ec..015d41f7b 100644 --- a/source/components/datatable/dataset.mjs +++ b/source/components/datatable/dataset.mjs @@ -12,290 +12,312 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { instanceSymbol, internalSymbol } from "../../constants.mjs"; -import { Pathfinder } from "../../data/pathfinder.mjs"; -import { getLinkedObjects, hasObjectLink } from "../../dom/attributes.mjs"; -import { customElementUpdaterLinkSymbol } from "../../dom/constants.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {Pathfinder} from "../../data/pathfinder.mjs"; import { - assembleMethodSymbol, - CustomElement, - attributeObserverSymbol, - registerCustomElement, + assembleMethodSymbol, + CustomElement, + attributeObserverSymbol, + registerCustomElement, } from "../../dom/customelement.mjs"; -import { findElementWithSelectorUpwards } from "../../dom/util.mjs"; -import { isString } from "../../types/is.mjs"; -import { Observer } from "../../types/observer.mjs"; -import { clone } from "../../util/clone.mjs"; +import {findElementWithSelectorUpwards} from "../../dom/util.mjs"; +import {isString} from "../../types/is.mjs"; +import {Observer} from "../../types/observer.mjs"; import { - ATTRIBUTE_DATASOURCE_SELECTOR, - ATTRIBUTE_DATATABLE_INDEX, + ATTRIBUTE_DATASOURCE_SELECTOR, + ATTRIBUTE_DATATABLE_INDEX, } from "./constants.mjs"; -import { Datasource } from "./datasource.mjs"; -import { DatasetStyleSheet } from "./stylesheet/dataset.mjs"; +import {Datasource} from "./datasource.mjs"; +import {DatasetStyleSheet} from "./stylesheet/dataset.mjs"; import { - handleDataSourceChanges, - datasourceLinkedElementSymbol, + handleDataSourceChanges, + datasourceLinkedElementSymbol, } from "./util.mjs"; -import { FormStyleSheet } from "../stylesheet/form.mjs"; +import {FormStyleSheet} from "../stylesheet/form.mjs"; -export { DataSet }; +export {DataSet}; /** - * The data set component is used to show the data of a data source. + * A data set component * - * <img src="./images/dataset.png"> + * @fragments /fragments/components/datatable/dataset * - * You can create this control either by specifying the HTML tag <monster-dataset />` directly in the HTML or using - * Javascript via the `document.createElement('monster-dataset');` method. + * @example /examples/components/datatable/dataset-simple + * @example /examples/components/datatable/dataset-rest * - * ```html - * <monster-dataset></monster-dataset> - * ``` - * - * Or you can create this CustomControl directly in Javascript: - * - * ```js - * import '@schukai/component-datatable/source/dataset.mjs'; - * document.createElement('monster-dataset'); - * ``` - * - * The Body should have a class "hidden" to ensure that the styles are applied correctly. - * - * ```css - * body.hidden { - * visibility: hidden; - * } - * ``` - * - * @startuml dataset.png - * skinparam monochrome true - * skinparam shadowing false - * HTMLElement <|-- CustomElement - * CustomElement <|-- DataSet - * @enduml + * @issue https://localhost.alvine.dev:8443/development/issues/closed/272.html * * @copyright schukai GmbH - * @summary A data set + * @summary A dataset component that can be used to show the data of a data source */ class DataSet extends CustomElement { - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/dataset@@instance"); - } - - /** - * This method determines which attributes are to be monitored by `attributeChangedCallback()`. - * - * @return {string[]} - * @since 1.15.0 - */ - static get observedAttributes() { - const attributes = super.observedAttributes; - attributes.push(ATTRIBUTE_DATATABLE_INDEX); - return attributes; - } - - /** - * 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 The datasource - * @property {string} datasource.selector The selector of the datasource - * @property {object} mapping The mapping - * @property {string} mapping.data The data - * @property {number} mapping.index The index - * @property {Array} data The data - */ - get defaults() { - const obj = Object.assign({}, super.defaults, { - templates: { - main: getTemplate(), - }, - - datasource: { - selector: null, - }, - - mapping: { - data: "dataset", - index: 0, - }, - - features: { - /** - * @since 3.70.0 - * @type {boolean} - */ - refreshOnMutation: true, - }, - - /** - * @since 3.70.0 - * @type {boolean} - */ - refreshOnMutation: { - selector: "input, select, textarea", - }, - - data: {}, - }); - - updateOptionsFromArguments.call(this, obj); - return obj; - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-dataset"; - } - - /** - * This method is called when the component is created. - * @since 3.70.0 - * @return {DataSet} - */ - refresh() { - // makes sure that handleDataSourceChanges is called - this.setOption("data", {}); - return this; - } - - /** - * - * @return {Promise<unknown>} - */ - write() { - return new Promise((resolve, reject) => { - if (!this[datasourceLinkedElementSymbol]) { - reject(new Error("No datasource")); - return; - } - - const internalUpdateCloneData = this.getInternalUpdateCloneData(); - if (!internalUpdateCloneData) { - reject(new Error("No update data")); - return; - } - - const internalData = internalUpdateCloneData?.["data"]; - if ( - internalData === undefined || - internalData === null || - internalData === "" - ) { - reject(new Error("No data")); - return; - } - - queueMicrotask(() => { - const path = this.getOption("mapping.data"); - const index = this.getOption("mapping.index"); - - let pathWithIndex; - - if (isString(path) && path !== "") { - pathWithIndex = path + "." + index; - } else { - pathWithIndex = String(index); - } - - const data = this[datasourceLinkedElementSymbol]?.data; - if (!data) { - reject(new Error("No data")); - return; - } - - const unref = JSON.stringify(data); - const ref = JSON.parse(unref); - - new Pathfinder(ref).setVia(pathWithIndex, internalData); - - this[datasourceLinkedElementSymbol].data = ref; - - resolve(); - }); - }); - } - - /** - * This method is responsible for assembling the component. - * - * It calls the parent's assemble method first, then initializes control references and event handlers. - * If the `datasource.selector` option is provided and is a string, it searches for the corresponding - * element in the DOM using that selector. - * - * If the selector matches exactly one element, it checks if the element is an instance of the `Datasource` class. - * - * If it is, the component's `datasourceLinkedElementSymbol` property is set to the element, and the component - * attaches an observer to the datasource's changes. - * - * The observer is a function that calls the `handleDataSourceChanges` method in the context of the component. - * Additionally, the component attaches an observer to itself, which also calls the `handleDataSourceChanges` - * method in the component's context. - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initEventHandler.call(this); - - if (!this[datasourceLinkedElementSymbol]) { - 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 (!(element instanceof Datasource)) { - throw new TypeError("the element must be a datasource"); - } - - this[datasourceLinkedElementSymbol] = element; - element.datasource.attachObserver( - new Observer(handleDataSourceChanges.bind(this)), - ); - } else { - throw new Error("the selector must be a string"); - } - } - - if ( - this.getOption("features.refreshOnMutation") && - this.getOption("refreshOnMutation.selector") - ) { - initMutationObserver.call(this); - } - } - - /** - * @return [CSSStyleSheet] - */ - static getCSSStyleSheet() { - return [FormStyleSheet, DatasetStyleSheet]; - } + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/dataset@@instance"); + } + + /** + * This method determines which attributes are to be monitored by `attributeChangedCallback()`. + * + * @return {string[]} + * @since 1.15.0 + */ + static get observedAttributes() { + const attributes = super.observedAttributes; + attributes.push(ATTRIBUTE_DATATABLE_INDEX); + attributes.push("data-monster-option-mapping-index"); + return attributes; + } + + /** + * 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 The datasource + * @property {string} datasource.selector The selector of the datasource + * @property {object} mapping The mapping + * @property {string} mapping.data The data + * @property {number} mapping.index The index + * @property {object} features The features + * @property {boolean} features.refreshOnMutation Refresh on mutation + * @property {object} refreshOnMutation The refresh on mutation + * @property {string} refreshOnMutation.selector The selector + */ + get defaults() { + const obj = Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + + datasource: { + selector: null, + }, + + mapping: { + data: "dataset", + index: 0, + }, + + features: { + /** + * @since 3.70.0 + * @type {boolean} + */ + refreshOnMutation: true, + }, + + /** + * @since 3.70.0 + * @type {boolean} + */ + refreshOnMutation: { + selector: "input, select, textarea", + }, + + data: {}, + }); + + updateOptionsFromArguments.call(this, obj); + return obj; + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-dataset"; + } + + /** + * This method is called when the component is created. + * @since 3.70.0 + * @return {DataSet} + */ + refresh() { + // makes sure that handleDataSourceChanges is called + this.setOption("data", {}); + return this; + } + + /** + * + * @return {Promise<unknown>} + */ + write() { + return new Promise((resolve, reject) => { + if (!this[datasourceLinkedElementSymbol]) { + reject(new Error("No datasource")); + return; + } + + const internalUpdateCloneData = this.getInternalUpdateCloneData(); + if (!internalUpdateCloneData) { + reject(new Error("No update data")); + return; + } + + const internalData = internalUpdateCloneData?.["data"]; + if ( + internalData === undefined || + internalData === null || + internalData === "" + ) { + reject(new Error("No data")); + return; + } + + queueMicrotask(() => { + const path = this.getOption("mapping.data"); + const index = this.getOption("mapping.index"); + + let pathWithIndex; + + if (isString(path) && path !== "") { + pathWithIndex = path + "." + index; + } else { + pathWithIndex = String(index); + } + + const data = this[datasourceLinkedElementSymbol]?.data; + if (!data) { + reject(new Error("No data")); + return; + } + + const unref = JSON.stringify(data); + const ref = JSON.parse(unref); + + new Pathfinder(ref).setVia(pathWithIndex, internalData); + + this[datasourceLinkedElementSymbol].data = ref; + + resolve(); + }); + }); + } + + /** + * This method is responsible for assembling the component. + * + * It calls the parent's assemble method first, then initializes control references and event handlers. + * If the `datasource.selector` option is provided and is a string, it searches for the corresponding + * element in the DOM using that selector. + * + * If the selector matches exactly one element, it checks if the element is an instance of the `Datasource` class. + * + * If it is, the component's `datasourceLinkedElementSymbol` property is set to the element, and the component + * attaches an observer to the datasource's changes. + * + * The observer is a function that calls the `handleDataSourceChanges` method in the context of the component. + * Additionally, the component attaches an observer to itself, which also calls the `handleDataSourceChanges` + * method in the component's context. + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + requestAnimationFrame(() => { + + if (!this[datasourceLinkedElementSymbol]) { + 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 (!(element instanceof Datasource)) { + throw new TypeError("the element must be a datasource"); + } + + this[datasourceLinkedElementSymbol] = element; + element.datasource.attachObserver( + new Observer(handleDataSourceChanges.bind(this)), + ); + + handleDataSourceChanges.call(this); + } else { + throw new Error("the selector must be a string"); + } + } + + if ( + this.getOption("features.refreshOnMutation") && + this.getOption("refreshOnMutation.selector") + ) { + initMutationObserver.call(this); + } + + initEventHandler.call(this); + + }); + + } + + /** + * @return [CSSStyleSheet] + */ + static getCSSStyleSheet() { + return [FormStyleSheet, DatasetStyleSheet]; + } } /** * @private */ function initEventHandler() { - this[attributeObserverSymbol][ATTRIBUTE_DATATABLE_INDEX] = () => { - const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); - if (index) { - this.setOption("mapping.index", parseInt(index, 10)); - } - }; + + this[attributeObserverSymbol][ATTRIBUTE_DATATABLE_INDEX] = () => { // @deprecated use data-monster-option-mapping-index + const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); + if (index) { + this.setOption("mapping.index", parseInt(index, 10)); + handleDataSourceChanges.call(this); + } + }; + + this[attributeObserverSymbol]["data-monster-option-mapping-index"] = () => { + const index = this.getAttribute("data-monster-option-mapping-index"); + if (index !== null && index !== undefined && index !== "") { + this.setOption("mapping.index", parseInt(index, 10)); + handleDataSourceChanges.call(this); + } + }; + + if (this[datasourceLinkedElementSymbol]) { + + this[datasourceLinkedElementSymbol].datasource.attachObserver( + new Observer(() => { + const page = this[datasourceLinkedElementSymbol]?.currentPage(); + if (page !== null && page !== undefined && page !== "") { + const index = parseInt(page, 10) - 1; + this.setOption("mapping.index", index); + handleDataSourceChanges.call(this); + } + }), + ); + + this[datasourceLinkedElementSymbol].attachObserver( + new Observer(() => { + const page = this[datasourceLinkedElementSymbol]?.currentPage(); + if (page !== null && page !== undefined && page !== "") { + const index = parseInt(page, 10) - 1; + this.setOption("mapping.index", index); + handleDataSourceChanges.call(this); + } + }), + ); + + handleDataSourceChanges.call(this); + } + } /** @@ -303,56 +325,56 @@ function initEventHandler() { * @param {Object} options */ function updateOptionsFromArguments(options) { - const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); + const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); // @deprecated use data-monster-option-mapping-index - if (index !== null && index !== undefined) { - options.mapping.index = parseInt(index, 10); - } + if (index !== null && index !== undefined) { + options.mapping.index = parseInt(index, 10); + } - const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); + const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); - if (selector) { - options.datasource.selector = selector; - } + if (selector) { + options.datasource.selector = selector; + } } /** * @private */ function initMutationObserver() { - const config = { attributes: false, childList: true, subtree: true }; - - const callback = (mutationList, observer) => { - if (mutationList.length === 0) { - return; - } - - let doneFlag = false; - for (const mutation of mutationList) { - if (mutation.type === "childList") { - for (const node of mutation.addedNodes) { - if ( - node instanceof HTMLElement && - node.matches(this.getOption("refreshOnMutation.selector")) - ) { - doneFlag = true; - break; - } - } - - if (doneFlag) { - break; - } - } - } - - if (doneFlag) { - this.refresh(); - } - }; - - const observer = new MutationObserver(callback); - observer.observe(this, config); + const config = {attributes: false, childList: true, subtree: true}; + + const callback = (mutationList, observer) => { + if (mutationList.length === 0) { + return; + } + + let doneFlag = false; + for (const mutation of mutationList) { + if (mutation.type === "childList") { + for (const node of mutation.addedNodes) { + if ( + node instanceof HTMLElement && + node.matches(this.getOption("refreshOnMutation.selector")) + ) { + doneFlag = true; + break; + } + } + + if (doneFlag) { + break; + } + } + } + + if (doneFlag) { + this.refresh(); + } + }; + + const observer = new MutationObserver(callback); + observer.observe(this, config); } /** @@ -360,8 +382,8 @@ function initMutationObserver() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div data-monster-role="control" part="control"> <slot></slot> </div> diff --git a/source/components/datatable/datasource.mjs b/source/components/datatable/datasource.mjs index c69326a45..09c9d4c52 100644 --- a/source/components/datatable/datasource.mjs +++ b/source/components/datatable/datasource.mjs @@ -30,21 +30,16 @@ const dataSourceSymbol = Symbol.for( ); /** - * The Datasource component is a basic class for the datatable component. + * A datasource * - * <img src="./images/datasource.png"> + * @fragments /fragments/components/datatable/datasource * - * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library + * @example /examples/components/datatable/datasource * - * @startuml datasource.png - * skinparam monochrome true - * skinparam shadowing false - * HTMLElement <|-- CustomElement - * CustomElement <|-- Datasource - * @enduml + * @issue https://localhost.alvine.dev:8443/development/issues/closed/272.html * * @copyright schukai GmbH - * @summary A abstract datasource + * @summary A generic datasource */ class Datasource extends CustomElement { /** @@ -77,8 +72,7 @@ class Datasource extends CustomElement { } /** - * - * @return {Monster.Components.Form.Form} + * @return {void} */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); @@ -93,7 +87,7 @@ class Datasource extends CustomElement { } /** - * set the data + * set the data with proxy * @param {Object} data */ set data(data) { @@ -101,18 +95,32 @@ class Datasource extends CustomElement { } /** - * Get the datasource - * @return {Monster.Data.Datasource} + * Get the base datasource + * @return {Datasource} */ get datasource() { return this[dataSourceSymbol]; } + /** + * Wrapper for the write method of the datasource + */ write() { this[dataSourceSymbol].write(); } + /** + * Wrapper for the read method of the datasource + */ read() { this[dataSourceSymbol].read(); } + + /** + * @return {int} + * @throws {Error} this method must be implemented by derived classes. + */ + currentPage() { + throw new Error("this method must be implemented by derived classes"); + } } diff --git a/source/components/datatable/datasource/dom.mjs b/source/components/datatable/datasource/dom.mjs index 42269e03a..5004dc8a9 100644 --- a/source/components/datatable/datasource/dom.mjs +++ b/source/components/datatable/datasource/dom.mjs @@ -53,6 +53,8 @@ class Dom extends Datasource { * * @property {Object} templates Template definitions * @property {string} templates.main Main template + * @property {Object} features Feature definitions + * @property {boolean} features.autoInit Automatically initializes the component */ get defaults() { return Object.assign({}, super.defaults, { @@ -63,11 +65,19 @@ class Dom extends Datasource { features: { autoInit: true, }, + + /** @private */ + sys: { + pagination: { + pages: 1, + objectsPerPage: 10, + currentPage: 1, + } + } }); } /** - * * @return {void} */ [assembleMethodSymbol]() { @@ -76,6 +86,17 @@ class Dom extends Datasource { updateDataSource.call(this); } + /** + * This method set the current page of the pagination + * + * @param {string} page + * @return {Dom} + */ + setParameters({page}) { + this.setOption("sys.pagination.currentPage", page); + return this; + } + /** * * @return {CSSStyleSheet[]} @@ -108,6 +129,13 @@ class Dom extends Datasource { updateDataSource.call(this); } } + + /** + * @return {int} + */ + currentPage() { + return this.getOption("sys.pagination.currentPage"); + } } /** @@ -170,7 +198,15 @@ function updateDataSource() { data = []; } + // set pagination + this.setOption("sys.pagination.objectsPerPage", 1 ); + this.setOption("sys.pagination.pages", data.length); + this.setOption("sys.pagination.currentPage", 1); + + /** call setter */ this.data = data; + + } /** diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs index cb0deebc0..51681f246 100644 --- a/source/components/datatable/datasource/rest.mjs +++ b/source/components/datatable/datasource/rest.mjs @@ -12,27 +12,27 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { diff } from "../../../data/diff.mjs"; -import { addAttributeToken } from "../../../dom/attributes.mjs"; -import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs"; -import { isArray } from "../../../types/is.mjs"; -import { Datasource, dataSourceSymbol } from "../datasource.mjs"; -import { DatasourceStyleSheet } from "../stylesheet/datasource.mjs"; -import { instanceSymbol } from "../../../constants.mjs"; +import {diff} from "../../../data/diff.mjs"; +import {addAttributeToken} from "../../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; +import {isArray} from "../../../types/is.mjs"; +import {Datasource, dataSourceSymbol} from "../datasource.mjs"; +import {DatasourceStyleSheet} from "../stylesheet/datasource.mjs"; +import {instanceSymbol} from "../../../constants.mjs"; import { - assembleMethodSymbol, - registerCustomElement, + assembleMethodSymbol, + registerCustomElement, } from "../../../dom/customelement.mjs"; -import { RestAPI } from "../../../data/datasource/server/restapi.mjs"; -import { Formatter } from "../../../text/formatter.mjs"; -import { clone } from "../../../util/clone.mjs"; -import { validateBoolean } from "../../../types/validate.mjs"; -import { findElementWithIdUpwards } from "../../../dom/util.mjs"; -import { Observer } from "../../../types/observer.mjs"; -import { Pathfinder } from "../../../data/pathfinder.mjs"; -import { fireCustomEvent } from "../../../dom/events.mjs"; +import {RestAPI} from "../../../data/datasource/server/restapi.mjs"; +import {Formatter} from "../../../text/formatter.mjs"; +import {clone} from "../../../util/clone.mjs"; +import {validateBoolean} from "../../../types/validate.mjs"; +import {findElementWithIdUpwards} from "../../../dom/util.mjs"; +import {Observer} from "../../../types/observer.mjs"; +import {Pathfinder} from "../../../data/pathfinder.mjs"; +import {fireCustomEvent} from "../../../dom/events.mjs"; -export { Rest }; +export {Rest}; /** * @private @@ -46,7 +46,7 @@ const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler"); * @type {symbol} */ const rawDataSymbol = Symbol.for( - "@schukai/monster/data/datasource/server/restapi/rawdata", + "@schukai/monster/data/datasource/server/restapi/rawdata", ); /** @@ -54,7 +54,7 @@ const rawDataSymbol = Symbol.for( * @type {symbol} */ const intersectionObserverObserverSymbol = Symbol( - "intersectionObserverObserver", + "intersectionObserverObserver", ); /** @@ -64,396 +64,427 @@ const intersectionObserverObserverSymbol = Symbol( const filterObserverSymbol = Symbol("filterObserver"); /** - * The Datasource component is a basic class for the datatable component. + * A rest api datasource * - * <img src="./images/rest.png"> + * @fragments /fragments/components/datatable/datasource/rest * - * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library + * @example /examples/components/datatable/datasource-rest-simple + * @example /examples/components/datatable/datasource-rest-auto-init + * @example /examples/components/datatable/datasource-rest-do-fetch * - * @startuml rest.png - * skinparam monochrome true - * skinparam shadowing false - * HTMLElement <|-- CustomElement - * CustomElement <|-- Datasource - * Datasource <|-- Rest - * @enduml + * @issue https://localhost.alvine.dev:8443/development/issues/closed/272.html * * @copyright schukai GmbH - * @summary A rest api datasource + * @summary A rest api datasource for the datatable or other components */ - - - class Rest extends Datasource { - /** - * the constructor of the class - */ - constructor() { - super(); - this[dataSourceSymbol] = new RestAPI(); - } - - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/datasource/rest@@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} features Feature definitions - * @property {boolean} features.autoInit If true, the component is initialized automatically - * @property {boolean} features.filter If true, the component is initialized automatically - * @property {Object} autoInit Auto init definitions - * @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically - * @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once - * @property {Object} filter Filter definitions - * @property {string} filter.id The id of the filter control - * @property {Object} datatable Datatable definitions - * @property {string} datatable.id The id of the datatable control - * @property {Object} response Response definitions - * @property {Object} response.path Path definitions (changed in 3.56.0) - * @property {string} response.path.message Path to the message (changed in 3.56.0) - * @property {Object} read Read configuration - * @property {string} read.url The url of the rest api - * @property {string} read.method The method of the rest api - * @property {Object} read.parameters The parameters of the rest api - * @property {Object} read.parameters.filter The filter of the rest api - * @property {Object} read.parameters.orderBy The order by of the rest api - * @property {Object} read.parameters.page The page of the rest api - * @property {Object} write Write configuration - */ - get defaults() { - const restOptions = new RestAPI().defaults; - - restOptions.read.parameters = { - filter: undefined, - oderBy: undefined, - page: "1", - }; - - return Object.assign({}, super.defaults, restOptions, { - templates: { - main: getTemplate(), - }, - - features: { - autoInit: false, - filter: false, - }, - - autoInit: { - intersectionObserver: false, - oneTime: true, - }, - - filter: { - id: undefined, - }, - - datatable: { - id: undefined, - }, - - response: { - path: { - message: "sys.message", - code: "sys.code", - }, - }, - }); - } - - /** - * - * @param {string} page - * @param {string} query - * @param {string} orderBy - * @return {Rest} - */ - setParameters({ page, query, orderBy }) { - const parameters = this.getOption("read.parameters"); - if (query !== undefined) { - parameters.query = `${query}`; - parameters.page = "1"; - } - - // after a query the page is set to 1, so if the page is not set, it is set to 1 - if (page !== undefined) parameters.page = `${page}`; - if (orderBy !== undefined) parameters.order = `${orderBy}`; - this.setOption("read.parameters", parameters); - return this; - } - - /** - * @return {void} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initEventHandler.call(this); - initAutoInit.call(this); - } - - /** - * @deprecated 2023-06-25 - * @return {Promise<never>|*} - */ - reload() { - return this.fetch(); - } - - /** - * Fetches the data from the rest api - * @return {Promise<never>|*} - */ - fetch() { - const opt = clone(this.getOption("read")); - this[dataSourceSymbol].setOption("read", opt); - - let url = this.getOption("read.url"); - const formatter = new Formatter(this.getOption("read.parameters")); - - if (!url) { - return Promise.reject(new Error("No url defined")); - } - - url = formatter.format(url); - - this[dataSourceSymbol].setOption("read.url", url); - - return new Promise((resolve, reject) => { - fireCustomEvent(this, "monster-datasource-fetch", { - datasource: this, - }); - - queueMicrotask(() => { - this[dataSourceSymbol] - .read() - .then((response) => { - fireCustomEvent(this, "monster-datasource-fetched", { - datasource: this, - }); - - resolve(response); - }) - .catch((error) => { - fireCustomEvent(this, "monster-datasource-error", { - error: error, - }); - - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); - reject(error); - }); - }); - }); - } - - /** - * - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [DatasourceStyleSheet]; - } - - /** - * @private - * @return {string} - */ - static getTag() { - return "monster-datasource-rest"; - } - - /** - * This method activates the intersection observer manually. - * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`. - * - * @return {Monster.Components.Datatable.Datasource.Rest} - */ - initIntersectionObserver() { - initIntersectionObserver.call(this); - return this; - } - - /** - * @private - */ - connectedCallback() { - super.connectedCallback(); - - queueMicrotask(() => { - if (this.getOption("features.filter", false) === true) { - initFilter.call(this); - } - }); - } - - /** - * @private - */ - disconnectedCallback() { - super.disconnectedCallback(); - removeFilter.call(this); - } - - /** - * @return {Promise<never>|*} - */ - read() { - return this.fetch(); - } - - /** - * Fetches the data from the rest api - * @return {Promise<never>|*} - */ - write() { - const opt = clone(this.getOption("write")); - this[dataSourceSymbol].setOption("write", opt); - - let url = this.getOption("write.url"); - const formatter = new Formatter(this.getOption("write.parameters")); - - if (!url) { - return Promise.reject(new Error("No url defined")); - } - - url = formatter.format(url); - - this[dataSourceSymbol].setOption("write.url", url); - - return new Promise((resolve, reject) => { - fireCustomEvent(this, "monster-datasource-fetch", { - datasource: this, - }); - - queueMicrotask(() => { - this[dataSourceSymbol] - .write() - .then((response) => { - fireCustomEvent(this, "monster-datasource-fetched", { - datasource: this, - }); - - resolve(response); - }) - .catch((error) => { - fireCustomEvent(this, "monster-datasource-error", { - error: error, - }); - - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); - reject(error); - }); - }); - }); - } + /** + * the constructor of the class + */ + constructor() { + super(); + this[dataSourceSymbol] = new RestAPI(); + } + + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/datasource/rest@@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} features Feature definitions + * @property {boolean} features.autoInit If true, the component is initialized automatically + * @property {boolean} features.filter If true, the component is initialized automatically + * @property {Object} autoInit Auto init definitions + * @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically + * @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once + * @property {Object} filter Filter definitions + * @property {string} filter.id The id of the filter control + * @property {Object} response Response definitions + * @property {Object} response.path Path definitions (changed in 3.56.0) + * @property {string} response.path.message Path to the message (changed in 3.56.0) + * @property {Object} read Read configuration + * @property {string} read.url The url of the rest api + * @property {string} read.method The method of the rest api + * @property {Object} read.parameters The parameters of the rest api + * @property {Object} read.parameters.filter The filter of the rest api + * @property {Object} read.parameters.orderBy The order by of the rest api + * @property {Object} read.parameters.page The page of the rest api + * @property {string} read.mapping.currentPage The current page + * @property {Object} write Write configuration + * @property {string} write.url The url of the rest api + * @property {string} write.method The method of the rest api + * @property {Object} write Write configuration + */ + get defaults() { + const restOptions = new RestAPI().defaults; + + restOptions.read.parameters = { + filter: null, + oderBy: null, + page: "1", + }; + + restOptions.read.mapping.currentPage = "sys.pagination.currentPage"; + + return Object.assign({}, super.defaults, restOptions, { + templates: { + main: getTemplate(), + }, + + features: { + autoInit: false, + filter: false, + }, + + autoInit: { + intersectionObserver: false, + oneTime: true, + }, + + filter: { + id: null, + }, + + /*datatable: { + id: undefined, // not used? + }, */ + + response: { + path: { + message: "sys.message", + code: "sys.code", + }, + }, + }); + } + + /** + * With this method, you can set the parameters for the rest api. The parameters are + * used for building the url. + * + * @param {string} page + * @param {string} query + * @param {string} orderBy + * @return {Rest} + */ + setParameters({page, query, orderBy}) { + const parameters = this.getOption("read.parameters"); + if (query !== undefined) { + parameters.query = `${query}`; + parameters.page = "1"; + } + + // after a query the page is set to 1, so if the page is not set, it is set to 1 + if (page !== undefined) parameters.page = `${page}`; + if (orderBy !== undefined) parameters.order = `${orderBy}`; + this.setOption("read.parameters", parameters); + return this; + } + + /** + * @private + * @return {void} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initEventHandler.call(this); + initAutoInit.call(this); + } + + /** + * This method reloads the data from the rest api, this method is deprecated. + * You should use the method `read` instead. + * + * @deprecated 2023-06-25 + * @return {Promise<never>|*} + */ + reload() { + return this.read(); + } + + /** + * Fetches the data from the rest api, this method is deprecated. + * You should use the method `read` instead. + * + * @deprecated 2024-12-24 + * @return {Promise<never>|*} + */ + fetch() { + return this.read(); + } + + /** + * + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [DatasourceStyleSheet]; + } + + /** + * @private + * @return {string} + */ + static getTag() { + return "monster-datasource-rest"; + } + + /** + * This method activates the intersection observer manually. + * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`. + * + * @return {Rest} + */ + initIntersectionObserver() { + initIntersectionObserver.call(this); + return this; + } + + /** + * @private + */ + connectedCallback() { + super.connectedCallback(); + + queueMicrotask(() => { + if (this.getOption("features.filter", false) === true) { + initFilter.call(this); + } + }); + } + + /** + * @private + */ + disconnectedCallback() { + super.disconnectedCallback(); + removeFilter.call(this); + } + + /** + * This method reads the data from the rest api. + * The data is stored in the internal dataset object. + * + * @return {Promise} + * @fires monster-datasource-fetch + * @fires monster-datasource-fetched + * @fires monster-datasource-error + */ + read() { + const opt = clone(this.getOption("read")); + this[dataSourceSymbol].setOption("read", opt); + + let url = this.getOption("read.url"); + const formatter = new Formatter(this.getOption("read.parameters")); + + if (!url) { + return Promise.reject(new Error("No url defined")); + } + + url = formatter.format(url); + + this[dataSourceSymbol].setOption("read.url", url); + + return new Promise((resolve, reject) => { + fireCustomEvent(this, "monster-datasource-fetch", { + datasource: this, + }); + + queueMicrotask(() => { + this[dataSourceSymbol] + .read() + .then((response) => { + fireCustomEvent(this, "monster-datasource-fetched", { + datasource: this, + }); + + resolve(response); + }) + .catch((error) => { + fireCustomEvent(this, "monster-datasource-error", { + error: error, + }); + + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); + reject(error); + }); + }); + }); + } + + /** + * Fetches the data from the rest api. + * @return {Promise} + */ + write() { + const opt = clone(this.getOption("write")); + this[dataSourceSymbol].setOption("write", opt); + + let url = this.getOption("write.url"); + const formatter = new Formatter(this.getOption("write.parameters")); + + if (!url) { + return Promise.reject(new Error("No url defined")); + } + + url = formatter.format(url); + + this[dataSourceSymbol].setOption("write.url", url); + + return new Promise((resolve, reject) => { + fireCustomEvent(this, "monster-datasource-fetch", { + datasource: this, + }); + + queueMicrotask(() => { + this[dataSourceSymbol] + .write() + .then((response) => { + fireCustomEvent(this, "monster-datasource-fetched", { + datasource: this, + }); + + resolve(response); + }) + .catch((error) => { + fireCustomEvent(this, "monster-datasource-error", { + error: error, + }); + + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString()); + reject(error); + }); + }); + }); + } + + // /** + // * @return {int} + // */ + // currentPage() { + // + // const key = this.getOption("read.mapping.currentPage") + // if (key === undefined) { + // return 1; + // } + // + // const pf = new Pathfinder(this.data); + // if (pf.exists(key)) { + // return parseInt(pf.getVia(key), 10); + // } + // + // return 1; + // + // } + } /** * @private */ function removeFilter() { - const filterID = this.getOption("filter.id", undefined); - if (!filterID) return; + const filterID = this.getOption("filter.id", undefined); + if (!filterID) return; - const filterControl = findElementWithIdUpwards(this, filterID); + const filterControl = findElementWithIdUpwards(this, filterID); - if (filterControl && this[filterObserverSymbol]) { - filterControl?.detachObserver(this[filterObserverSymbol]); - } + if (filterControl && this[filterObserverSymbol]) { + filterControl?.detachObserver(this[filterObserverSymbol]); + } } /** * @private */ function initFilter() { - const filterID = this.getOption("filter.id", undefined); - - if (!filterID) - throw new Error("filter feature is enabled but no filter id is defined"); - - const filterControl = findElementWithIdUpwards(this, filterID); - if (!filterControl) - throw new Error( - "filter feature is enabled but no filter control with id " + - filterID + - " is found", - ); - - this[filterObserverSymbol] = new Observer(() => { - const query = filterControl.getOption("query"); - if (query === undefined) { - return; - } - this.setParameters({ query: query }); - this.fetch() - .then((response) => { - if (!(response instanceof Response)) { - throw new Error("Response is not an instance of Response"); - } - - if (response?.ok === true) { - this.dispatchEvent(new CustomEvent("reload", { bubbles: true })); - filterControl?.showSuccess(); - } - - if (response.bodyUsed === true) { - return handleIntersectionObserver.call( - this, - response[rawDataSymbol], - response, - filterControl, - ); - } - - response - .text() - .then((jsonAsText) => { - let json; - try { - json = JSON.parse(jsonAsText); - } catch (e) { - const message = e instanceof Error ? e.message : `${e}`; - filterControl?.showFailureMessage(message); - return Promise.reject(e); - } - - return handleIntersectionObserver.call( - this, - json, - response, - filterControl, - ); - }) - .catch((e) => { - filterControl?.showFailureMessage(e.message); - }); - }) - .catch((e) => { - this.dispatchEvent( - new CustomEvent("error", { bubbles: true, detail: e }), - ); - - if (!(e instanceof Error)) { - e = new Error(e); - } - - filterControl?.showFailureMessage(e.message); - return Promise.reject(e); - }); - }); - - filterControl.attachObserver(this[filterObserverSymbol]); + const filterID = this.getOption("filter.id", undefined); + + if (!filterID) + throw new Error("filter feature is enabled but no filter id is defined"); + + const filterControl = findElementWithIdUpwards(this, filterID); + if (!filterControl) + throw new Error( + "filter feature is enabled but no filter control with id " + + filterID + + " is found", + ); + + this[filterObserverSymbol] = new Observer(() => { + const query = filterControl.getOption("query"); + if (query === undefined) { + return; + } + this.setParameters({query: query}); + this.fetch() + .then((response) => { + if (!(response instanceof Response)) { + throw new Error("Response is not an instance of Response"); + } + + if (response?.ok === true) { + this.dispatchEvent(new CustomEvent("reload", {bubbles: true})); + filterControl?.showSuccess(); + } + + if (response.bodyUsed === true) { + return handleIntersectionObserver.call( + this, + response[rawDataSymbol], + response, + filterControl, + ); + } + + response + .text() + .then((jsonAsText) => { + let json; + try { + json = JSON.parse(jsonAsText); + } catch (e) { + const message = e instanceof Error ? e.message : `${e}`; + filterControl?.showFailureMessage(message); + return Promise.reject(e); + } + + return handleIntersectionObserver.call( + this, + json, + response, + filterControl, + ); + }) + .catch((e) => { + filterControl?.showFailureMessage(e.message); + }); + }) + .catch((e) => { + this.dispatchEvent( + new CustomEvent("error", {bubbles: true, detail: e}), + ); + + if (!(e instanceof Error)) { + e = new Error(e); + } + + filterControl?.showFailureMessage(e.message); + return Promise.reject(e); + }); + }); + + filterControl.attachObserver(this[filterObserverSymbol]); } /** @@ -464,88 +495,89 @@ function initFilter() { * @returns {Promise<never>|Promise<Awaited<unknown>>} */ function handleIntersectionObserver(json, response, filterControl) { - const path = new Pathfinder(json); - - const codePath = this.getOption("response.path.code"); - - if (path.exists(codePath)) { - const code = `${path.getVia(codePath)}`; - if (code && code === "200") { - filterControl?.showSuccess(); - return Promise.resolve(response); - } - - const messagePath = this.getOption("response.path.message"); - if (path.exists(messagePath)) { - const message = path.getVia(messagePath); - filterControl?.showFailureMessage(message); - return Promise.reject(new Error(message)); - } - - return Promise.reject(new Error("Response code is not 200")); - } + const path = new Pathfinder(json); + + const codePath = this.getOption("response.path.code"); + + if (path.exists(codePath)) { + const code = `${path.getVia(codePath)}`; + if (code && code === "200") { + filterControl?.showSuccess(); + return Promise.resolve(response); + } + + const messagePath = this.getOption("response.path.message"); + if (path.exists(messagePath)) { + const message = path.getVia(messagePath); + filterControl?.showFailureMessage(message); + return Promise.reject(new Error(message)); + } + + return Promise.reject(new Error("Response code is not 200")); + } } /** * @private */ function initAutoInit() { - const autoInit = this.getOption("features.autoInit"); - validateBoolean(autoInit); + const autoInit = this.getOption("features.autoInit"); + validateBoolean(autoInit); - if (autoInit !== true) return; + if (autoInit !== true) return; - if (this.getOption("autoInit.intersectionObserver") === true) { - initIntersectionObserver.call(this); - return; - } + if (this.getOption("autoInit.intersectionObserver") === true) { + initIntersectionObserver.call(this); + return; + } - queueMicrotask(() => { - this.fetch().catch(() => {}); - }); + queueMicrotask(() => { + this.fetch().catch(() => { + }); + }); } /** * @private */ function initEventHandler() { - this[intersectionObserverHandlerSymbol] = (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - if (entry.intersectionRatio > 0) { - this.fetch(); - } - - // only load once - if ( - this.getOption("autoInit.oneTime") === true && - this[intersectionObserverObserverSymbol] !== undefined - ) { - this[intersectionObserverObserverSymbol].unobserve(this); - } - } - }); - }; + this[intersectionObserverHandlerSymbol] = (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (entry.intersectionRatio > 0) { + this.fetch(); + } + + // only load once + if ( + this.getOption("autoInit.oneTime") === true && + this[intersectionObserverObserverSymbol] !== undefined + ) { + this[intersectionObserverObserverSymbol].unobserve(this); + } + } + }); + }; } /** * @private */ function initIntersectionObserver() { - this.classList.add("intersection-observer"); + this.classList.add("intersection-observer"); - const options = { - root: null, - rootMargin: "0px", - threshold: 0.1, - }; + const options = { + root: null, + rootMargin: "0px", + threshold: 0.1, + }; - this[intersectionObserverObserverSymbol] = new IntersectionObserver( - this[intersectionObserverHandlerSymbol], - options, - ); + this[intersectionObserverObserverSymbol] = new IntersectionObserver( + this[intersectionObserverHandlerSymbol], + options, + ); - this[intersectionObserverObserverSymbol].observe(this); + this[intersectionObserverObserverSymbol].observe(this); } /** @@ -553,8 +585,8 @@ function initIntersectionObserver() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <slot></slot>`; } diff --git a/source/components/datatable/embedded-pagination.mjs b/source/components/datatable/embedded-pagination.mjs index 02fc8b121..befdb068c 100644 --- a/source/components/datatable/embedded-pagination.mjs +++ b/source/components/datatable/embedded-pagination.mjs @@ -42,10 +42,21 @@ class EmbeddedPagination extends Pagination { return Symbol.for("@schukai/monster/components/embedded-pagination"); } + /** + * @private + */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); } + /** + * + * @property {Object} classes Class definitions + * @property {string} classes.spinner Spinner class + * @property {string} classes.spinnerContainer Spinner container class + * @property {string} classes.error Error class + * @property {string} classes.errorContainer Error container class + */ get defaults() { return Object.assign({}, super.defaults, { classes: { diff --git a/source/components/datatable/pagination.mjs b/source/components/datatable/pagination.mjs index 41d64542a..b3eed0e66 100644 --- a/source/components/datatable/pagination.mjs +++ b/source/components/datatable/pagination.mjs @@ -13,32 +13,32 @@ */ import { - assembleMethodSymbol, - CustomElement, - registerCustomElement, + assembleMethodSymbol, + CustomElement, + registerCustomElement, } from "../../dom/customelement.mjs"; -import { findElementWithSelectorUpwards, getWindow } from "../../dom/util.mjs"; -import { DeadMansSwitch } from "../../util/deadmansswitch.mjs"; -import { ThemeStyleSheet } from "../stylesheet/theme.mjs"; -import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs"; -import { Datasource } from "./datasource.mjs"; -import { Observer } from "../../types/observer.mjs"; -import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; -import { findTargetElementFromEvent } from "../../dom/events.mjs"; -import { PaginationStyleSheet } from "./stylesheet/pagination.mjs"; -import { DisplayStyleSheet } from "../stylesheet/display.mjs"; -import { isString } from "../../types/is.mjs"; -import { Pathfinder } from "../../data/pathfinder.mjs"; -import { instanceSymbol } from "../../constants.mjs"; -import { Formatter } from "../../text/formatter.mjs"; +import {findElementWithSelectorUpwards, getWindow} from "../../dom/util.mjs"; +import {DeadMansSwitch} from "../../util/deadmansswitch.mjs"; +import {ThemeStyleSheet} from "../stylesheet/theme.mjs"; +import {ATTRIBUTE_DATASOURCE_SELECTOR} from "./constants.mjs"; +import {Datasource} from "./datasource.mjs"; +import {Observer} from "../../types/observer.mjs"; +import {ATTRIBUTE_ROLE} from "../../dom/constants.mjs"; +import {findTargetElementFromEvent} from "../../dom/events.mjs"; +import {PaginationStyleSheet} from "./stylesheet/pagination.mjs"; +import {DisplayStyleSheet} from "../stylesheet/display.mjs"; +import {isString} from "../../types/is.mjs"; +import {Pathfinder} from "../../data/pathfinder.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {Formatter} from "../../text/formatter.mjs"; import "../form/select.mjs"; -import { addAttributeToken } from "../../dom/attributes.mjs"; -import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs"; +import {addAttributeToken} from "../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../dom/constants.mjs"; import "./datasource/dom.mjs"; import "./datasource/rest.mjs"; -export { Pagination }; +export {Pagination}; /** * @private @@ -71,215 +71,227 @@ const sizeDataSymbol = Symbol("sizeData"); const debounceSizeSymbol = Symbol("debounceSize"); /** - * The Pagination component + * A Pagination component + * + * @fragments /fragments/components/datatable/pagination + * + * @example /examples/components/datatable/pagination-simple * * @copyright schukai GmbH * @summary The Pagination component is used to show the current page and the total number of pages. */ class Pagination extends CustomElement { - /** - */ - constructor() { - super(); - this[datasourceLinkedElementSymbol] = null; - } - - /** - * This method is called by the `instanceof` operator. - * @return {symbol} - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/pagination"); - } - - /** - * 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 Datasource selector - * @property {Object} labels Label definitions - * @property {string} labels.page Page label - * @property {string} labels.description Description label - * @property {string} labels.previous Previous label - * @property {string} labels.next Next label - * @property {string} labels.of Of label - * @property {string} href Href - * @property {number} currentPage Current page - * @property {number} pages Pages - * @property {number} objectsPerPage Objects per page - * @property {Object} mapping Mapping - * @property {string} mapping.pages Pages mapping - * @property {string} mapping.objectsPerPage Objects per page mapping - * @property {string} mapping.currentPage Current page mapping - * @property {Object} pagination Pagination - */ - get defaults() { - return Object.assign( - {}, - super.defaults, - { - templates: { - main: getTemplate(), - }, - - datasource: { - selector: null, - }, - - labels: { - page: "${page}", - description: "Page ${page}", - previous: "Previous", - next: "Next", - of: "of", - }, - - href: "page-${page}", - - currentPage: undefined, - pages: undefined, - objectsPerPage: 20, - - mapping: { - pages: "sys.pagination.pages", - objectsPerPage: "sys.pagination.objectsPerPage", - currentPage: "sys.pagination.currentPage", - }, - - pagination: { - items: [], - }, - }, - initOptionsFromArguments.call(this), - ); - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-pagination"; - } - - /** - * @return {void} - */ - disconnectedCallback() { - super.disconnectedCallback(); - if (this?.[resizeObserverSymbol] instanceof ResizeObserver) { - this[resizeObserverSymbol].disconnect(); - } - } - - /** - * @return {void} - */ - connectedCallback() { - super.connectedCallback(); - - const parentNode = this.parentNode; - if (!parentNode) { - return; - } - - const parentParentNode = parentNode?.parentNode || parentNode; - - const parentWidth = parentParentNode.offsetWidth; - const ownWidth = this.offsetWidth; - - this[sizeDataSymbol] = { - last: { - parentWidth: parentParentNode.offsetWidth || 0, - }, - showNumbers: ownWidth < parentWidth, - }; - - handleDataSourceChanges.call(this); - - setTimeout(() => { - this[resizeObserverSymbol] = new ResizeObserver((entries) => { - if (this[debounceSizeSymbol] instanceof DeadMansSwitch) { - try { - this[debounceSizeSymbol].touch(); - return; - } catch (e) { - delete this[debounceSizeSymbol]; - } - } - - this[debounceSizeSymbol] = new DeadMansSwitch(250, () => { - queueMicrotask(() => { - const parentWidth = parentParentNode.offsetWidth; - const ownWidth = this.clientWidth; - - if (this[sizeDataSymbol]?.last?.parentWidth === parentWidth) { - return; - } - - this[sizeDataSymbol].last = { - parentWidth: parentWidth, - }; - - this[sizeDataSymbol].showNumbers = ownWidth < parentWidth; - handleDataSourceChanges.call(this); - }); - }); - }); - - this[resizeObserverSymbol].observe(this?.parentNode?.parentNode); - }, 500); - } - - /** - * @return {void} - */ - [assembleMethodSymbol]() { - 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 (!(element instanceof Datasource)) { - throw new TypeError("the element must be a datasource"); - } - - this[datasourceLinkedElementSymbol] = element; - element.datasource.attachObserver( - new Observer(handleDataSourceChanges.bind(this)), - ); - - handleDataSourceChanges.call(this); - } - } - - /** - * @private - * @return {CSSStyleSheet} - */ - static getControlCSSStyleSheet() { - return PaginationStyleSheet; - } - - /** - * @return {CSSStyleSheet[]} - */ - static getCSSStyleSheet() { - return [this.getControlCSSStyleSheet(), DisplayStyleSheet, ThemeStyleSheet]; - } + /** + */ + constructor() { + super(); + this[datasourceLinkedElementSymbol] = null; + } + + /** + * This method is called by the `instanceof` operator. + * @return {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/pagination"); + } + + /** + * 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 Datasource selector + * @property {Object} labels Label definitions + * @property {string} labels.page Page label + * @property {string} labels.description Description label + * @property {string} labels.previous Previous label + * @property {string} labels.next Next label + * @property {string} labels.of Of label + * @property {string} href Href + * @property {number} currentPage Current page + * @property {number} pages Pages + * @property {number} objectsPerPage Objects per page + * @property {Object} mapping Mapping + * @property {string} mapping.pages Pages mapping + * @property {string} mapping.objectsPerPage Objects per page mapping + * @property {string} mapping.currentPage Current page mapping + */ + get defaults() { + return Object.assign( + {}, + super.defaults, + { + templates: { + main: getTemplate(), + }, + + datasource: { + selector: null, + }, + + labels: { + page: "${page}", + description: "Page ${page}", + previous: "Previous", + next: "Next", + of: "of", + }, + + href: "page-${page}", + + pages: null, + objectsPerPage: 20, + currentPage: null, + + mapping: { + pages: "sys.pagination.pages", + objectsPerPage: "sys.pagination.objectsPerPage", + currentPage: "sys.pagination.currentPage", + }, + + /* @private */ + pagination: { + items: [], + }, + }, + initOptionsFromArguments.call(this), + ); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-pagination"; + } + + /** + * @return {void} + */ + disconnectedCallback() { + super.disconnectedCallback(); + if (this?.[resizeObserverSymbol] instanceof ResizeObserver) { + this[resizeObserverSymbol].disconnect(); + } + } + + /** + * @return {void} + */ + connectedCallback() { + super.connectedCallback(); + + const parentNode = this.parentNode; + if (!parentNode) { + return; + } + + try { + handleDataSourceChanges.call(this); + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e?.message || `${e}`); + } + + requestAnimationFrame(() => { + const parentParentNode = parentNode?.parentNode || parentNode; + + const parentWidth = parentParentNode.offsetWidth; + const ownWidth = this.offsetWidth; + + this[sizeDataSymbol] = { + last: { + parentWidth: 0, + }, + showNumbers: ownWidth < parentWidth, + }; + + this[resizeObserverSymbol] = new ResizeObserver((entries) => { + if (this[debounceSizeSymbol] instanceof DeadMansSwitch) { + try { + this[debounceSizeSymbol].touch(); + return; + } catch (e) { + delete this[debounceSizeSymbol]; + } + } + + this[debounceSizeSymbol] = new DeadMansSwitch(250, () => { + queueMicrotask(() => { + const parentWidth = parentParentNode.offsetWidth; + const ownWidth = this.clientWidth; + + if (this[sizeDataSymbol]?.last?.parentWidth === parentWidth) { + return; + } + + this[sizeDataSymbol].last = { + parentWidth: parentWidth, + }; + + this[sizeDataSymbol].showNumbers = ownWidth <= parentWidth; + handleDataSourceChanges.call(this); + }); + }); + }); + + this[resizeObserverSymbol].observe(this?.parentNode?.parentNode); + }); + } + + /** + * @return {void} + */ + [assembleMethodSymbol]() { + 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 (!(element instanceof Datasource)) { + throw new TypeError("the element must be a datasource"); + } + + this[datasourceLinkedElementSymbol] = element; + element.datasource.attachObserver( + new Observer(handleDataSourceChanges.bind(this)), + ); + + element.attachObserver( + new Observer(handleDataSourceChanges.bind(this)), + ); + + handleDataSourceChanges.call(this); + } + } + + /** + * @private + * @return {CSSStyleSheet} + */ + static getControlCSSStyleSheet() { + return PaginationStyleSheet; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [this.getControlCSSStyleSheet(), DisplayStyleSheet, ThemeStyleSheet]; + } } /** @@ -288,87 +300,88 @@ class Pagination extends CustomElement { * @throws {Error} no shadow-root is defined */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } - this[paginationElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=pagination]", - ); + this[paginationElementSymbol] = this.shadowRoot.querySelector( + "[data-monster-role=pagination]", + ); } /** * @private */ function initEventHandler() { - const self = this; - - self[paginationElementSymbol].addEventListener("click", function (event) { - let element = null; - const datasource = self[datasourceLinkedElementSymbol]; - if (!datasource) { - return; - } - - element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "pagination-item", - ); - if (!element) { - element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "pagination-next", - ); - if (!element) { - element = findTargetElementFromEvent( - event, - ATTRIBUTE_ROLE, - "pagination-prev", - ); - if (!element) { - return; - } - } - } - - if (!(element instanceof HTMLElement)) { - return; - } - - let page = null; - - if (!element.hasAttribute("data-page-no")) { - return; - } - - page = element.getAttribute("data-page-no"); - event.preventDefault(); - - if ( - !page || - page === "" || - page === null || - page === undefined || - page === "undefined" || - page === "null" - ) { - return; - } - - if (typeof datasource.setParameters !== "function") { - return; - } - - datasource.setParameters({ page }); - - if (typeof datasource.reload !== "function") { - return; - } - - datasource.reload(); - }); + const self = this; + + self[paginationElementSymbol].addEventListener("click", function (event) { + let element = null; + const datasource = self[datasourceLinkedElementSymbol]; + if (!datasource) { + return; + } + + element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "pagination-item", + ); + + if (!element) { + element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "pagination-next", + ); + if (!element) { + element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "pagination-prev", + ); + if (!element) { + return; + } + } + } + + if (!(element instanceof HTMLElement)) { + return; + } + + let page = null; + + if (!element.hasAttribute("data-page-no")) { + return; + } + + page = element.getAttribute("data-page-no"); + + if ( + !page || + page === "" || + page === null || + page === undefined || + page === "undefined" || + page === "null" + ) { + return; + } + + if (typeof datasource.setParameters !== "function") { + return; + } + + event.preventDefault(); + datasource.setParameters({page}); + + if (typeof datasource.reload !== "function") { + return; + } + + datasource.reload(); + }); } /** @@ -384,53 +397,58 @@ function initEventHandler() { * @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 options = {}; + const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR); + if (selector) { + options.datasource = {selector: selector}; + } - return options; + return options; } /** * @private */ function handleDataSourceChanges() { - let pagination; - - if (!this[datasourceLinkedElementSymbol]) { - return; - } - - const mapping = this.getOption("mapping"); - for (const key in mapping) { - const path = mapping[key]; - - let value; - try { - value = new Pathfinder(this[datasourceLinkedElementSymbol].data).getVia( - path, - ); - this.setOption(key, value); - } catch (e) { - addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); - } - } - - pagination = buildPagination.call( - this, - this.getOption("currentPage"), - this.getOption("pages"), - ); - - if (this?.[sizeDataSymbol]?.showNumbers !== true) { - pagination.items = []; - } - - getWindow().requestAnimationFrame(() => { - this.setOption("pagination", pagination); - }); + let pagination; + + if (!this[datasourceLinkedElementSymbol]) { + return; + } + + const mapping = this.getOption("mapping"); + const pf = new Pathfinder(this[datasourceLinkedElementSymbol].data); + + for (const key in mapping) { + const path = mapping[key]; + + if (pf.exists(path)) { + const value = pf.getVia( + path, + ); + this.setOption(key, value); + } + + const o = this[datasourceLinkedElementSymbol].getOption(path); + if (o !== undefined && o !== null) { + this.setOption(key, o); + } + + } + + pagination = buildPagination.call( + this, + this.getOption("currentPage"), + this.getOption("pages"), + ); + + if (this?.[sizeDataSymbol]?.showNumbers !== true) { + pagination.items = []; + } + + getWindow().requestAnimationFrame(() => { + this.setOption("pagination", pagination); + }); } /** @@ -440,88 +458,92 @@ function handleDataSourceChanges() { * @return {object} */ function buildPagination(current, max) { - let prev = current === 1 ? null : current - 1; - let next = current === max ? null : current + 1; - const itemList = [1]; - - if (current > 4) itemList.push("…"); - - const r = 2; - const r1 = current - r; - const r2 = current + r; - - for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) itemList.push(i); - - if (r2 + 1 < max) itemList.push("…"); - if (r2 < max) itemList.push(max); - - let prevClass = ""; - - if (prev === null) { - prevClass = " disabled"; - } - - let nextClass = ""; - if (next === null) { - nextClass = " disabled"; - } - - const items = itemList.map((item) => { - const p = `${item}`; - const c = `${current}`; - - const obj = { - pageNo: item, // as integer - page: p, // as string - current: p === c, - class: (p === c ? "current" : "").trim(), - }; - - if (p === "…") { - obj.class += " disabled".trim(); - } - - const formatter = new Formatter(obj); - - obj.description = formatter.format(this.getOption("labels.description")); - obj.label = formatter.format(this.getOption("labels.page")); - obj.href = - p === "…" - ? "#" - : p === c - ? "#" - : p === "1" - ? "#" - : `#${formatter.format(this.getOption("href"))}`; - return obj; - }); - - const nextNo = next; - next = `${next}`; - - const nextHref = - next === "null" - ? "#" - : `#${new Formatter({ page: next }).format(this.getOption("href"))}`; - const prevNo = prev; - prev = `${prev}`; - const prevHref = - prev === "null" - ? "#" - : `#${new Formatter({ page: prev }).format(this.getOption("href"))}`; - - return { - current, - nextNo, - next, - nextClass, - nextHref, - prevNo, - prev, - prevClass, - prevHref, - items, - }; + + current = parseInt(current, 10); + max = parseInt(max, 10); + + let prev = current === 1 ? null : current - 1; + let next = current === max ? null : current + 1; + const itemList = [1]; + + if (current > 4) itemList.push("…"); + + const r = 2; + const r1 = current - r; + const r2 = current + r; + + for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) itemList.push(i); + + if (r2 + 1 < max) itemList.push("…"); + if (r2 < max) itemList.push(max); + + let prevClass = ""; + + if (prev === null) { + prevClass = " disabled"; + } + + let nextClass = ""; + if (next === null) { + nextClass = " disabled"; + } + + const items = itemList.map((item) => { + const p = `${item}`; + const c = `${current}`; + + const obj = { + pageNo: item, // as integer + page: p, // as string + current: p === c, + class: (p === c ? "current" : "").trim(), + }; + + if (p === "…") { + obj.class += " disabled".trim(); + } + + const formatter = new Formatter(obj); + + obj.description = formatter.format(this.getOption("labels.description")); + obj.label = formatter.format(this.getOption("labels.page")); + obj.href = + p === "…" + ? "#" + : p === c + ? "#" + : p === "1" + ? "#" + : `#${formatter.format(this.getOption("href"))}`; + return obj; + }); + + const nextNo = next; + next = `${next}`; + + const nextHref = + next === "null" + ? "#" + : `#${new Formatter({page: next}).format(this.getOption("href"))}`; + const prevNo = prev; + prev = `${prev}`; + const prevHref = + prev === "null" + ? "#" + : `#${new Formatter({page: prev}).format(this.getOption("href"))}`; + + return { + current, + nextNo, + next, + nextClass, + nextHref, + prevNo, + prev, + prevClass, + prevHref, + items, + }; } /** @@ -529,8 +551,8 @@ function buildPagination(current, max) { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <template id="items"> <li><a data-monster-attributes="class path:items.class, href path:items.href, diff --git a/source/components/datatable/status.mjs b/source/components/datatable/status.mjs index 99240e660..046d0543b 100644 --- a/source/components/datatable/status.mjs +++ b/source/components/datatable/status.mjs @@ -89,7 +89,7 @@ class DatasourceStatus extends CustomElement { */ static get [instanceSymbol]() { return Symbol.for( - "@schukai/monster/components/datatables/status@@instance", + "@schukai/monster/components/datatable/status@@instance", ); } diff --git a/source/components/datatable/style/pagination.pcss b/source/components/datatable/style/pagination.pcss index 1ebe044b7..6ca49352c 100644 --- a/source/components/datatable/style/pagination.pcss +++ b/source/components/datatable/style/pagination.pcss @@ -33,9 +33,9 @@ & ul li a { @mixin button; - background-color: var(--monster-theme-control-bg-color); + background-color: var(--monster-bg-color-primary-1); color: var(--monster-theme-control-color); - border-color: var(--monster-theme-control-bg-color); + border-color: var(--monster-bg-color-primary-1); width: max-content; &.current { diff --git a/source/components/datatable/stylesheet/pagination.mjs b/source/components/datatable/stylesheet/pagination.mjs index 43de9782f..fe03e3e99 100644 --- a/source/components/datatable/stylesheet/pagination.mjs +++ b/source/components/datatable/stylesheet/pagination.mjs @@ -10,10 +10,10 @@ * For more information about purchasing a commercial license, please contact schukai GmbH. */ -import { addAttributeToken } from "../../../dom/attributes.mjs"; -import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs"; +import {addAttributeToken} from "../../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; -export { PaginationStyleSheet }; +export {PaginationStyleSheet} /** * @private @@ -22,17 +22,10 @@ export { PaginationStyleSheet }; const PaginationStyleSheet = new CSSStyleSheet(); try { - PaginationStyleSheet.insertRule( - ` + PaginationStyleSheet.insertRule(` @layer pagination { -:where(html){line-height:1.15;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}:where(h1){font-size:2em;margin-block-end:.67em;margin-block-start:.67em}:where(dl,ol,ul) :where(dl,ol,ul){margin-block-end:0;margin-block-start:0}:where(hr){box-sizing:content-box;color:inherit;height:0}:where(abbr[title]){text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,pre,samp){font-family:monospace,monospace;font-size:1em}:where(small){font-size:80%}:where(table){border-color:currentColor;text-indent:0}:where(button,input,select){margin:0}:where(button){text-transform:none}:where(button,input:is([type=button i],[type=reset i],[type=submit i])){-webkit-appearance:button}:where(progress){vertical-align:baseline}:where(select){text-transform:none}:where(textarea){margin:0}:where(input[type=search i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(button,input:is([type=button i],[type=color i],[type=reset i],[type=submit i]))::-moz-focus-inner{border-style:none;padding:0}:where(button,input:is([type=button i],[type=color i],[type=reset i],[type=submit i]))::-moz-focusring{outline:1px dotted ButtonText}:where(:-moz-ui-invalid){box-shadow:none}:where(dialog){background-color:#fff;border:solid;color:#000;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(summary){display:list-item}html{height:100%}body,html{min-height:calc(100vh - 40px)}body{box-sizing:border-box;margin:0;padding:0;word-break:break-word}.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-button-primary,button{align-items:center;background-color:var(--monster-bg-color-primary-1);background-position:50%;border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-primary{background-color:var(--monster-bg-color-primary-4);border-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4)}.monster-button-secondary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-bg-color-secondary-4);border-color:var(--monster-bg-color-secondary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-color-secondary-4)}.monster-button-secondary,.monster-button-tertiary{align-items:center;background-position:50%;box-shadow:var(--monster-box-shadow-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-tertiary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-bg-color-tertiary-4);border-color:var(--monster-bg-color-tertiary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-color-tertiary-4)}.monster-button-outline-primary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-primary-4);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-bg-color-primary-4)}.monster-button-outline-primary,.monster-button-outline-secondary{align-items:center;background-position:50%;box-shadow:var(--monster-box-shadow-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-outline-secondary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-secondary-4);border-color:var(--monster-bg-color-secondary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-bg-color-secondary-4)}.monster-button-outline-tertiary{align-items:center;background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-tertiary-4);background-position:50%;border-color:var(--monster-bg-color-tertiary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);color:var(--monster-bg-color-tertiary-4);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}button:active,button:hover{box-shadow:var(--monster-box-shadow-2);transition:background .8s,color .25s .0833333333s}button:active{z-index:var(--monster-z-index-outline)}.monster-button-bar,.monster-button-group{align-content:center;align-items:stretch;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between}.monster-button-group{box-sizing:border-box;gap:0;margin:1rem 0}.monster-button-group>:not(:last-child){margin-right:calc(var(--monster-border-width)*-1)}.monster-button-group :hover{box-shadow:none}button:focus{outline:1px dashed var(--monster-color-selection-4);outline-offset:2px;z-index:var(--monster-z-index-outline)}@media (prefers-color-scheme:light){button:focus{outline:1px dashed var(--monster-color-selection-3);outline-offset:2px;z-index:var(--monster-z-index-outline)}}[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}:after,:before,:root{--monster-font-family:-apple-system,BlinkMacSystemFont,\"Quicksand\",\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--monster-font-family-monospace:\"Consolas\",\"Courier New\",\"Roboto Mono\",\"Source Code Pro\",\"Fira Mono\",monospace;--monster-color-primary-1:var(--monster-color-gray-6);--monster-color-primary-2:var(--monster-color-gray-6);--monster-color-primary-3:var(--monster-color-cinnamon-1);--monster-color-primary-4:var(--monster-color-cinnamon-1);--monster-bg-color-primary-1:var(--monster-color-gray-1);--monster-bg-color-primary-2:var(--monster-color-gray-2);--monster-bg-color-primary-3:var(--monster-color-gray-6);--monster-bg-color-primary-4:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-1);--monster-color-primary-2:var(--monster-color-gray-1);--monster-color-primary-3:var(--monster-color-gray-6);--monster-color-primary-4:var(--monster-color-gray-6);--monster-bg-color-primary-1:var(--monster-color-gray-6);--monster-bg-color-primary-2:var(--monster-color-gray-3);--monster-bg-color-primary-3:var(--monster-color-gray-2);--monster-bg-color-primary-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-4);--monster-color-secondary-2:var(--monster-color-red-4);--monster-color-secondary-3:var(--monster-color-red-1);--monster-color-secondary-4:var(--monster-color-red-1);--monster-bg-color-secondary-1:var(--monster-color-gray-1);--monster-bg-color-secondary-2:var(--monster-color-red-2);--monster-bg-color-secondary-3:var(--monster-color-red-3);--monster-bg-color-secondary-4:var(--monster-color-red-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-1);--monster-color-secondary-2:var(--monster-color-red-1);--monster-color-secondary-3:var(--monster-color-red-6);--monster-color-secondary-4:var(--monster-color-red-4);--monster-bg-color-secondary-1:var(--monster-color-gray-6);--monster-bg-color-secondary-2:var(--monster-color-red-3);--monster-bg-color-secondary-3:var(--monster-color-red-2);--monster-bg-color-secondary-4:var(--monster-color-red-1)}}:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-4);--monster-color-tertiary-2:var(--monster-color-magenta-4);--monster-color-tertiary-3:var(--monster-color-magenta-6);--monster-color-tertiary-4:var(--monster-color-magenta-1);--monster-bg-color-tertiary-1:var(--monster-color-gray-1);--monster-bg-color-tertiary-2:var(--monster-color-magenta-1);--monster-bg-color-tertiary-3:var(--monster-color-magenta-2);--monster-bg-color-tertiary-4:var(--monster-color-magenta-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-1);--monster-color-tertiary-2:var(--monster-color-magenta-6);--monster-color-tertiary-3:var(--monster-color-magenta-4);--monster-color-tertiary-4:var(--monster-color-magenta-4);--monster-bg-color-tertiary-1:var(--monster-color-gray-6);--monster-bg-color-tertiary-2:var(--monster-color-magenta-2);--monster-bg-color-tertiary-3:var(--monster-color-magenta-1);--monster-bg-color-tertiary-4:var(--monster-color-magenta-1)}}:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-4);--monster-color-destructive-3:var(--monster-color-red-6);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-4);--monster-bg-color-destructive-2:var(--monster-color-gray-1);--monster-bg-color-destructive-3:var(--monster-color-red-2);--monster-bg-color-destructive-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-3);--monster-color-destructive-3:var(--monster-color-red-4);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-5);--monster-bg-color-destructive-2:var(--monster-color-gray-6);--monster-bg-color-destructive-3:var(--monster-color-red-1);--monster-bg-color-destructive-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-4);--monster-color-success-3:var(--monster-color-green-6);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-3);--monster-bg-color-success-2:var(--monster-color-gray-1);--monster-bg-color-success-3:var(--monster-color-green-2);--monster-bg-color-success-4:var(--monster-color-green-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-2);--monster-color-success-3:var(--monster-color-green-4);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-5);--monster-bg-color-success-2:var(--monster-color-gray-6);--monster-bg-color-success-3:var(--monster-color-green-1);--monster-bg-color-success-4:var(--monster-color-green-3)}}:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-4);--monster-color-warning-3:var(--monster-color-orange-6);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-3);--monster-bg-color-warning-2:var(--monster-color-gray-1);--monster-bg-color-warning-3:var(--monster-color-orange-2);--monster-bg-color-warning-4:var(--monster-color-orange-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-3);--monster-color-warning-3:var(--monster-color-orange-4);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-5);--monster-bg-color-warning-2:var(--monster-color-gray-6);--monster-bg-color-warning-3:var(--monster-color-orange-1);--monster-bg-color-warning-4:var(--monster-color-orange-3)}}:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-4);--monster-color-error-3:var(--monster-color-red-6);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-4);--monster-bg-color-error-2:var(--monster-color-gray-1);--monster-bg-color-error-3:var(--monster-color-red-2);--monster-bg-color-error-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-3);--monster-color-error-3:var(--monster-color-red-4);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-5);--monster-bg-color-error-2:var(--monster-color-gray-6);--monster-bg-color-error-3:var(--monster-color-red-1);--monster-bg-color-error-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}}:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-4);--monster-color-primary-disabled-4:var(--monster-color-gray-4);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-1);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-3);--monster-color-primary-disabled-4:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-6);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-gradient-1:#833ab4;--monster-color-gradient-2:#fd1d1d;--monster-color-gradient-3:#fcb045;--monster-box-shadow-1:none;--monster-box-shadow-2:-1px 1px 10px 1px hsla(0,0%,76%,.61);--monster-text-shadow:none;--monster-theme-control-bg-color:var(--monster-color-seashell-1);--monster-theme-control-color:var(--monster-color-seashell-6);--monster-theme-control-hover-color:var(--monster-color-seashell-6);--monster-theme-control-hover-bg-color:var(--monster-color-seashell-2);--monster-theme-control-border-width:2px;--monster-theme-control-border-style:solid;--monster-theme-control-border-radius:0;--monster-theme-control-border-color:var(--monster-color-primary-1)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-control-bg-color:var(--monster-color-gray-5);--monster-theme-control-color:var(--monster-color-gray-1);--monster-theme-control-border-color:var(--monster-color-gray-3);--monster-theme-control-hover-color:var(--monster-color-gray-1);--monster-theme-control-hover-bg-color:var(--monster-color-gray-6)}}:after,:before,:root{--monster-theme-on-color:var(--monster-color-green-1);--monster-theme-on-bg-color:var(--monster-color-green-5);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-on-color:var(--monster-color-gray-6);--monster-theme-on-bg-color:var(--monster-color-gray-1);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-5)}}:after,:before,:root{--monster-border-style:solid;--monster-border-width:3px;--monster-border-radius:0;--monster-popper-witharrrow-distance:-4px;--monster-z-index-default:0;--monster-z-index-outline:10;--monster-z-index-dropdown:200;--monster-z-index-dropdown-overlay:210;--monster-z-index-sticky:300;--monster-z-index-sticky-overlay:310;--monster-z-index-fixed:400;--monster-z-index-fixed-overlay:410;--monster-z-index-modal-backdrop:500;--monster-z-index-modal-backdrop-overlay:510;--monster-z-index-offcanvas:600;--monster-z-index-offcanvas-overlay:610;--monster-z-index-modal:700;--monster-z-index-modal-overlay:710;--monster-z-index-popover:800;--monster-z-index-popover-overlay:810;--monster-z-index-tooltip:800;--monster-z-index-tooltip-overlay:910;--monster-space-0:0;--monster-space-1:2px;--monster-space-2:4px;--monster-space-3:6px;--monster-space-4:10px;--monster-space-5:16px;--monster-space-6:26px;--monster-space-7:42px;--monster-breakpoint-0:480px;--monster-breakpoint-4:480px;--monster-breakpoint-7:768px;--monster-breakpoint-9:992px;--monster-breakpoint-12:1200px;--monster-dragger-width:2px;--monster-dragger-handle-width:4px;--monster-dragger-handle-height:50px}span.monster-fx-ripple{animation:monster-fx-ripple .6s linear;background-color:hsla(0,0%,100%,.7);border-radius:50%;position:absolute;transform:scale(0)}@keyframes monster-fx-ripple{to{opacity:0;transform:scale(4)}}[data-monster-role=pagination]{box-sizing:border-box;display:flex;font-size:1rem;font-weight:400;justify-content:center;line-height:1.6}[data-monster-role=pagination] ul{align-items:center;display:flex;flex-wrap:wrap;justify-content:center;list-style:none;margin:20px 0;padding-left:0}[data-monster-role=pagination] ul li{margin:0 1px}[data-monster-role=pagination] ul li a{align-items:center;background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-theme-control-bg-color);background-position:50%;border-color:var(--monster-theme-control-bg-color);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);color:var(--monster-theme-control-color);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch;width:-moz-max-content;width:max-content}.current:is([data-monster-role=pagination] ul li a){background-color:var(--monster-bg-color-primary-4);border-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4);cursor:unset}.disabled:is([data-monster-role=pagination] ul li a){background-color:var(--monster-bg-color-primary-disabled-1);color:var(--monster-color-primary-disabled-1);cursor:not-allowed} -}`, - 0, - ); +:where(html){line-height:1.15;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}:where(h1){font-size:2em;margin-block-end:.67em;margin-block-start:.67em}:where(dl,ol,ul) :where(dl,ol,ul){margin-block-end:0;margin-block-start:0}:where(hr){box-sizing:content-box;color:inherit;height:0}:where(abbr[title]){text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}:where(b,strong){font-weight:bolder}:where(code,kbd,pre,samp){font-family:monospace,monospace;font-size:1em}:where(small){font-size:80%}:where(table){border-color:currentColor;text-indent:0}:where(button,input,select){margin:0}:where(button){text-transform:none}:where(button,input:is([type=button i],[type=reset i],[type=submit i])){-webkit-appearance:button}:where(progress){vertical-align:baseline}:where(select){text-transform:none}:where(textarea){margin:0}:where(input[type=search i]){-webkit-appearance:textfield;outline-offset:-2px}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}:where(button,input:is([type=button i],[type=color i],[type=reset i],[type=submit i]))::-moz-focus-inner{border-style:none;padding:0}:where(button,input:is([type=button i],[type=color i],[type=reset i],[type=submit i]))::-moz-focusring{outline:1px dotted ButtonText}:where(:-moz-ui-invalid){box-shadow:none}:where(dialog){background-color:#fff;border:solid;color:#000;height:-moz-fit-content;height:fit-content;left:0;margin:auto;padding:1em;position:absolute;right:0;width:-moz-fit-content;width:fit-content}:where(dialog:not([open])){display:none}:where(summary){display:list-item}html{height:100%}body,html{min-height:calc(100vh - 40px)}body{box-sizing:border-box;margin:0;padding:0;word-break:break-word}body:focus-visible{outline:none}:focus-visible{outline:none}.block{display:block}.inline{display:inline}.inline-block{display:inline-block}.grid{display:grid}.inline-grid{display:inline-grid}.flex{display:flex}.inline-flex{display:inline-flex}.hidden,.hide,.none{display:none}.visible{visibility:visible}.invisible{visibility:hidden}.monster-button-primary,button{align-items:center;background-color:var(--monster-bg-color-primary-1);background-position:50%;border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-primary{background-color:var(--monster-bg-color-primary-4);border-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4)}.monster-button-secondary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-bg-color-secondary-4);border-color:var(--monster-bg-color-secondary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-color-secondary-4)}.monster-button-secondary,.monster-button-tertiary{align-items:center;background-position:50%;box-shadow:var(--monster-box-shadow-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-tertiary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-bg-color-tertiary-4);border-color:var(--monster-bg-color-tertiary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-color-tertiary-4)}.monster-button-outline-primary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-primary-4);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-bg-color-primary-4)}.monster-button-outline-primary,.monster-button-outline-secondary{align-items:center;background-position:50%;box-shadow:var(--monster-box-shadow-1);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}.monster-button-outline-secondary{background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-secondary-4);border-color:var(--monster-bg-color-secondary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);color:var(--monster-color-primary-1);color:var(--monster-bg-color-secondary-4)}.monster-button-outline-tertiary{align-items:center;background-color:var(--monster-bg-color-primary-1);background-color:var(--monster-color-tertiary-4);background-position:50%;border-color:var(--monster-bg-color-tertiary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);color:var(--monster-bg-color-tertiary-4);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch}button:active,button:hover{box-shadow:var(--monster-box-shadow-2);transition:background .8s,color .25s .0833333333s}button:active{z-index:var(--monster-z-index-outline)}.monster-button-bar,.monster-button-group{align-content:center;align-items:stretch;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between}.monster-button-group{box-sizing:border-box;gap:0;margin:1rem 0}.monster-button-group>:not(:last-child){margin-right:calc(var(--monster-border-width)*-1)}.monster-button-group :hover{box-shadow:none}button:focus{outline:1px dashed var(--monster-color-selection-4);outline-offset:2px;z-index:var(--monster-z-index-outline)}@media (prefers-color-scheme:light){button:focus{outline:1px dashed var(--monster-color-selection-3);outline-offset:2px;z-index:var(--monster-z-index-outline)}}[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}:after,:before,:root{--monster-font-family:-apple-system,BlinkMacSystemFont,\"Quicksand\",\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--monster-font-family-monospace:\"Consolas\",\"Courier New\",\"Roboto Mono\",\"Source Code Pro\",\"Fira Mono\",monospace;--monster-color-primary-1:var(--monster-color-gray-6);--monster-color-primary-2:var(--monster-color-gray-6);--monster-color-primary-3:var(--monster-color-cinnamon-1);--monster-color-primary-4:var(--monster-color-cinnamon-1);--monster-bg-color-primary-1:var(--monster-color-gray-1);--monster-bg-color-primary-2:var(--monster-color-gray-2);--monster-bg-color-primary-3:var(--monster-color-gray-6);--monster-bg-color-primary-4:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-1);--monster-color-primary-2:var(--monster-color-gray-1);--monster-color-primary-3:var(--monster-color-gray-6);--monster-color-primary-4:var(--monster-color-gray-6);--monster-bg-color-primary-1:var(--monster-color-gray-6);--monster-bg-color-primary-2:var(--monster-color-gray-3);--monster-bg-color-primary-3:var(--monster-color-gray-2);--monster-bg-color-primary-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-4);--monster-color-secondary-2:var(--monster-color-red-4);--monster-color-secondary-3:var(--monster-color-red-1);--monster-color-secondary-4:var(--monster-color-red-1);--monster-bg-color-secondary-1:var(--monster-color-gray-1);--monster-bg-color-secondary-2:var(--monster-color-red-2);--monster-bg-color-secondary-3:var(--monster-color-red-3);--monster-bg-color-secondary-4:var(--monster-color-red-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-1);--monster-color-secondary-2:var(--monster-color-red-1);--monster-color-secondary-3:var(--monster-color-red-6);--monster-color-secondary-4:var(--monster-color-red-4);--monster-bg-color-secondary-1:var(--monster-color-gray-6);--monster-bg-color-secondary-2:var(--monster-color-red-3);--monster-bg-color-secondary-3:var(--monster-color-red-2);--monster-bg-color-secondary-4:var(--monster-color-red-1)}}:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-4);--monster-color-tertiary-2:var(--monster-color-magenta-4);--monster-color-tertiary-3:var(--monster-color-magenta-6);--monster-color-tertiary-4:var(--monster-color-magenta-1);--monster-bg-color-tertiary-1:var(--monster-color-gray-1);--monster-bg-color-tertiary-2:var(--monster-color-magenta-1);--monster-bg-color-tertiary-3:var(--monster-color-magenta-2);--monster-bg-color-tertiary-4:var(--monster-color-magenta-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-1);--monster-color-tertiary-2:var(--monster-color-magenta-6);--monster-color-tertiary-3:var(--monster-color-magenta-4);--monster-color-tertiary-4:var(--monster-color-magenta-4);--monster-bg-color-tertiary-1:var(--monster-color-gray-6);--monster-bg-color-tertiary-2:var(--monster-color-magenta-2);--monster-bg-color-tertiary-3:var(--monster-color-magenta-1);--monster-bg-color-tertiary-4:var(--monster-color-magenta-1)}}:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-4);--monster-color-destructive-3:var(--monster-color-red-6);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-4);--monster-bg-color-destructive-2:var(--monster-color-gray-1);--monster-bg-color-destructive-3:var(--monster-color-red-2);--monster-bg-color-destructive-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-3);--monster-color-destructive-3:var(--monster-color-red-4);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-5);--monster-bg-color-destructive-2:var(--monster-color-gray-6);--monster-bg-color-destructive-3:var(--monster-color-red-1);--monster-bg-color-destructive-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-4);--monster-color-success-3:var(--monster-color-green-6);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-3);--monster-bg-color-success-2:var(--monster-color-gray-1);--monster-bg-color-success-3:var(--monster-color-green-2);--monster-bg-color-success-4:var(--monster-color-green-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-2);--monster-color-success-3:var(--monster-color-green-4);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-5);--monster-bg-color-success-2:var(--monster-color-gray-6);--monster-bg-color-success-3:var(--monster-color-green-1);--monster-bg-color-success-4:var(--monster-color-green-3)}}:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-4);--monster-color-warning-3:var(--monster-color-orange-6);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-3);--monster-bg-color-warning-2:var(--monster-color-gray-1);--monster-bg-color-warning-3:var(--monster-color-orange-2);--monster-bg-color-warning-4:var(--monster-color-orange-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-3);--monster-color-warning-3:var(--monster-color-orange-4);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-5);--monster-bg-color-warning-2:var(--monster-color-gray-6);--monster-bg-color-warning-3:var(--monster-color-orange-1);--monster-bg-color-warning-4:var(--monster-color-orange-3)}}:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-4);--monster-color-error-3:var(--monster-color-red-6);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-4);--monster-bg-color-error-2:var(--monster-color-gray-1);--monster-bg-color-error-3:var(--monster-color-red-2);--monster-bg-color-error-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-3);--monster-color-error-3:var(--monster-color-red-4);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-5);--monster-bg-color-error-2:var(--monster-color-gray-6);--monster-bg-color-error-3:var(--monster-color-red-1);--monster-bg-color-error-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}}:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-4);--monster-color-primary-disabled-4:var(--monster-color-gray-4);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-1);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-3);--monster-color-primary-disabled-4:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-6);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-gradient-1:#833ab4;--monster-color-gradient-2:#fd1d1d;--monster-color-gradient-3:#fcb045;--monster-box-shadow-1:none;--monster-box-shadow-2:-1px 1px 10px 1px hsla(0,0%,76%,.61);--monster-text-shadow:none;--monster-theme-control-bg-color:var(--monster-color-seashell-1);--monster-theme-control-color:var(--monster-color-seashell-6);--monster-theme-control-hover-color:var(--monster-color-seashell-6);--monster-theme-control-hover-bg-color:var(--monster-color-seashell-2);--monster-theme-control-border-width:2px;--monster-theme-control-border-style:solid;--monster-theme-control-border-radius:0;--monster-theme-control-border-color:var(--monster-color-primary-1)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-control-bg-color:var(--monster-color-gray-5);--monster-theme-control-color:var(--monster-color-gray-1);--monster-theme-control-border-color:var(--monster-color-gray-3);--monster-theme-control-hover-color:var(--monster-color-gray-1);--monster-theme-control-hover-bg-color:var(--monster-color-gray-6)}}:after,:before,:root{--monster-theme-on-color:var(--monster-color-green-1);--monster-theme-on-bg-color:var(--monster-color-green-5);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-on-color:var(--monster-color-gray-6);--monster-theme-on-bg-color:var(--monster-color-gray-1);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-5)}}:after,:before,:root{--monster-border-style:solid;--monster-border-width:3px;--monster-border-radius:0;--monster-outline-width:1px;--monster-popper-witharrrow-distance:-4px;--monster-z-index-default:0;--monster-z-index-outline:10;--monster-z-index-dropdown:200;--monster-z-index-dropdown-overlay:210;--monster-z-index-sticky:300;--monster-z-index-sticky-overlay:310;--monster-z-index-fixed:400;--monster-z-index-fixed-overlay:410;--monster-z-index-modal-backdrop:500;--monster-z-index-modal-backdrop-overlay:510;--monster-z-index-offcanvas:600;--monster-z-index-offcanvas-overlay:610;--monster-z-index-modal:700;--monster-z-index-modal-overlay:710;--monster-z-index-popover:800;--monster-z-index-popover-overlay:810;--monster-z-index-tooltip:800;--monster-z-index-tooltip-overlay:910;--monster-space-0:0;--monster-space-1:2px;--monster-space-2:4px;--monster-space-3:6px;--monster-space-4:10px;--monster-space-5:16px;--monster-space-6:26px;--monster-space-7:42px;--monster-breakpoint-0:480px;--monster-breakpoint-4:480px;--monster-breakpoint-7:768px;--monster-breakpoint-9:992px;--monster-breakpoint-12:1200px;--monster-dragger-width:2px;--monster-dragger-handle-width:4px;--monster-dragger-handle-height:50px}span.monster-fx-ripple{animation:monster-fx-ripple .6s linear;background-color:hsla(0,0%,100%,.7);border-radius:50%;position:absolute;transform:scale(0)}@keyframes monster-fx-ripple{to{opacity:0;transform:scale(4)}}[data-monster-role=pagination]{box-sizing:border-box;display:flex;font-size:1rem;font-weight:400;justify-content:center;line-height:1.6}[data-monster-role=pagination] ul{align-items:center;display:flex;flex-wrap:wrap;justify-content:center;list-style:none;margin:20px 0;padding-left:0}[data-monster-role=pagination] ul li{margin:0 1px}[data-monster-role=pagination] ul li a{align-items:center;background-color:var(--monster-bg-color-primary-1);background-position:50%;border-color:var(--monster-bg-color-primary-1);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-color-primary-1);color:var(--monster-theme-control-color);cursor:pointer;display:flex;font-family:var(--monster-font-family);font-size:1rem;font-weight:400;gap:.4rem;justify-content:center;line-height:1.5;outline:none;overflow:hidden;padding:.375rem .75rem;position:relative;text-align:center;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:-webkit-fill-available;width:-moz-available;width:stretch;width:-moz-max-content;width:max-content}.current:is([data-monster-role=pagination] ul li a){background-color:var(--monster-bg-color-primary-4);border-color:var(--monster-bg-color-primary-4);color:var(--monster-color-primary-4);cursor:unset}.disabled:is([data-monster-role=pagination] ul li a){background-color:var(--monster-bg-color-primary-disabled-1);color:var(--monster-color-primary-disabled-1);cursor:not-allowed} +}`, 0); } catch (e) { - addAttributeToken( - document.getRootNode().querySelector("html"), - ATTRIBUTE_ERRORMESSAGE, - e + "", - ); + addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + ""); } diff --git a/source/components/datatable/util.mjs b/source/components/datatable/util.mjs index 63478f05a..2743723de 100644 --- a/source/components/datatable/util.mjs +++ b/source/components/datatable/util.mjs @@ -29,6 +29,7 @@ const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement"); * @private */ function handleDataSourceChanges() { + if (!this[datasourceLinkedElementSymbol]) { return; } diff --git a/source/components/host/config-manager.mjs b/source/components/host/config-manager.mjs index d5194e2c2..bd6b3d349 100644 --- a/source/components/host/config-manager.mjs +++ b/source/components/host/config-manager.mjs @@ -238,12 +238,10 @@ function clearObjectStore() { return new Promise((resolve, reject) => { const req = store.clear(); req.onsuccess = function (evt) { - console.log("clearObjectStore completed"); resolve(); }; req.onerror = function (evt) { - console.error("clearObjectStore:", evt.target.errorCode); - reject(evt.target.errorCode); + reject(evt.target?.errorCode); }; }); } diff --git a/source/data/datasource/server.mjs b/source/data/datasource/server.mjs index a0d8f4642..f835139ec 100644 --- a/source/data/datasource/server.mjs +++ b/source/data/datasource/server.mjs @@ -125,6 +125,7 @@ function doDiff(obj) { */ function doTransform(type, obj) { const transformation = this.getOption(`${type}.mapping.transformer`); + if (transformation !== undefined && transformation !== null) { const pipe = new Pipe(transformation); const callbacks = this.getOption(`${type}.mapping.callbacks`); diff --git a/source/data/datasource/server/restapi.mjs b/source/data/datasource/server/restapi.mjs index 691c6e760..2847d1d49 100644 --- a/source/data/datasource/server/restapi.mjs +++ b/source/data/datasource/server/restapi.mjs @@ -18,6 +18,7 @@ import { diff } from "../../diff.mjs"; import { Server } from "../server.mjs"; import { WriteError } from "./restapi/writeerror.mjs"; import { DataFetchError } from "./restapi/data-fetch-error.mjs"; +import {clone} from "../../../util/clone.mjs"; export { RestAPI }; @@ -63,7 +64,7 @@ class RestAPI extends Server { /** * @property {Object} write={} Options - * @property {Object} write.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor} + * @property {Object} write.init={} An option object, containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor} * @property {string} write.init.method=POST * @property {Object} write.init.headers Object containing any custom headers that you want to apply to the request. * @property {string} write.responseCallback Callback function to be executed after the request has been completed. @@ -71,7 +72,7 @@ class RestAPI extends Server { * @property {string} write.url URL * @property {Object} write.mapping the mapping is applied before writing. * @property {String} write.mapping.transformer Transformer to select the appropriate entries - * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing. + * @property {exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing. * @property {Object} write.report * @property {String} write.report.path Path to validations * @property {Object} write.partial @@ -80,13 +81,13 @@ class RestAPI extends Server { * @property {Object} write.sheathing.object Object to be wrapped * @property {string} write.sheathing.path Path to the data * @property {Object} read={} Options - * @property {Object} read.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor} + * @property {Object} read.init={} An option object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor} * @property {string} read.init.method=GET - * @property {string} read.acceptedStatus=[200] + * @property {array} read.acceptedStatus=[200] * @property {string} read.url URL * @property {Object} read.mapping the mapping is applied after reading. * @property {String} read.mapping.transformer Transformer to select the appropriate entries - * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading. + * @property {exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading. */ get defaults() { return Object.assign({}, super.defaults, { @@ -94,19 +95,19 @@ class RestAPI extends Server { init: { method: "POST", }, - responseCallback: undefined, + responseCallback: null, acceptedStatus: [200, 201], url: null, mapping: { - transformer: undefined, + transformer: null, callbacks: [], }, sheathing: { - object: undefined, - path: undefined, + object: null, + path: null, }, report: { - path: undefined, + path: null, }, partial: { @@ -118,11 +119,11 @@ class RestAPI extends Server { method: "GET", }, path: null, - responseCallback: undefined, + responseCallback: null, acceptedStatus: [200], url: null, mapping: { - transformer: undefined, + transformer: null, callbacks: [], }, }, @@ -175,10 +176,16 @@ class RestAPI extends Server { * @return {RestAPI} */ getClone() { - return new RestAPI( - this[internalSymbol].getRealSubject()["options"].read, - this[internalSymbol].getRealSubject()["options"].write, - ); + const api = new RestAPI(); + + const read = clone(this[internalSymbol].getRealSubject()["options"].read); + const write = clone(this[internalSymbol].getRealSubject()["options"].write); + + api.setOption("read", read); + api.setOption("write", write); + + return api; + } } diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs index b8fa1cebc..3249e548c 100644 --- a/source/dom/customelement.mjs +++ b/source/dom/customelement.mjs @@ -219,6 +219,40 @@ class CustomElement extends HTMLElement { return Symbol.for("@schukai/monster/dom/custom-element@@instance"); } +// /** +// * This method is called by the `instanceof` operator. +// * +// * @param instance +// * @returns {arg is any[]} +// */ +// static [Symbol.hasInstance](instance) { +// if (!isObject(instance)) { +// return false; +// } +// debugger +// const instanceSym = instance?.[instanceSymbol]; +// if (instanceSym === undefined) { +// return false; +// } +// +// // Direktes Matching des Symbols +// if (instanceSym === this[instanceSymbol]) { +// return true; +// } +// +// // Prüfe den Prototypenbaum des Objekts für eine Übereinstimmung +// let proto = Object.getPrototypeOf(instance); +// while (proto) { +// const protoSymbol = proto.constructor?.[instanceSymbol]; +// if (protoSymbol === this[instanceSymbol]) { +// return true; +// } +// proto = Object.getPrototypeOf(proto); // Gehe weiter den Prototypenbaum entlang +// } +// +// return false; +// } + /** * This method determines which attributes are to be * monitored by `attributeChangedCallback()`. Unfortunately, this method is static. diff --git a/source/types/has.mjs b/source/types/has.mjs new file mode 100644 index 000000000..bb145ed06 --- /dev/null +++ b/source/types/has.mjs @@ -0,0 +1,29 @@ +/** + * 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 + */ + +export { + hasImplementation +}; + + +/** + * With this function, you can check if a value is iterable. + * + * @param object + * @param methods + * @returns {boolean} + */ +function hasImplementation(object, methods) { + return methods.every(method => typeof object[method] === 'function'); +} \ No newline at end of file -- GitLab