From 3b9b28610ff6e5ac64ad580f4d91016a422cb145 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Thu, 27 Feb 2025 19:16:13 +0100
Subject: [PATCH] feat: new calendar control #296

---
 development/issues/open/296.html              |  22 +++
 development/issues/open/296.mjs               |  14 ++
 development/templates/vite.config.mjs         |  92 ++++++++-
 source/components/time/calendar.mjs           | 181 ++++++++++++++++++
 source/components/time/style/calendar.pcss    |   0
 .../components/time/stylesheet/calendar.mjs   |  31 +++
 6 files changed, 339 insertions(+), 1 deletion(-)
 create mode 100644 development/issues/open/296.html
 create mode 100644 development/issues/open/296.mjs
 create mode 100644 source/components/time/calendar.mjs
 create mode 100644 source/components/time/style/calendar.pcss
 create mode 100644 source/components/time/stylesheet/calendar.mjs

diff --git a/development/issues/open/296.html b/development/issues/open/296.html
new file mode 100644
index 000000000..053fab092
--- /dev/null
+++ b/development/issues/open/296.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>new calendar control #296</title>
+    <script src="./296.mjs" type="module"></script>
+</head>
+<body>
+    <h1>new calendar control #296</h1>
+    <p></p>
+    <ul>
+        <li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/296">Issue #296</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/296.mjs b/development/issues/open/296.mjs
new file mode 100644
index 000000000..0be173717
--- /dev/null
+++ b/development/issues/open/296.mjs
@@ -0,0 +1,14 @@
+/**
+* @file development/issues/open/296.mjs
+* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/296
+* @description new calendar control
+* @issue 296
+*/
+
+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";
+
diff --git a/development/templates/vite.config.mjs b/development/templates/vite.config.mjs
index 1d54977cb..fb7f48e9c 100644
--- a/development/templates/vite.config.mjs
+++ b/development/templates/vite.config.mjs
@@ -1,4 +1,3 @@
-import {resolve} from 'path';
 import {defineConfig} from 'vite'
 
 import {projectRoot} from './import.mjs';
@@ -15,8 +14,97 @@ import postcssResponsiveType from 'postcss-responsive-type';
 import {ViteMinifyPlugin} from 'vite-plugin-minify'
 import {viteMockServe} from 'vite-plugin-mock';
 
+import fs from 'fs';
+import path from 'path';
+
 import {buildCSS, createScriptFilenameFromStyleFilename} from '../scripts/buildStylePostCSS.mjs';
 
