diff --git a/Taskfile.yml b/Taskfile.yml index 400b793ac888e68638fe5cd54cc79d66b40865f5..650cafaa4673f8b424b2a32a8df076d120f66f74 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -81,6 +81,14 @@ tasks: cmds: - format-and-lint-code + create-new-component: + silent: true + desc: Create a new component + aliases: + - cnc + cmds: + - create-new-component + build-and-publish: silent: true desc: Build the app and publish it to the npm registry diff --git a/development/issues/open/184.html b/development/issues/closed/184.html similarity index 100% rename from development/issues/open/184.html rename to development/issues/closed/184.html diff --git a/development/issues/open/184.mjs b/development/issues/closed/184.mjs similarity index 100% rename from development/issues/open/184.mjs rename to development/issues/closed/184.mjs diff --git a/development/issues/open/186.html b/development/issues/open/186.html new file mode 100644 index 0000000000000000000000000000000000000000..3eca59ae2153bad4a30746aae1f7dbc3eeaa827c --- /dev/null +++ b/development/issues/open/186.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>add new field set for forms #186</title> + <script src="./186.mjs" type="module"></script> +</head> +<body> + <h1>add new field set for forms #186</h1> + <p></p> + <ul> + <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/186">Issue #186</a></li> + <li><a href="/">Back to overview</a></li> + </ul> + <main> + <!-- Write your code here --> + </main> + +</body> +</html> diff --git a/development/issues/open/186.mjs b/development/issues/open/186.mjs new file mode 100644 index 0000000000000000000000000000000000000000..763aa20e8306645f8ea954c208d1e710263f147c --- /dev/null +++ b/development/issues/open/186.mjs @@ -0,0 +1,11 @@ +/** +* @file development/issues/open/186.mjs +* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/186 +* @description add new field set for forms +* @issue 186 +*/ + +import "../../../source/components/style/property.pcss"; +import "../../../source/components/style/normalize.pcss"; +import "../../../source/components/style/typography.pcss"; + diff --git a/development/issues/open/video.html b/development/issues/open/video.html deleted file mode 100644 index 3903ddb82771f6797eb84a18632e3fe76393bd94..0000000000000000000000000000000000000000 --- a/development/issues/open/video.html +++ /dev/null @@ -1,36 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <title>Split Screen</title> - <script type="module"> - import "../../../source/components/style/property.pcss"; - import "../../../source/components/style/normalize.pcss"; - import "../../../source/components/style/typography.pcss"; - import "../../../source/components/style/color.pcss"; - import "../../../source/components/layout/panel.mjs"; - import "../../../source/components/layout/split-panel.mjs"; - </script> - - <style> - *:not(:defined) { - visibility: hidden; - } - </style> -</head> -<body> -<h1>Split Screen</h1> -<monster-panel data-monster-option-heightadjustment="-1"> - <monster-split-panel data-monster-option-splittype="vertical"> - <monster-panel data-monster-option-heightadjustment="-3" slot="start"> - <h1>Start Panel</h1> - <p>Some content x1</p> - </monster-panel> - <monster-panel data-monster-option-heightadjustment="-3" slot="end"> - <h1>End Panel</h1> - <p>Some content x1</p> - </monster-panel> - </monster-split-panel> -</monster-panel> -</body> -</html> diff --git a/development/scripts/createNewComponentClass.mjs b/development/scripts/createNewComponentClass.mjs new file mode 100644 index 0000000000000000000000000000000000000000..22c24ea4cbc0cdc5b42b1d566c507467e47b2f39 --- /dev/null +++ b/development/scripts/createNewComponentClass.mjs @@ -0,0 +1,441 @@ +import {sourcePath, projectRoot, license} from "./import.mjs"; +import {writeFileSync, readFileSync, mkdirSync, existsSync} from "fs"; +import {buildCSS, createScriptFilenameFromStyleFilename} from "./buildStylePostCSS.mjs"; + + +let version = "1.0.0"; +try { + const data = readFileSync(projectRoot + "/package.json", "utf8"); + version = JSON.parse(data).version; +} catch (e) { + console.error(e); + process.exit(1); + +} + +// small hack to get the version from the package.json, should be replaced with a proper version from git +const versionParts = version.split('.'); +versionParts[1] = parseInt(versionParts[1]) + 1; +versionParts[2] = 0; +const nextVersion = versionParts.join('.'); + + +const template = `${license}import { instanceSymbol } from "{{BACK_TO_ROOT_PATH}}/constants.mjs"; +import { addAttributeToken } from "{{BACK_TO_ROOT_PATH}}/dom/attributes.mjs"; +import { + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, +} from "{{BACK_TO_ROOT_PATH}}/dom/constants.mjs"; +import { CustomControl } from "{{BACK_TO_ROOT_PATH}}/dom/customcontrol.mjs"; +import { + assembleMethodSymbol, + registerCustomElement, +} from "{{BACK_TO_ROOT_PATH}}/dom/customelement.mjs"; +import { findTargetElementFromEvent } from "{{BACK_TO_ROOT_PATH}}/dom/events.mjs"; +import { isFunction } from "{{BACK_TO_ROOT_PATH}}/types/is.mjs"; +import { {{CLASSNAME}}StyleSheet } from "./stylesheet/{{HTML_TAG_SUFFIX}}.mjs"; +import { fireCustomEvent } from "{{BACK_TO_ROOT_PATH}}/dom/events.mjs"; + +export { {{CLASSNAME}} }; + +/** + * @private + * @type {symbol} + */ +export const {{CLASSNAME_LOWER_FIRST}}ElementSymbol = Symbol("{{CLASSNAME_LOWER_FIRST}}Element"); + +/** + * This CustomControl creates a {{CLASSNAME}} element with a variety of options. + * + * <img src="./images/{{HTML_TAG_SUFFIX}}.png"> + * + * You can create this control either by specifying the HTML tag <monster-{{HTML_TAG_SUFFIX}} />\` directly in the HTML or using + * Javascript via the \`document.createElement('monster-{{HTML_TAG_SUFFIX}}');\` method. + * + * \`\`\`html + * <monster-{{HTML_TAG_SUFFIX}}></monster-{{HTML_TAG_SUFFIX}}> + * \`\`\` + * + * Or you can create this CustomControl directly in Javascript: + * + * \`\`\`js + * import {{{CLASSNAME}}} from '@schukai/monster/source/{{NAMESPACE_AS_PATH}}/{{HTML_TAG_SUFFIX}}.mjs'; + * document.createElement('monster-{{HTML_TAG_SUFFIX}}'); + * \`\`\` + * + * @externalExample {{BACK_TO_ROOT_PATH}}../example/{{NAMESPACE_AS_PATH}}/{{HTML_TAG_SUFFIX}}.mjs + * @startuml {{HTML_TAG_SUFFIX}}.png + * skinparam monochrome true + * skinparam shadowing false + * {{UML}} + * @enduml + * + * @since {{VERSION}} + * @copyright schukai GmbH + * @memberOf Monster.{{NAMESPACE_WITH_DOTS}} + * @summary A simple {{CLASSNAME}} + */ +class {{CLASSNAME}} extends {{PARENT_CLASS}} { + /** + * This method is called by the \`instanceof\` operator. + * @returns {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/{{NAMESPACE_AS_PATH}}/{{CLASSNAME_LOWER}}@@instance"); + } + + /** + * + * @return {{{NAMESPACE_WITH_DOTS}}.{{CLASSNAME}} + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + return this; + } + + /** + * To set the options via the html tag the attribute \`data-monster-options\` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {Object} templates Template definitions + * @property {string} templates.main Main template + * @property {Object} labels Label definitions + * @property {Object} actions Callbacks + * @property {string} actions.click="throw Error" Callback when clicked + * @property {Object} features Features + * @property {Object} classes CSS classes + * @property {boolean} disabled=false Disabled state + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + labels: { + }, + classes: { + }, + disabled: false, + features: { + }, + actions: { + click: () => { + throw new Error("the click action is not defined"); + }, + }{{VALUE_DEFAULTS}} + }); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-{{HTML_TAG_SUFFIX}}"; + } + + /** + * + * @return {Array<CSSStyleSheet>} + */ + static getCSSStyleSheet() { + return [{{CLASSNAME}}StyleSheet]; + } + + {{HTML_ELEMENT_FORM_INTERNALS}} +} + +/** + * @private + * @return {initEventHandler} + * @fires Monster.Components.Form.event:monster-{{HTML_TAG_SUFFIX}}-clicked + */ +function initEventHandler() { + const self = this; + const element = this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol]; + + const type = "click"; + + element.addEventListener(type, function (event) { + const callback = self.getOption("actions.click"); + + fireCustomEvent(self, "monster-{{HTML_TAG_SUFFIX}}-clicked", { + element: self, + }); + + if (!isFunction(callback)) { + return; + } + + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "control", + ); + + if (!(element instanceof Node && self.hasNode(element))) { + return; + } + + callback.call(self, event); + }); + + return this; +} + +/** + * @private + */ +function initControlReferences() { + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol] = this.shadowRoot.querySelector( + \`[\${ATTRIBUTE_ROLE}="control"]\`, + ); +} + +/** + * @private + * @return {string} + */ +function getTemplate() { + // language=HTML + return \` + <div data-monster-role="control" part="control"> + </div>\`; +} + + +registerCustomElement({{CLASSNAME}}); +`; + +const internalTemplate = ` + /** + * The {{CLASSNAME}}.click() method simulates a click on the internal element. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click} + */ + click() { + if (this.getOption("disabled") === true) { + return; + } + + if ( + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol] && + isFunction(this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].click) + ) { + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].click(); + } + } + + /** + * The Button.focus() method sets focus on the internal element. + * + * @param {Object} options + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} + */ + focus(options) { + if (this.getOption("disabled") === true) { + return; + } + + if ( + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol] && + isFunction(this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].focus) + ) { + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].focus(options); + } + } + + /** + * The Button.blur() method removes focus from the internal element. + */ + blur() { + if ( + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol] && + isFunction(this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].blur) + ) { + this[{{CLASSNAME_LOWER_FIRST}}ElementSymbol].blur(); + } + } + + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @return {boolean} + */ + static get formAssociated() { + return true; + } + + /** + * The current value of the form control. + * + * \`\`\`js + * e = document.querySelector('monster-{{HTML_TAG_SUFFIX}}'); + * console.log(e.value) + * \`\`\` + * + * @property {string} + */ + get value() { + return this.getOption("value"); + } + + /** + * Set value of the form control. + * + * \`\`\` + * e = document.querySelector('monster-{{HTML_TAG_SUFFIX}}'); + * e.value=1 + * \`\`\` + * + * @property {string} value + * @throws {Error} unsupported type + */ + set value(value) { + this.setOption("value", value); + try { + this?.setFormValue(this.value); + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + } + } + +`; + + +const args = process.argv.slice(2); + +const argsObj = args.reduce((acc, arg) => { + let [key, value] = arg.split('='); + key = key.replace(/^--/, ''); + acc[key] = value; + return acc; +}, {}); + +function printUsage() { + console.log("Usage: node createNewComponentClass.mjs --classname=YourClassName --namespace=components.yourNamespace"); +} + + +if (!argsObj.classname) { + console.error("No classname provided, you can use --classname=YourClassName"); + printUsage(); + process.exit(1); +} + +if (argsObj.classname.match(/[^A-Za-z0-9]/)) { + console.error("Classname can only contain letters and numbers"); + process.exit(1); +} + +if (argsObj.classname.match(/^[0-9]/)) { + console.error("Classname can not start with a number"); + process.exit(1); +} + +if (argsObj.classname.match(/^[a-z]/)) { + console.error("Classname should start with an uppercase letter"); + process.exit(1); +} + + +if (!argsObj.namespace) { + console.error("No namespace provided, you can use --namespace=Your.Namespace"); + printUsage(); + process.exit(1); +} + +if (argsObj.namespace.match(/[^A-Za-z0-9.]/)) { + console.error("Namespace can only contain letters, numbers and dots"); + process.exit(1); +} + +if (!argsObj.namespace.match(/^components/)) { + console.error("Namespace must start with components"); + process.exit(1); +} + + +const lowerFirst = argsObj.classname[0].toLowerCase() + argsObj.classname.slice(1); +const htmlTagSuffix = argsObj.classname.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + +const namespace = argsObj.namespace.split('.').map((part) => part[0].toUpperCase() + part.slice(1)) +const namespaceAsPath = namespace.join('/').toLowerCase(); +const namespaceWithDots = namespace.join('.'); + +const directory = `${sourcePath}/${namespaceAsPath}`; +const styleDirectory = `${directory}/style`; +const filename = `${directory}/${htmlTagSuffix}.mjs`; +const pcssFilename = `${styleDirectory}/${htmlTagSuffix}.pcss`; + +const exampleDirectory = `${projectRoot}/example/${namespaceAsPath}`; +const exampleFilename = `${exampleDirectory}/${htmlTagSuffix}.mjs`; + +const backToRootPath = '../'.repeat(namespace.length).slice(0, -1); + +const isFormControl = argsObj.namespace.match(/components\.form/); +const formInternalsCode = isFormControl ? internalTemplate : ''; +const parentClass = isFormControl ? 'CustomControl' : 'CustomElement'; +const valueDefaults = isFormControl ? ",\n value: null" : ""; + +const uml = isFormControl ? `HTMLElement <|-- CustomElement + * CustomElement <|-- CustomControl + * CustomControl <|-- {{CLASSNAME}}` : `HTMLElement <|-- CustomElement + * CustomElement <|-- {{CLASSNAME}}`; + +const code = template.replace(/{{HTML_ELEMENT_FORM_INTERNALS}}/g, formInternalsCode) + .replace(/{{UML}}/g, uml) + .replace(/{{CLASSNAME}}/g, argsObj.classname) + .replace(/{{CLASSNAME_LOWER_FIRST}}/g, lowerFirst) + .replace(/{{CLASSNAME_LOWER}}/g, argsObj.classname.toLowerCase()) + .replace(/{{HTML_TAG_SUFFIX}}/g, htmlTagSuffix) + .replace(/{{NAMESPACE_AS_PATH}}/g, namespaceAsPath) + .replace(/{{BACK_TO_ROOT_PATH}}/g, backToRootPath) + .replace(/{{PARENT_CLASS}}/g, parentClass) + .replace(/{{VALUE_DEFAULTS}}/g, valueDefaults) + + .replace(/{{VERSION}}/g, nextVersion) + .replace(/{{NAMESPACE_WITH_DOTS}}/g, namespaceWithDots); + +try { + writeFileSync(filename, code); +} catch (e) { + console.error(e); + process.exit(1); +} + +const exampleCode = `import "@schukai/monster/source/${namespaceAsPath}/${htmlTagSuffix}.mjs"; +const element = document.createElement('monster-${htmlTagSuffix}'); +element.setOption('disable', 'false') +document.body.appendChild(element); +`; + +try { + + if (!existsSync(exampleDirectory)) { + mkdirSync(exampleDirectory, {recursive: true}); + } + + writeFileSync(exampleFilename, exampleCode); +} catch (e) { + console.error(e); + process.exit(1); +} + +try { + writeFileSync(pcssFilename, ''); +} catch (e) { + console.error(e); + process.exit(1); +} + +buildCSS(pcssFilename, createScriptFilenameFromStyleFilename(pcssFilename)).then(() => { + console.log(`Created ${filename} and ${pcssFilename}`); + process.exit(0); +}).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/devenv.nix b/devenv.nix index 647acd709b69e4fad0bc47e1818af81367fbd600..fd4d7a872b055ef92a4dd9eefaf0d0a8a8cee482 100644 --- a/devenv.nix +++ b/devenv.nix @@ -41,6 +41,8 @@ // THIS FILE IS AUTOGENERATED. DO NOT EDIT THIS FILE DIRECTLY. ''; }; + + commonFunctionsScript = pkgs.writeScript "common-functions" '' @@ -611,13 +613,27 @@ in { echo_section "build monster file" - if ! $(${pkgs.nodejs_20}/bin/node ${config.devenv.root}/development/scripts/buildMonsterFile.mjs) + if ! ${pkgs.nodejs_20}/bin/node ${config.devenv.root}/development/scripts/buildMonsterFile.mjs then - echo "ERROR: script buildMonsterFile.mjs failed, check your JS!" + echo_fail "script buildMonsterFile.mjs failed, check your JS!" exit 1 fi echo_ok "Monster file created" ''; + + scripts.create-new-component-class.exec = '' + #!${pkgs.bash}/bin/bash + source ${commonFunctionsScript} + + echo_section "create new component class" + + if ! ${pkgs.nodejs_20}/bin/node ${config.devenv.root}/development/scripts/createNewComponentClass.mjs "$@" + then + echo_fail "script createNewClass.mjs failed, check your JS!" + exit 1 + fi + echo_ok "New component class created" + ''; scripts.run-web-tests.exec = '' PROJECT_ROOT="${config.devenv.root}" diff --git a/example/components/form/field-set.mjs b/example/components/form/field-set.mjs new file mode 100644 index 0000000000000000000000000000000000000000..e87ab75d01c37df302df6520ffe3e13ba7c1cb7a --- /dev/null +++ b/example/components/form/field-set.mjs @@ -0,0 +1,4 @@ +import "@schukai/monster/source/components/form/field-set.mjs"; +const element = document.createElement('monster-field-set'); +element.setOption('disable', 'false') +document.body.appendChild(element); diff --git a/example/components/form/select.mjs b/example/components/form/select.mjs index 808e47ee2ef645b042e95ed92818f719becffc2f..04bae4dc95c976bb9c10fb07cb40fedc1fe556db 100644 --- a/example/components/form/select.mjs +++ b/example/components/form/select.mjs @@ -1,4 +1,4 @@ -import {Select} from '@schukai/component-form/source/select.js'; +import '@schukai/component-form/source/select.js'; const select = document.createElement('monster-select'); select.setOption('mapping.labelTemplate', '${name} (${alpha-2})') diff --git a/opt/scripts/build-stylesheets.cjs b/opt/scripts/build-stylesheets.cjs deleted file mode 100644 index ba7037bb3cd8584969c70e89421e0009853e4f9f..0000000000000000000000000000000000000000 --- a/opt/scripts/build-stylesheets.cjs +++ /dev/null @@ -1,158 +0,0 @@ -// const fs = require('fs'); -// const path = require('path'); -// const args = process.argv.slice(2); -// -// if (args.length < 1) { -// console.log("Usage: node build-stylesheets.js <project-root>"); -// process.exit(1); -// } -// -// const projectRoot = args[0]; -// -// if (!fs.existsSync(projectRoot)) { -// console.log("Project root " + projectRoot + " does not exist"); -// process.exit(1); -// } -// -// let relPath = null; -// for (let i = 0; i < args.length; i++) { -// if (args[i] === '--rel-path') { -// relPath = args[i + 1]; -// } -// } -// -// if (relPath === null || relPath === undefined || relPath === "") { -// console.error("Relativ path must be set"); -// console.log("Usage: node build-stylesheets.js --relPath <relativ-path>"); -// process.exit(1); -// } -// -// if (relPath.substr(-1) !== '/') { -// relPath += '/'; -// } -// -// if (relPath.substr(0, 1) === '/') { -// console.error("Relativ path must not start with /"); -// console.log("Usage: node build-stylesheets.js --relPath <relativ-path>"); -// process.exit(1); -// } -// -// // build rel, return path ../ to root -// let backToRootPath = ""; -// const parts = relPath.split("/"); -// for (let i = 0; i < parts.length; i++) { -// backToRootPath += "../"; -// } -// -// const absoluteProjectRoot = path.resolve(projectRoot) + "/"; -// const absoluteSourcePath = absoluteProjectRoot + 'source/'; -// const styleSourceRoot = absoluteSourcePath + relPath + 'style/' -// const styleSheetRoot = absoluteSourcePath + relPath + 'stylesheet/' -// const nodeModuleStyleSourceRoot = absoluteProjectRoot + 'node_modules/' -// -// if (!fs.existsSync(styleSourceRoot)) { -// console.log("Style source root " + styleSourceRoot + " does not exist"); -// process.exit(1); -// } -// -// if (!fs.existsSync(nodeModuleStyleSourceRoot)) { -// console.log("Node module style source root " + nodeModuleStyleSourceRoot + " does not exist"); -// process.exit(1); -// } -// -// if (!fs.existsSync(styleSheetRoot)) { -// fs.mkdirSync(styleSheetRoot); -// } -// -// let promises = []; -// let styles = new Map(); -// -// -// export function buildScriptCSS(styleSheetRoot, styles) { -// styles.forEach((css, fn) => { -// -// let className = path.parse(fn).name; -// className = className.charAt(0).toUpperCase() + className.slice(1); -// className = className.replace(/-([a-z])/g, function (g) { -// return g[1].toUpperCase(); -// }); -// let layerName = className.toLowerCase(); -// -// css = css.replace(/"/g, '\\"'); -// const code = codeTemplate -// .replaceAll("{{backToRootPath}}", path.relative(styleSheetRoot, projectRoot) + "/") -// .replaceAll("{{copyRightYear}}", new Date().getFullYear()) -// .replaceAll("{{ClassName}}", className) -// .replaceAll("{{LayerName}}", layerName) -// .replaceAll("{{css}}", css); -// -// const targetFile = path.normalize(path.join(styleSheetRoot, fn)); -// fs.mkdirSync(path.dirname(targetFile), {recursive: true}); -// fs.writeFileSync(targetFile, code); -// }) -// } -// -// -// function scanFiles(root, keyPath) { -// -// if (keyPath === undefined) { -// keyPath = []; -// } -// -// return new Promise((resolve, reject) => { -// -// let localPromises = []; -// let f; -// -// const dir = fs.opendirSync(root); -// while ((f = dir.readSync()) !== null) { -// -// const key = f.name; -// const fn = path.join(root, f.name); -// const currentPath = [...keyPath] -// currentPath.push(key); -// -// if (f.isDirectory()) { -// -// if (["node_modules", "mixin"].includes(f.name)) { -// continue; -// } -// -// localPromises.push(scanFiles(fn, currentPath)); -// continue; -// } else if (!f.isFile()) { -// continue; -// } -// -// if ((path.extname(f.name) !== ".css" && path.extname(f.name) !== ".pcss")) { -// continue; -// } -// -// //console.log("Found file: " + fn); -// let content = fs.readFileSync(fn, 'utf8'); -// localPromises.push(new Promise((resolve, reject) => { -// (async () => { -// const {rewriteCSS} = await import('./rewriteCSS.mjs'); -// resolve(rewriteCSS(styles, content, fn, currentPath, absoluteSourcePath, nodeModuleStyleSourceRoot, styleSourceRoot, absoluteProjectRoot)); -// -// })(); -// })); -// -// } -// -// dir.closeSync(); -// -// Promise.all(localPromises).then(resolve).catch(reject); -// -// }) -// } -// -// -// scanFiles(path.normalize(styleSourceRoot)).then(() => { -// -// buildScriptCSS(styleSheetRoot, styles); -// -// }).catch(e => { -// console.log("Error creating stylesheets"); -// console.error(e); -// }) diff --git a/source/components/form/field-set.mjs b/source/components/form/field-set.mjs new file mode 100644 index 0000000000000000000000000000000000000000..fdb7cb325286b34470211be067d0c9abb3ed0df8 --- /dev/null +++ b/source/components/form/field-set.mjs @@ -0,0 +1,296 @@ +/** + * 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. + */ + +import { instanceSymbol } from "../../constants.mjs"; +import { addAttributeToken } from "../../dom/attributes.mjs"; +import { + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_ROLE, +} from "../../dom/constants.mjs"; +import { CustomControl } from "../../dom/customcontrol.mjs"; +import { + assembleMethodSymbol, + registerCustomElement, +} from "../../dom/customelement.mjs"; +import { findTargetElementFromEvent } from "../../dom/events.mjs"; +import { isFunction } from "../../types/is.mjs"; +import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs"; +import { fireCustomEvent } from "../../dom/events.mjs"; + +export { FieldSet }; + +/** + * @private + * @type {symbol} + */ +export const fieldSetElementSymbol = Symbol("fieldSetElement"); + +/** + * This CustomControl creates a FieldSet element with a variety of options. + * + * <img src="./images/field-set.png"> + * + * You can create this control either by specifying the HTML tag <monster-field-set />` directly in the HTML or using + * Javascript via the `document.createElement('monster-field-set');` method. + * + * ```html + * <monster-field-set></monster-field-set> + * ``` + * + * Or you can create this CustomControl directly in Javascript: + * + * ```js + * import {FieldSet} from '@schukai/monster/source/components/form/field-set.mjs'; + * document.createElement('monster-field-set'); + * ``` + * + * @externalExample ../..../example/components/form/field-set.mjs + * @startuml field-set.png + * skinparam monochrome true + * skinparam shadowing false + * HTMLElement <|-- CustomElement + * CustomElement <|-- CustomControl + * CustomControl <|-- FieldSet + * @enduml + * + * @since 3.65.0 + * @copyright schukai GmbH + * @memberOf Monster.Components.Form + * @summary A simple FieldSet + */ +class FieldSet extends CustomControl { + /** + * This method is called by the `instanceof` operator. + * @returns {symbol} + */ + static get [instanceSymbol]() { + return Symbol.for("@schukai/monster/components/form/fieldset@@instance"); + } + + /** + * + * @return {Components.Form.FieldSet + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + initControlReferences.call(this); + initEventHandler.call(this); + return this; + } + + /** + * To set the options via the html tag the attribute `data-monster-options` must be used. + * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} + * + * The individual configuration values can be found in the table. + * + * @property {Object} templates Template definitions + * @property {string} templates.main Main template + * @property {Object} labels Label definitions + * @property {Object} actions Callbacks + * @property {string} actions.click="throw Error" Callback when clicked + * @property {Object} features Features + * @property {Object} classes CSS classes + * @property {boolean} disabled=false Disabled state + */ + get defaults() { + return Object.assign({}, super.defaults, { + templates: { + main: getTemplate(), + }, + labels: { + }, + classes: { + }, + disabled: false, + features: { + }, + actions: { + click: () => { + throw new Error("the click action is not defined"); + }, + }, + value: null + }); + } + + /** + * + * @return {string} + */ + static getTag() { + return "monster-field-set"; + } + + /** + * + * @return {Array<CSSStyleSheet>} + */ + static getCSSStyleSheet() { + return [FieldSetStyleSheet]; + } + + + /** + * The FieldSet.click() method simulates a click on the internal element. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click} + */ + click() { + if (this.getOption("disabled") === true) { + return; + } + + if ( + this[fieldSetElementSymbol] && + isFunction(this[fieldSetElementSymbol].click) + ) { + this[fieldSetElementSymbol].click(); + } + } + + /** + * The Button.focus() method sets focus on the internal element. + * + * @param {Object} options + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus} + */ + focus(options) { + if (this.getOption("disabled") === true) { + return; + } + + if ( + this[fieldSetElementSymbol] && + isFunction(this[fieldSetElementSymbol].focus) + ) { + this[fieldSetElementSymbol].focus(options); + } + } + + /** + * The Button.blur() method removes focus from the internal element. + */ + blur() { + if ( + this[fieldSetElementSymbol] && + isFunction(this[fieldSetElementSymbol].blur) + ) { + this[fieldSetElementSymbol].blur(); + } + } + + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @return {boolean} + */ + static get formAssociated() { + return true; + } + + /** + * The current value of the form control. + * + * ```js + * e = document.querySelector('monster-field-set'); + * console.log(e.value) + * ``` + * + * @property {string} + */ + get value() { + return this.getOption("value"); + } + + /** + * Set value of the form control. + * + * ``` + * e = document.querySelector('monster-field-set'); + * e.value=1 + * ``` + * + * @property {string} value + * @throws {Error} unsupported type + */ + set value(value) { + this.setOption("value", value); + try { + this?.setFormValue(this.value); + } catch (e) { + addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message); + } + } + + +} + +/** + * @private + * @return {initEventHandler} + * @fires Monster.Components.Form.event:monster-field-set-clicked + */ +function initEventHandler() { + const self = this; + const element = this[fieldSetElementSymbol]; + + const type = "click"; + + element.addEventListener(type, function (event) { + const callback = self.getOption("actions.click"); + + fireCustomEvent(self, "monster-field-set-clicked", { + element: self, + }); + + if (!isFunction(callback)) { + return; + } + + const element = findTargetElementFromEvent( + event, + ATTRIBUTE_ROLE, + "control", + ); + + if (!(element instanceof Node && self.hasNode(element))) { + return; + } + + callback.call(self, event); + }); + + return this; +} + +/** + * @private + */ +function initControlReferences() { + this[fieldSetElementSymbol] = this.shadowRoot.querySelector( + `[${ATTRIBUTE_ROLE}="control"]`, + ); +} + +/** + * @private + * @return {string} + */ +function getTemplate() { + // language=HTML + return ` + <div data-monster-role="control" part="control"> + </div>`; +} + + +registerCustomElement(FieldSet); diff --git a/source/components/form/style/field-set.pcss b/source/components/form/style/field-set.pcss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/source/components/form/stylesheet/field-set.mjs b/source/components/form/stylesheet/field-set.mjs new file mode 100644 index 0000000000000000000000000000000000000000..46532ef239f18b76aba95bc83f4dcc5e1d3f812d --- /dev/null +++ b/source/components/form/stylesheet/field-set.mjs @@ -0,0 +1,31 @@ +/** + * Copyright © schukai GmbH and all contributing authors, 2024. 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. + */ + +import {addAttributeToken} from "../../../dom/attributes.mjs"; +import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs"; + +export {FieldSetStyleSheet} + +/** + * @private + * @type {CSSStyleSheet} + */ +const FieldSetStyleSheet = new CSSStyleSheet(); + +try { + FieldSetStyleSheet.insertRule(` +@layer fieldset { + +}`, 0); +} catch (e) { + addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + ""); +}