diff --git a/development/config/import.mjs b/development/config/import.mjs index 04d57734e3d6c045ee69580ce68282348a272de1..56891c6a727408ed3873e2cb9d426e715c47e51c 100644 --- a/development/config/import.mjs +++ b/development/config/import.mjs @@ -1,7 +1,7 @@ export const projectRoot = "/home/vs/workspaces/oss/monster/monster"; export const sourcePath = "/home/vs/workspaces/oss/monster/monster/source"; export const developmentPath = "/home/vs/workspaces/oss/monster/monster/development"; -export const pnpxBin = "/nix/store/z8s3r4vwf4r26g2d7shnw5lva6ihim8f-pnpm-9.15.0/bin/pnpx"; +export const pnpxBin = "/nix/store/6fbs7524azbhk59lc7mfmvjmzsi0l4dx-pnpm-9.15.1/bin/pnpx"; export const nodeBin = "/nix/store/hnkyz55vndmvwhg6nzpliv86gh6sxg7h-nodejs-22.10.0/bin/node"; export const license = "/**" + "\n" + " * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved." + "\n" + diff --git a/development/issues/closed/273.html b/development/issues/closed/273.html new file mode 100644 index 0000000000000000000000000000000000000000..6d6764957f7b06f2b585cef89dc4b0a220950e8b --- /dev/null +++ b/development/issues/closed/273.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>open the first tab if no active defined #273</title> + <script src="273.mjs" type="module"></script> +</head> +<body> + <h1>open the first tab if no active defined #273</h1> + <p></p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/273">Issue #273</a></li> + <li><a href="/">Back to overview</a></li> + </ul> + <main> + + <monster-tabs> + <monster-tab>Tab 1</monster-tab> + <monster-tab>Tab 2</monster-tab> + <monster-tab>Tab 3</monster-tab> + </monster-tabs> + + </main> +</body> +</html> diff --git a/development/issues/closed/273.mjs b/development/issues/closed/273.mjs new file mode 100644 index 0000000000000000000000000000000000000000..5c0a45b3d8144fc824e763c7935b82aea6a9055e --- /dev/null +++ b/development/issues/closed/273.mjs @@ -0,0 +1,15 @@ +/** +* @file development/issues/open/273.mjs +* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/273 +* @description open the first tab if no active defined +* @issue 273 +*/ + +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/layout/tabs.mjs"; + diff --git a/source/components/datatable/dataset.mjs b/source/components/datatable/dataset.mjs index 84d418dcf70d16941098ce5fdf023c99b2fc21a6..105a02f612b513247d9417e187b2cc4dd74f3b91 100644 --- a/source/components/datatable/dataset.mjs +++ b/source/components/datatable/dataset.mjs @@ -12,30 +12,30 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { instanceSymbol } from "../../constants.mjs"; -import { Pathfinder } from "../../data/pathfinder.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 {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}; /** * A data set component @@ -51,268 +51,277 @@ export { DataSet }; * @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); - 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]; - } + /** + * 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] = () => { - // @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); - } + 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(() => { + + let index = 0; + if (typeof this[datasourceLinkedElementSymbol]?.currentPage === "function") { + const page = this[datasourceLinkedElementSymbol].currentPage(); + if (page !== null && page !== undefined && page !== "") { + index = parseInt(page, 10) - 1; + } + } + + this.setOption("mapping.index", index); + handleDataSourceChanges.call(this); + }), + ); + + this[datasourceLinkedElementSymbol].attachObserver( + new Observer(() => { + let index = 0; + if (typeof this[datasourceLinkedElementSymbol]?.currentPage === "function") { + const page = this[datasourceLinkedElementSymbol].currentPage(); + if (page !== null && page !== undefined && page !== "") { + index = parseInt(page, 10) - 1; + } + } + + this.setOption("mapping.index", index); + handleDataSourceChanges.call(this); + }), + ); + + handleDataSourceChanges.call(this); + } } /** @@ -320,56 +329,56 @@ function initEventHandler() { * @param {Object} options */ function updateOptionsFromArguments(options) { - const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX); // @deprecated use data-monster-option-mapping-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); } /** @@ -377,8 +386,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/util.mjs b/source/components/datatable/util.mjs index 63478f05ab71e604e88a4e091660a1ce30597992..e4c7c0d41f10dc32d42cbfc4462e40483dafbb56 100644 --- a/source/components/datatable/util.mjs +++ b/source/components/datatable/util.mjs @@ -29,7 +29,8 @@ const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement"); * @private */ function handleDataSourceChanges() { - if (!this[datasourceLinkedElementSymbol]) { + + if (!this[datasourceLinkedElementSymbol]) { return; } diff --git a/source/components/layout/tabs.mjs b/source/components/layout/tabs.mjs index bb174e9fa524a1f08d233571071478ef98223680..99bdf30a65daa819d009962439e97479bd797fe6 100644 --- a/source/components/layout/tabs.mjs +++ b/source/components/layout/tabs.mjs @@ -151,6 +151,7 @@ const resizeObserverSymbol = Symbol("resizeObserver"); * * @issue https://localhost.alvine.dev:8443/development/issues/closed/268.html * @issue https://localhost.alvine.dev:8443/development/issues/closed/271.html + * @issue https://localhost.alvine.dev:8443/development/issues/closed/273.html * * @since 3.74.0 * @copyright schukai GmbH @@ -178,6 +179,7 @@ class Tabs extends CustomElement { * @property {Object} features * @property {number} features.openDelay=500 Open delay in milliseconds * @property {string} features.removeBehavior="auto" Remove behavior, auto (default), next, previous and none + * @property {boolean} features.openFirst=true Open the first tab * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) * @property {String} fetch.redirect=error * @property {String} fetch.method=GET @@ -213,6 +215,7 @@ class Tabs extends CustomElement { features: { openDelay: null, removeBehavior: "auto", + openFirst: true, }, classes: { @@ -918,6 +921,16 @@ function initTabButtons() { this.setOption("marker", random()); return adjustButtonVisibility.call(this).then(() => { + + if (!activeReference&& this.getOption("features.openFirst") === true) { + const firstButton = this.getOption("buttons.standard").find( + (button) => button.disabled !== true, + ); + if (firstButton) { + activeReference = firstButton.reference; + } + } + if (activeReference) { return new Processing(() => { const button = this.shadowRoot.querySelector(