+function createIssueDirectoryListingPlugin() {
+    return {
+        name: 'create-issue-directory-listing-plugin',
+        configureServer(server) {
+            server.middlewares.use((req, res, next) => {
+                const urlPath = decodeURIComponent(req.url.split('?')[0]); // Verhindert Invalid URL Fehler
+                const pp = path.join(server.config.root, '/development/issues');
+                const fsPath = path.join(pp, urlPath);
+
+
+                if (path.extname(urlPath)) {
+                    if (urlPath.endsWith('.html') || urlPath.match(/\/(open|closed)\/\d+\.m?js$/)) {
+                        req.url = '/development/issues' + req.url;
+                        next();
+                        return;
+                    }
+
+                    next();
+                    return;
+                }
+
+                if (fs.existsSync(fsPath) && fs.statSync(fsPath).isDirectory()) {
+                    const files = fs.readdirSync(fsPath);
+
+
+                    // filter only html files and all directories
+                    const htmlAndDirectories = files.filter(file => {
+                        return fs.statSync(path.join(fsPath, file)).isDirectory() || file.endsWith('.html');
+                    });
+
+                    // Korrekte Erstellung der Liste mit absoluten URLs
+                    const listItems = htmlAndDirectories.map(file => {
+                        const filePath = path.posix.join(urlPath, file);
+                        const fullFilePath = path.join(fsPath, file);
+
+                        // Check if the file is an HTML file to extract title
+                        if (file.endsWith('.html')) {
+                            const content = fs.readFileSync(fullFilePath, 'utf8');
+                            const titleMatch = content.match(/<title>(.*?)<\/title>/);
+                            const title = titleMatch ? titleMatch[1] : file;  // Use the file name if no title is found
+                            return `<li><a href="`+filePath+`">`+title+`</a></li>`;
+                        } else {
+                            return `<li><a href="`+filePath+`">`+file+`</a></li>`;
+                        }
+                    }).join('');
+
+                    let homeButton = '';
+                    if (urlPath !== '/') {
+                        homeButton = `<a href="/">🏠 Home</a>`;
+                    }
+
+                    // Sicherstellen, dass das HTML korrekt zurückgegeben wird
+                    const html = `
+                        <!DOCTYPE html>
+                        <html lang="en">
+                        <head>
+                            <meta charset="UTF-8">
+                            <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                            <title>Index of ${urlPath}</title>
+                            <style>
+                                body { font-family: Arial, sans-serif; margin: 20px; }
+                                ul { list-style-type: none; padding: 0; }
+                                li { margin: 5px 0; }
+                                a { text-decoration: none; color: #007bff; }
+                                a:hover { text-decoration: underline; }
+                            </style>
+                        </head>
+                        <body>
+                            ` + homeButton + `
+                            <h1>Index of ` + urlPath + `</h1>
+                            <ul>` + listItems + `</ul>
+                        </body>
+                        </html>
+                    `;
+
+                    res.setHeader('Content-Type', 'text/html');
+                    res.end(html);
+                } else {
+                    next();
+                }
+            });
+        },
+    };
+}
+
+
 export default defineConfig({
     clearScreen: false,
 
@@ -41,6 +129,8 @@ export default defineConfig({
         ViteMinifyPlugin(),
         //directoryIndex({}),
 
+        createIssueDirectoryListingPlugin(),
+
         viteMockServe({
             mockPath: projectRoot + '/development/mock',
         }),
diff --git a/source/components/time/calendar.mjs b/source/components/time/calendar.mjs
new file mode 100644
index 000000000..f2507bae0
--- /dev/null
+++ b/source/components/time/calendar.mjs
@@ -0,0 +1,181 @@
+/**
+ * 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 { CustomElement } from "../../dom/customelement.mjs";
+import {
+	assembleMethodSymbol,
+	registerCustomElement,
+} from "../../dom/customelement.mjs";
+import { findTargetElementFromEvent } from "../../dom/events.mjs";
+import { isFunction } from "../../types/is.mjs";
+import { CalendarStyleSheet } from "./stylesheet/calendar.mjs";
+import { fireCustomEvent } from "../../dom/events.mjs";
+
+export { Calendar };
+
+/**
+ * @private
+ * @type {symbol}
+ */
+export const calendarElementSymbol = Symbol("calendarElement");
+
+/**
+ * A Calendar
+ * 
+ * @fragments /fragments/components/time/calendar/
+ * 
+ * @example /examples/components/time/calendar-simple
+ * 
+ * @since 3.112.0
+ * @copyright schukai GmbH
+ * @summary A beautiful Calendar that can make your life easier and also looks good.
+ */
+ class Calendar extends CustomElement {
+	/**
+	 * This method is called by the `instanceof` operator.
+	 * @returns {symbol}
+	 */
+	static get [instanceSymbol]() {
+		return Symbol.for("@schukai/monster/components/time/calendar@@instance");
+	}
+
+	/**
+	 *
+	 * @return {Components.Time.Calendar
+	 */
+	[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");
+				},
+			}
+		});
+	}
+
+	/**
+	 * @return {string}
+	 */
+	static getTag() {
+		return "monster-calendar";
+	}
+
+	/**
+	 * @return {CSSStyleSheet[]}
+	 */
+	static getCSSStyleSheet() {
+		return [CalendarStyleSheet];
+	}
+	
+	
+}
+
+/**
+ * @private
+ * @return {initEventHandler}
+ * @fires monster-calendar-clicked
+ */
+function initEventHandler() {
+	const self = this;
+	const element = this[calendarElementSymbol];
+
+	const type = "click";
+
+	element.addEventListener(type, function (event) {
+		const callback = self.getOption("actions.click");
+
+		fireCustomEvent(self, "monster-calendar-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[calendarElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}="control"]`,
+	);
+}
+
+/**
+ * @private
+ * @return {string}
+ */
+function getTemplate() {
+	// language=HTML
+	return `
+        <div data-monster-role="control" part="control">
+        </div>`;
+}
+
+
+registerCustomElement(Calendar);
diff --git a/source/components/time/style/calendar.pcss b/source/components/time/style/calendar.pcss
new file mode 100644
index 000000000..e69de29bb
diff --git a/source/components/time/stylesheet/calendar.mjs b/source/components/time/stylesheet/calendar.mjs
new file mode 100644
index 000000000..972beb091
--- /dev/null
+++ b/source/components/time/stylesheet/calendar.mjs
@@ -0,0 +1,31 @@
+/**
+ * Copyright © schukai GmbH and all contributing authors, 2025. 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 {CalendarStyleSheet}
+
+/**
+ * @private
+ * @type {CSSStyleSheet}
+ */
+const CalendarStyleSheet = new CSSStyleSheet();
+
+try {
+  CalendarStyleSheet.insertRule(`
+@layer calendar { 
+ 
+}`, 0);
+} catch (e) {
+  addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
+}
-- 
GitLab