Something went wrong on our end
Select Git revision
global.html#Monster
-
Volker Schukai authoredVolker Schukai authored
createNewComponentClass.mjs 14.68 KiB
import {sourcePath, projectRoot, license} from "./import.mjs";
import {writeFileSync, readFileSync, mkdirSync, existsSync} from "fs";
import {dirname, join} from "path";
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 { CustomElement } from "{{BACK_TO_ROOT_PATH}}/dom/customelement.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");
/**
* A {{CLASSNAME}}
*
* @fragments /fragments/components/form/{{HTML_TAG_SUFFIX}}/
*
* @example /examples/components/form/{{HTML_TAG_SUFFIX}}-simple
*
* @since {{VERSION}}
* @copyright schukai GmbH
* @summary A beautiful {{CLASSNAME}} that can make your life easier and also looks good.
*/
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}}/{{HTML_TAG_SUFFIX}}@@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 {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [{{CLASSNAME}}StyleSheet];
}
{{HTML_ELEMENT_FORM_INTERNALS}}
}
/**
* @private
* @return {initEventHandler}
* @fires 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
* @return {void}
*/
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}
* @returns {void}
*/
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 {{CLASSNAME}}.focus() method sets focus on the internal element.
*
* @param {Object} options
* @returns {void}
* @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 {{CLASSNAME}}.blur() method removes focus from the internal element.
* @returns {void}
*/
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)
* \`\`\`
*
* @return {mixed} The value of the form control
*/
get value() {
return this.getOption("value");
}
/**
* Set value of the form control.
*
* \`\`\`
* e = document.querySelector('monster-{{HTML_TAG_SUFFIX}}');
* e.value=1
* \`\`\`
*
* @param {mixed} value
* @return {void}
* @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: task create-new-component-class --classname=YourClassName --namespace=Components.Your.Namespace");
}
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}/showroom/source/examples/${namespaceAsPath}`;
const exampleFilename = `${exampleDirectory}/${htmlTagSuffix}.mjs`;
const fragmentsDirectory = `${projectRoot}/showroom/source/fragments/${namespaceAsPath}`;
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 code = template.replace(/{{HTML_ELEMENT_FORM_INTERNALS}}/g, formInternalsCode)
.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 {
const d = dirname(filename);
console.log(`Creating ${d}`);
if (!existsSync(d)) {
mkdirSync(d, {recursive: true});
mkdirSync(join(d, "style"), {recursive: true});
mkdirSync(join(d, "stylesheet"), {recursive: true});
}
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);
});
const overviewHTML = `
<h2>Introduction</h2>
<p>
This is the Monster {{CLASSNAME}} component. It is a versatile and customizable control element
to an interactive and engaging user experience that integrates seamlessly into various web applications.
Whether you are developing a simple website or a complex enterprise application, the Monster
Button is designed to increase user interaction and satisfaction.
</p>
<h2>Key Features</h2>
<ul>
<li><strong>Dynamic interaction</strong>: Users can interact with content dynamically,
making the Web experience more intuitive and user-centric.
</li>
<li><strong>Customizable appearance</strong>: Customize the appearance of the button
to match the design of your brand or application to improve visual consistency.
</li>
<li><strong>Accessibility</strong>: Designed with accessibility in mind to ensure all
users have a seamless experience regardless of their browsing context.
</li>
<li><strong>Programmatic Control</strong>: Provides methods such as click, focus,
and blur to programmatically control the behavior of the button, giving developers flexibility.
</li>
</ul>
<h2>Improving the user experience</h2>
<p>
The Monster {{CLASSNAME}} goes beyond the traditional functions of a {{CLASSNAME}} to provide an enhanced
and interactive user experience.
</p>
<p>
These improvements are supported by user studies that show a positive impact on user
commitment and satisfaction.
</p>
<h2>Efficiency in the development process</h2>
<p>
Integrating the Monster {{CLASSNAME}} into your development process is easy. Its compatibility with
standard web technologies and ease of customization allow for seamless integration with
your existing tools and libraries. Whether you are working on a small project or a large
application, Monster {{CLASSNAME}}'s modular design guarantees easy integration that streamlines
your development process and increases your productivity.
</p>
`.replace(/{{CLASSNAME}}/g, argsObj.classname);
const designHTML = `
<h2>Design</h2>
<p>The control element can be adapted to your own requirements. To do this, the control element
can be designed with CSS like almost any other HTML element.</p>
<p>However, there are a few things to bear in mind. As the innards of the control are located
in a ShadowRoot, they cannot be accessed directly with CSS selectors. Only the elements specified
for this purpose can be accessed. These elements have the attribute <i>part</i>.</p>
<p>In CSS, these parts can then be used for styling via a CSS pseudo-element Parts.
Here you can see an example of how you can use this.</p>
<pre><code class="language-css">
::part(container) {
border: 1px solid red;
}
</code></pre>
<!-- p>The following diagram shows the parts and the slots.</p>
<img src="assets/{{NAMESPACE_WITH_DOTS}}.{{CLASSNAME_LOWER}}.svg" alt="Parts and slots of the {{CLASSNAME}}"-->
`.replace(/{{NAMESPACE_WITH_DOTS}}/g, namespaceWithDots)
.replace(/{{CLASSNAME}}/g, argsObj.classname);
const showItHTML = `
<monster-{{HTML_TAG_SUFFIX}}></monster-{{HTML_TAG_SUFFIX}}>
`.replace(/{{HTML_TAG_SUFFIX}}/g, htmlTagSuffix);
try {
if (!existsSync(fragmentsDirectory)) {
mkdirSync(fragmentsDirectory, {recursive: true});
}
writeFileSync(`${fragmentsDirectory}/overview.html`, overviewHTML);
writeFileSync(`${fragmentsDirectory}/design.html`,designHTML);
writeFileSync(`${fragmentsDirectory}/show-it.html`, showItHTML);
} catch (e) {
console.error(e);
process.exit(1);
}