Skip to content
Snippets Groups Projects
Select Git revision
  • 66318fe6da723e3ccd5dc85ae852b94e82be8823
  • master default protected
  • 1.2.4
  • 1.2.3
  • 1.2.2
  • 1.2.1
  • 1.2.0
  • v1.1.0
8 results

config.go

Blame
  • 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);
    }