Skip to content
Snippets Groups Projects
Verified Commit 7e63f288 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: new calendar control #296

parent 3b9b2861
No related branches found
No related tags found
No related merge requests found
Showing
with 1851 additions and 614 deletions
......@@ -3,19 +3,124 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>new calendar control #296</title>
<title>new month calendar control #296</title>
<script src="./296.mjs" type="module"></script>
</head>
<body>
<h1>new calendar control #296</h1>
<h1>new month 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 -->
<monster-datasource-dom id="menu-dom">
<script type="application/json">
[
{
"id": "1",
"startDate": "2025-03-11",
"endDate": "2025-04-05",
"color": "#df0fb6"
},
{
"id": "2",
"startDate": "2025-03-13",
"endDate": "2025-03-15",
"color": "#ffffff"
},
{
"id": "3",
"startDate": "2025-03-12",
"endDate": "2025-03-20",
"color": "#ec4c6e"
},
{
"id": "4",
"startDate": "2025-03-25",
"endDate": "2025-03-27",
"color": "#a32408"
},
{
"id": "5",
"startDate": "2025-03-28",
"endDate": "2025-03-30",
"color": "#f7f7f7"
},
{
"id": "6",
"startDate": "2025-03-31",
"endDate": "2025-04-02",
"color": "#f7f7f7"
},
{
"id": "7",
"startDate": "2025-04-01",
"endDate": "2025-04-03",
"color": "#888888"
},
{
"id": "8",
"startDate": "2025-03-04",
"endDate": "2025-04-06",
"color": "#777777"
},
{
"id": "9",
"startDate": "2025-03-04",
"endDate": "2025-04-09",
"color": "#666666"
},
{
"id": "10",
"startDate": "2025-03-04",
"endDate": "2025-04-12",
"color": "#333333"
},
{
"id": "11",
"startDate": "2025-03-04",
"endDate": "2025-04-15",
"color": "#f70022"
},
{
"id": "12",
"startDate": "2025-03-04",
"endDate": "2025-04-18",
"color": "#ff3322"
},
{
"id": "13",
"startDate": "2025-03-04",
"endDate": "2025-04-21",
"color": "#0000ff"
},
{
"id": "14",
"startDate": "2025-03-04",
"endDate": "2025-04-24",
"color": "#00ff00"
},
{
"id": "15",
"startDate": "2025-03-04",
"endDate": "2025-04-27",
"color": "#ff0000"
}
]
</script>
</monster-datasource-dom>
<main style="width: 800px;
margin: 0 auto;">
<monster-month-calendar
data-monster-option-datasource-selector="#menu-dom"
data-monster-option-startdate="2025-03-11">
</monster-month-calendar>
</main>
</body>
......
......@@ -11,4 +11,6 @@ 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/time/month-calendar.mjs";
import "../../../source/components/time/appointment/segment.mjs";
import "../../../source/components/datatable/datasource/dom.mjs";
......@@ -77,7 +77,7 @@ function createIssueDirectoryListingPlugin() {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index of ${urlPath}</title>
<title>Index of ${urlPath} by Monster — Build fantastic websites!</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
ul { list-style-type: none; padding: 0; }
......@@ -88,7 +88,8 @@ function createIssueDirectoryListingPlugin() {
</head>
<body>
` + homeButton + `
<h1>Index of ` + urlPath + `</h1>
<h1>Monster — Build fantastic websites!</h1>
<h2>Index of ` + urlPath + `</h2>
<ul>` + listItems + `</ul>
</body>
</html>
......
This diff is collapsed.
......@@ -10,10 +10,10 @@
* 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";
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
export {CameraCaptureStyleSheet}
export { CameraCaptureStyleSheet };
/**
* @private
......@@ -22,10 +22,17 @@ export {CameraCaptureStyleSheet}
const CameraCaptureStyleSheet = new CSSStyleSheet();
try {
CameraCaptureStyleSheet.insertRule(`
CameraCaptureStyleSheet.insertRule(
`
@layer cameracapture {
[data-monster-role=control]{box-sizing:border-box;outline:none;width:100%}[data-monster-role=control].flex{align-items:center;display:flex;flex-direction:row}:host{box-sizing:border-box;display:block}:after,:before,:root{--monster-font-family:-apple-system,BlinkMacSystemFont,\"Quicksand\",\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";--monster-font-family-monospace:\"Consolas\",\"Courier New\",\"Roboto Mono\",\"Source Code Pro\",\"Fira Mono\",monospace;--monster-color-primary-1:var(--monster-color-gray-6);--monster-color-primary-2:var(--monster-color-gray-6);--monster-color-primary-3:var(--monster-color-cinnamon-1);--monster-color-primary-4:var(--monster-color-cinnamon-1);--monster-bg-color-primary-1:var(--monster-color-gray-1);--monster-bg-color-primary-2:var(--monster-color-gray-2);--monster-bg-color-primary-3:var(--monster-color-gray-6);--monster-bg-color-primary-4:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-1:var(--monster-color-gray-1);--monster-color-primary-2:var(--monster-color-gray-1);--monster-color-primary-3:var(--monster-color-gray-6);--monster-color-primary-4:var(--monster-color-gray-6);--monster-bg-color-primary-1:var(--monster-color-gray-6);--monster-bg-color-primary-2:var(--monster-color-gray-3);--monster-bg-color-primary-3:var(--monster-color-gray-2);--monster-bg-color-primary-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-4);--monster-color-secondary-2:var(--monster-color-red-4);--monster-color-secondary-3:var(--monster-color-red-1);--monster-color-secondary-4:var(--monster-color-red-1);--monster-bg-color-secondary-1:var(--monster-color-gray-1);--monster-bg-color-secondary-2:var(--monster-color-red-2);--monster-bg-color-secondary-3:var(--monster-color-red-3);--monster-bg-color-secondary-4:var(--monster-color-red-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-secondary-1:var(--monster-color-red-1);--monster-color-secondary-2:var(--monster-color-red-1);--monster-color-secondary-3:var(--monster-color-red-6);--monster-color-secondary-4:var(--monster-color-red-4);--monster-bg-color-secondary-1:var(--monster-color-gray-6);--monster-bg-color-secondary-2:var(--monster-color-red-3);--monster-bg-color-secondary-3:var(--monster-color-red-2);--monster-bg-color-secondary-4:var(--monster-color-red-1)}}:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-4);--monster-color-tertiary-2:var(--monster-color-magenta-4);--monster-color-tertiary-3:var(--monster-color-magenta-6);--monster-color-tertiary-4:var(--monster-color-magenta-1);--monster-bg-color-tertiary-1:var(--monster-color-gray-1);--monster-bg-color-tertiary-2:var(--monster-color-magenta-1);--monster-bg-color-tertiary-3:var(--monster-color-magenta-2);--monster-bg-color-tertiary-4:var(--monster-color-magenta-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-tertiary-1:var(--monster-color-magenta-1);--monster-color-tertiary-2:var(--monster-color-magenta-6);--monster-color-tertiary-3:var(--monster-color-magenta-4);--monster-color-tertiary-4:var(--monster-color-magenta-4);--monster-bg-color-tertiary-1:var(--monster-color-gray-6);--monster-bg-color-tertiary-2:var(--monster-color-magenta-2);--monster-bg-color-tertiary-3:var(--monster-color-magenta-1);--monster-bg-color-tertiary-4:var(--monster-color-magenta-1)}}:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-4);--monster-color-destructive-3:var(--monster-color-red-6);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-4);--monster-bg-color-destructive-2:var(--monster-color-gray-1);--monster-bg-color-destructive-3:var(--monster-color-red-2);--monster-bg-color-destructive-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-destructive-1:var(--monster-color-red-1);--monster-color-destructive-2:var(--monster-color-red-3);--monster-color-destructive-3:var(--monster-color-red-4);--monster-color-destructive-4:var(--monster-color-red-1);--monster-bg-color-destructive-1:var(--monster-color-red-5);--monster-bg-color-destructive-2:var(--monster-color-gray-6);--monster-bg-color-destructive-3:var(--monster-color-red-1);--monster-bg-color-destructive-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-4);--monster-color-success-3:var(--monster-color-green-6);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-3);--monster-bg-color-success-2:var(--monster-color-gray-1);--monster-bg-color-success-3:var(--monster-color-green-2);--monster-bg-color-success-4:var(--monster-color-green-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-success-1:var(--monster-color-green-1);--monster-color-success-2:var(--monster-color-green-2);--monster-color-success-3:var(--monster-color-green-4);--monster-color-success-4:var(--monster-color-green-1);--monster-bg-color-success-1:var(--monster-color-green-5);--monster-bg-color-success-2:var(--monster-color-gray-6);--monster-bg-color-success-3:var(--monster-color-green-1);--monster-bg-color-success-4:var(--monster-color-green-3)}}:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-4);--monster-color-warning-3:var(--monster-color-orange-6);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-3);--monster-bg-color-warning-2:var(--monster-color-gray-1);--monster-bg-color-warning-3:var(--monster-color-orange-2);--monster-bg-color-warning-4:var(--monster-color-orange-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-warning-1:var(--monster-color-orange-1);--monster-color-warning-2:var(--monster-color-orange-3);--monster-color-warning-3:var(--monster-color-orange-4);--monster-color-warning-4:var(--monster-color-orange-1);--monster-bg-color-warning-1:var(--monster-color-orange-5);--monster-bg-color-warning-2:var(--monster-color-gray-6);--monster-bg-color-warning-3:var(--monster-color-orange-1);--monster-bg-color-warning-4:var(--monster-color-orange-3)}}:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-4);--monster-color-error-3:var(--monster-color-red-6);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-4);--monster-bg-color-error-2:var(--monster-color-gray-1);--monster-bg-color-error-3:var(--monster-color-red-2);--monster-bg-color-error-4:var(--monster-color-red-5)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-error-1:var(--monster-color-red-1);--monster-color-error-2:var(--monster-color-red-3);--monster-color-error-3:var(--monster-color-red-4);--monster-color-error-4:var(--monster-color-red-1);--monster-bg-color-error-1:var(--monster-color-red-5);--monster-bg-color-error-2:var(--monster-color-gray-6);--monster-bg-color-error-3:var(--monster-color-red-1);--monster-bg-color-error-4:var(--monster-color-red-4)}}:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-selection-1:var(--monster-color-gray-6);--monster-color-selection-2:var(--monster-color-gray-6);--monster-color-selection-3:var(--monster-color-gray-6);--monster-color-selection-4:var(--monster-color-gray-1);--monster-bg-color-selection-1:var(--monster-color-yellow-2);--monster-bg-color-selection-2:var(--monster-color-yellow-1);--monster-bg-color-selection-3:var(--monster-color-yellow-2);--monster-bg-color-selection-4:var(--monster-color-yellow-6)}}:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-4);--monster-color-primary-disabled-4:var(--monster-color-gray-4);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-1);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-6)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-color-primary-disabled-1:var(--monster-color-gray-4);--monster-color-primary-disabled-2:var(--monster-color-gray-4);--monster-color-primary-disabled-3:var(--monster-color-gray-3);--monster-color-primary-disabled-4:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-1:var(--monster-color-gray-6);--monster-bg-color-primary-disabled-2:var(--monster-color-gray-3);--monster-bg-color-primary-disabled-3:var(--monster-color-gray-2);--monster-bg-color-primary-disabled-4:var(--monster-color-gray-1)}}:after,:before,:root{--monster-color-gradient-1:#833ab4;--monster-color-gradient-2:#fd1d1d;--monster-color-gradient-3:#fcb045;--monster-box-shadow-1:none;--monster-box-shadow-2:-1px 1px 10px 1px hsla(0,0%,76%,.61);--monster-text-shadow:none;--monster-theme-control-bg-color:var(--monster-color-seashell-1);--monster-theme-control-color:var(--monster-color-seashell-6);--monster-theme-control-hover-color:var(--monster-color-seashell-6);--monster-theme-control-hover-bg-color:var(--monster-color-seashell-2);--monster-theme-control-border-width:2px;--monster-theme-control-border-style:solid;--monster-theme-control-border-radius:0;--monster-theme-control-border-color:var(--monster-color-primary-1)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-control-bg-color:var(--monster-color-gray-5);--monster-theme-control-color:var(--monster-color-gray-1);--monster-theme-control-border-color:var(--monster-color-gray-3);--monster-theme-control-hover-color:var(--monster-color-gray-1);--monster-theme-control-hover-bg-color:var(--monster-color-gray-6)}}:after,:before,:root{--monster-theme-on-color:var(--monster-color-green-1);--monster-theme-on-bg-color:var(--monster-color-green-5);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-4)}@media (prefers-color-scheme:dark){:after,:before,:root{--monster-theme-on-color:var(--monster-color-gray-6);--monster-theme-on-bg-color:var(--monster-color-gray-1);--monster-theme-off-color:var(--monster-color-gray-1);--monster-theme-off-bg-color:var(--monster-color-gray-5)}}:after,:before,:root{--monster-border-style:solid;--monster-border-width:3px;--monster-border-radius:0;--monster-outline-width:1px;--monster-popper-witharrrow-distance:-4px;--monster-z-index-default:0;--monster-z-index-outline:10;--monster-z-index-dropdown:200;--monster-z-index-dropdown-overlay:210;--monster-z-index-sticky:300;--monster-z-index-sticky-overlay:310;--monster-z-index-fixed:400;--monster-z-index-fixed-overlay:410;--monster-z-index-modal-backdrop:500;--monster-z-index-modal-backdrop-overlay:510;--monster-z-index-offcanvas:600;--monster-z-index-offcanvas-overlay:610;--monster-z-index-modal:700;--monster-z-index-modal-overlay:710;--monster-z-index-popover:800;--monster-z-index-popover-overlay:810;--monster-z-index-tooltip:800;--monster-z-index-tooltip-overlay:910;--monster-space-0:0;--monster-space-1:2px;--monster-space-2:4px;--monster-space-3:6px;--monster-space-4:10px;--monster-space-5:16px;--monster-space-6:26px;--monster-space-7:42px;--monster-breakpoint-0:480px;--monster-breakpoint-4:480px;--monster-breakpoint-7:768px;--monster-breakpoint-9:992px;--monster-breakpoint-12:1200px;--monster-dragger-width:2px;--monster-dragger-handle-width:4px;--monster-dragger-handle-height:50px}div[data-monster-role=popper]{align-content:center;background:var(--monster-bg-color-primary-1);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);box-sizing:border-box;color:var(--monster-color-primary-1);display:none;justify-content:space-between;left:0;padding:1.1em;position:absolute;top:0;width:-moz-max-content;width:max-content;z-index:var(--monster-z-index-modal)}div[data-monster-role=popper] div[data-monster-role=arrow]{background:var(--monster-bg-color-primary-1);height:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);pointer-events:none;position:absolute;width:calc(max(var(--monster-popper-witharrrow-distance), -1 * var(--monster-popper-witharrrow-distance))*2);z-index:-1}[data-monster-role=control]{background-color:var(--monster-color-primary-4);border-color:var(--monster-bg-color-primary-4);border-radius:var(--monster-border-radius);border-style:var(--monster-border-style);border-width:var(--monster-border-width);box-shadow:var(--monster-box-shadow-1);color:var(--monster-bg-color-primary-4);display:flex;flex-direction:column;justify-content:space-between;margin:0;padding:0}canvas,video{display:block;max-height:100%;max-width:100%}monster-button::part(button){border:none}
}`, 0);
}`,
0,
);
} catch (e) {
addAttributeToken(document.getRootNode().querySelector('html'), ATTRIBUTE_ERRORMESSAGE, e + "");
addAttributeToken(
document.getRootNode().querySelector("html"),
ATTRIBUTE_ERRORMESSAGE,
e + "",
);
}
......@@ -13,26 +13,26 @@
*/
import {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
assembleMethodSymbol,
CustomElement,
registerCustomElement,
} from "../../dom/customelement.mjs";
import {findElementWithSelectorUpwards} from "../../dom/util.mjs";
import {ThemeStyleSheet} from "../stylesheet/theme.mjs";
import {Datasource} from "./datasource.mjs";
import {SpinnerStyleSheet} from "../stylesheet/spinner.mjs";
import {isString} from "../../types/is.mjs";
import {instanceSymbol} from "../../constants.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { Datasource } from "./datasource.mjs";
import { SpinnerStyleSheet } from "../stylesheet/spinner.mjs";
import { isString } from "../../types/is.mjs";
import { instanceSymbol } from "../../constants.mjs";
import "../form/select.mjs";
import "./datasource/dom.mjs";
import "./datasource/rest.mjs";
import "../form/popper.mjs";
import "../form/context-error.mjs";
import {StatusStyleSheet} from "./stylesheet/status.mjs";
import {Formatter} from "../../text/formatter.mjs";
import { StatusStyleSheet } from "./stylesheet/status.mjs";
import { Formatter } from "../../text/formatter.mjs";
export {DatasourceStatus};
export { DatasourceStatus };
/**
* @private
......@@ -59,97 +59,96 @@ const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement");
* @summary The Status component is used to show the current status of a datasource.
*/
class DatasourceStatus extends CustomElement {
/**
*/
constructor() {
super();
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/datatable/status@@instance");
}
/**
* 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 Datasource configuration
* @property {string} datasource.selector The selector of the datasource
* @property {Object} callbacks Callbacks
* @property {Function} callbacks.onError Callback function for error handling <code>function(message: string, event: Event): string</code>
* @property {Object} timeouts Timeouts
* @property {number} timeouts.message Timeout for the message
* @property {Object} state State
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
datasource: {
selector: null,
},
callbacks: {
onError: null
},
timeouts: {
message: 4000,
},
state: {
spinner: "hide",
},
});
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datasource-status";
}
/**
* @private
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
}
/**
*
* @param message
* @param timeout
* @returns {DatasourceStatus}
*/
setErrorMessage(message, timeout) {
this[errorElementSymbol].setErrorMessage(message, timeout);
return this;
}
/**
*
* @return [CSSStyleSheet]
*/
static getCSSStyleSheet() {
return [StatusStyleSheet, SpinnerStyleSheet, ThemeStyleSheet];
}
/**
*/
constructor() {
super();
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/datatable/status@@instance");
}
/**
* 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 Datasource configuration
* @property {string} datasource.selector The selector of the datasource
* @property {Object} callbacks Callbacks
* @property {Function} callbacks.onError Callback function for error handling <code>function(message: string, event: Event): string</code>
* @property {Object} timeouts Timeouts
* @property {number} timeouts.message Timeout for the message
* @property {Object} state State
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
datasource: {
selector: null,
},
callbacks: {
onError: null,
},
timeouts: {
message: 4000,
},
state: {
spinner: "hide",
},
});
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-datasource-status";
}
/**
* @private
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
}
/**
*
* @param message
* @param timeout
* @returns {DatasourceStatus}
*/
setErrorMessage(message, timeout) {
this[errorElementSymbol].setErrorMessage(message, timeout);
return this;
}
/**
*
* @return [CSSStyleSheet]
*/
static getCSSStyleSheet() {
return [StatusStyleSheet, SpinnerStyleSheet, ThemeStyleSheet];
}
}
/**
......@@ -158,85 +157,84 @@ class DatasourceStatus extends CustomElement {
* @throws {Error} no shadow-root is defined
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[errorElementSymbol] = this.shadowRoot.querySelector(
"monster-context-error",
);
this[errorElementSymbol] = this.shadowRoot.querySelector(
"monster-context-error",
);
}
/**
* @private
*/
function initEventHandler() {
const selector = this.getOption("datasource.selector", "");
const self = this;
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");
}
let fadeOutTimer = null;
this[datasourceLinkedElementSymbol] = element;
element.addEventListener("monster-datasource-fetched", function () {
fadeOutTimer = setTimeout(() => {
self.setOption("state.spinner", "hide");
}, 800);
});
element.addEventListener("monster-datasource-fetch", function () {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
fadeOutTimer = null;
}
self.setOption("state.spinner", "show");
});
element.addEventListener("monster-datasource-error", function (event) {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
fadeOutTimer = null;
}
self.setOption("state.spinner", "hide");
const timeout = self.getOption("timeouts.message", 4000);
let msg = "Cannot load data";
try {
if (event.detail.error instanceof Error) {
msg = event.detail.error.message;
} else if (event.detail.error instanceof Object) {
msg = JSON.stringify(event.detail.error);
} else if (event.detail.error instanceof String) {
msg = event.detail.error;
} else if (event.detail.error instanceof Number) {
msg = event.detail.error.toString();
} else {
msg = event.detail.error;
}
} catch (e) {
} finally {
const callback = self.getOption("callbacks.onError", null);
if (callback) {
callback.call(self, msg, event);
} else {
self[errorElementSymbol].setErrorMessage(msg, timeout);
}
}
});
}
const selector = this.getOption("datasource.selector", "");
const self = this;
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");
}
let fadeOutTimer = null;
this[datasourceLinkedElementSymbol] = element;
element.addEventListener("monster-datasource-fetched", function () {
fadeOutTimer = setTimeout(() => {
self.setOption("state.spinner", "hide");
}, 800);
});
element.addEventListener("monster-datasource-fetch", function () {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
fadeOutTimer = null;
}
self.setOption("state.spinner", "show");
});
element.addEventListener("monster-datasource-error", function (event) {
if (fadeOutTimer) {
clearTimeout(fadeOutTimer);
fadeOutTimer = null;
}
self.setOption("state.spinner", "hide");
const timeout = self.getOption("timeouts.message", 4000);
let msg = "Cannot load data";
try {
if (event.detail.error instanceof Error) {
msg = event.detail.error.message;
} else if (event.detail.error instanceof Object) {
msg = JSON.stringify(event.detail.error);
} else if (event.detail.error instanceof String) {
msg = event.detail.error;
} else if (event.detail.error instanceof Number) {
msg = event.detail.error.toString();
} else {
msg = event.detail.error;
}
} catch (e) {
} finally {
const callback = self.getOption("callbacks.onError", null);
if (callback) {
callback.call(self, msg, event);
} else {
self[errorElementSymbol].setErrorMessage(msg, timeout);
}
}
});
}
}
/**
......@@ -244,8 +242,8 @@ function initEventHandler() {
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
// language=HTML
return `
<div data-monster-role="control" part="control"
data-monster-attributes="disabled path:disabled | if:true">
<monster-context-error
......
......@@ -175,9 +175,8 @@ class Reload extends CustomElement {
return loadContent.call(this);
} catch (e) {
addErrorAttribute(this, e);
return Promise.reject(e)
return Promise.reject(e);
}
}
}
......
......@@ -76,7 +76,8 @@ function loadAndAssignContent(element, url, options, filter) {
if (oldScript.defer) newScript.defer = oldScript.defer;
if (oldScript.crossOrigin) newScript.crossOrigin = oldScript.crossOrigin;
if (oldScript.integrity) newScript.integrity = oldScript.integrity;
if (oldScript.referrerPolicy) newScript.referrerPolicy = oldScript.referrerPolicy;
if (oldScript.referrerPolicy)
newScript.referrerPolicy = oldScript.referrerPolicy;
newScript.textContent = oldScript.textContent;
document.head.appendChild(newScript);
if (oldScript.parentNode) {
......@@ -121,7 +122,9 @@ function loadContent(url, options) {
if (statusClass === "4") {
throw new Error(`client error ${response.statusText}`);
}
throw new Error(`undefined status (${response.status} / ${response.statusText}) or type (${response.type})`);
throw new Error(
`undefined status (${response.status} / ${response.statusText}) or type (${response.type})`,
);
}
return response.text().then((content) => ({
content,
......
/**
* 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, initMethodSymbol } from "../../dom/customelement.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction } from "../../types/is.mjs";
import { AppointmentStyleSheet } from "./stylesheet/appointment.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
export { Appointment };
/**
* @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 Appointment extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/time/appointment@@instance");
}
[initMethodSymbol]() {
super[initMethodSymbol]();
}
/**
*
* @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: {},
// locale : {
// weekdayFormat: 'short',
// },
startDate: "",
calendarDays: [],
calendarWeekdays: [],
});
}
/**
* @return {string}
*/
static getTag() {
return "monster-appointment";
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [AppointmentStyleSheet];
}
}
//
// function getWeekdays(format = 'long') {
//
// const locale = getLocaleOfDocument();
//
// const weekdays = [];
// for (let i = 0; i < 7; i++) {
// const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
// weekdays.push(new Intl.DateTimeFormat(locale, { weekday: format }).format(date));
// }
// return weekdays;
// }
//
// /**
// * Generates two arrays: one for the calendar grid (42 days) and one for the weekday headers (7 days).
// * The grid always starts on the Monday of the week that contains the 1st of the given month.
// *
// * @returns {Object} An object containing:
// * - calendarDays: Array of 42 objects, each representing a day.
// * - calendarWeekdays: Array of 7 objects, each representing a weekday header.
// */
// function generateCalendarData() {
//
// let selectedDate = this.getOption("startDate");
// if (!(selectedDate instanceof Date)) {
// if (typeof selectedDate === "string") {
//
// try {
// selectedDate = new Date(selectedDate);
// } catch (e) {
// addErrorAttribute(this, "Invalid calendar date");
// return { calendarDays, calendarWeekdays };
// }
// } else {
// addErrorAttribute(this, "Invalid calendar date");
// return { calendarDays, calendarWeekdays };
// }
//
// }
//
// const calendarDays = [];
// let calendarWeekdays = [];
//
// if (!(selectedDate instanceof Date)) {
// addErrorAttribute(this, "Invalid calendar date");
// return { calendarDays, calendarWeekdays };
// }
//
// // Get the year and month from the provided date
// const year = selectedDate.getFullYear();
// const month = selectedDate.getMonth(); // 0-based index (0 = January)
//
// // Create a Date object for the 1st of the given month
// const firstDayOfMonth = new Date(year, month, 1);
//
// // Determine the weekday index of the 1st day, ensuring Monday = 0
// const weekdayIndex = (firstDayOfMonth.getDay() + 6) % 7;
//
// // Calculate the start date: move backward to the Monday of the starting week
// const startDate = new Date(firstDayOfMonth);
// startDate.setDate(firstDayOfMonth.getDate() - weekdayIndex);
//
// // Generate 42 days (6 weeks × 7 days)
// for (let i = 0; i < 42; i++) {
// const current = new Date(startDate);
// current.setDate(startDate.getDate() + i);
//
// const label = current.getDate().toString();
//
// calendarDays.push({
// date: current,
// day: current.getDate(),
// month: current.getMonth() + 1, // 1-based month (1-12)
// year: current.getFullYear(),
// isCurrentMonth: current.getMonth() === month,
// label: label,
// classes: "day-cell " + (current.getMonth() === month ? "current-month" : "other-month") + (current.getDay() === 0 || current.getDay() === 6 ? " weekend" : "") + (current.toDateString() === new Date().toDateString() ? " today" : "")
// });
// }
//
// // Generate weekday header array (Monday through Sunday)
// let format = this.getOption("locale.weekdayFormat");
// if (![ 'long', 'short', 'narrow'].includes(format)) {
// addErrorAttribute(this, "Invalid weekday format option " + format);
// format = 'short';
// }
// const weekdayNames = getWeekdays(format);
// calendarWeekdays = weekdayNames.map((name, index) => {
// return {
// label: name,
// index: index
// };
// });
//
// return { calendarDays, calendarWeekdays };
// }
/**
* @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 data-monster-role="appointment">test</div>
</div>
`;
}
registerCustomElement(Appointment);
......@@ -10,24 +10,29 @@
* For more information about purchasing a commercial license, please contact schukai GmbH.
*/
import { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
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";
} from "../../../dom/constants.mjs";
import { CustomControl } from "../../../dom/customcontrol.mjs";
import {
CustomElement,
initMethodSymbol,
} 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";
} from "../../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../../dom/events.mjs";
import { isFunction } from "../../../types/is.mjs";
import { SegmentStyleSheet } from "./stylesheet/segment.mjs";
import { fireCustomEvent } from "../../../dom/events.mjs";
import { getLocaleOfDocument } from "../../../dom/locale.mjs";
import { addErrorAttribute } from "../../../dom/error.mjs";
export { Calendar };
export { Segment };
/**
* @private
......@@ -37,22 +42,28 @@ 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 {
class Segment 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 Symbol.for(
"@schukai/monster/components/time/appointment/segment@@instance",
);
}
[initMethodSymbol]() {
super[initMethodSymbol]();
}
/**
......@@ -61,6 +72,7 @@ export const calendarElementSymbol = Symbol("calendarElement");
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
return this;
......@@ -87,17 +99,21 @@ export const calendarElementSymbol = Symbol("calendarElement");
main: getTemplate(),
},
labels: {
text: "Appointment",
},
classes: {
},
classes: {},
disabled: false,
features: {
},
actions: {
click: () => {
throw new Error("the click action is not defined");
},
}
features: {},
actions: {},
// locale : {
// weekdayFormat: 'short',
// },
startDate: "",
calendarDays: [],
calendarWeekdays: [],
});
}
......@@ -105,17 +121,15 @@ export const calendarElementSymbol = Symbol("calendarElement");
* @return {string}
*/
static getTag() {
return "monster-calendar";
return "monster-appointment-segment";
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [CalendarStyleSheet];
return [SegmentStyleSheet];
}
}
/**
......@@ -125,33 +139,6 @@ export const calendarElementSymbol = Symbol("calendarElement");
*/
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;
}
......@@ -174,8 +161,9 @@ function getTemplate() {
// language=HTML
return `
<div data-monster-role="control" part="control">
</div>`;
<div data-monster-role="appointment" data-monster-replace="path:labels.text"></div>
</div>
`;
}
registerCustomElement(Calendar);
registerCustomElement(Segment);
@import "../../../style/property.pcss";
@import "../../../style/control.pcss";
@import "../../../style/accessibility.pcss";
@import "../../../style/button.pcss";
@import "../../../style/border.pcss";
@import "../../../style/typography.pcss";
@import "../../../style/theme.pcss";
@import "../../../style/color.pcss";
:host {
display: flex;
z-index: var(--monster-z-index-sticky);
position: absolute;
left: 0;
height: 0.5rem;
}
\ No newline at end of file
This diff is collapsed.
/**
* 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,
getSlottedElements,
initMethodSymbol,
} from "../../dom/customelement.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction, isString } from "../../types/is.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { MonthCalendarStyleSheet } from "./stylesheet/month-calendar.mjs";
import {
datasourceLinkedElementSymbol,
handleDataSourceChanges,
} from "../datatable/util.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { Datasource } from "../datatable/datasource.mjs";
import { Observer } from "../../types/observer.mjs";
export { MonthCalendar };
/**
* @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 MonthCalendar extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/time/calendar@@instance");
}
[initMethodSymbol]() {
super[initMethodSymbol]();
const def = generateCalendarData.call(this);
this.setOption("calendarDays", def.calendarDays);
this.setOption("calendarWeekdays", def.calendarWeekdays);
}
/**
*
* @return {Components.Time.Calendar
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initDataSource.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: {},
locale: {
weekdayFormat: "short",
},
startDate: "",
calendarDays: [],
calendarWeekdays: [],
data: [],
datasource: {
selector: null,
},
});
}
/**
* This method is called when the component is created.
* @return {Promise}
*/
refresh() {
// makes sure that handleDataSourceChanges is called
return new Promise((resolve) => {
this.setOption("data", {});
queueMicrotask(() => {
handleDataSourceChanges.call(this);
placeAppointments();
resolve();
});
});
}
/**
* @return {string}
*/
static getTag() {
return "monster-month-calendar";
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [MonthCalendarStyleSheet];
}
}
/**
* Calculates how many days of an appointment are distributed across calendar rows (weeks).
* Uses the start date of the calendar grid (e.g., from generateCalendarData()) as a reference.
*
* @param {Date} appointmentStart - Start date of the appointment.
* @param {Date} appointmentEnd - End date of the appointment (inclusive).
* @param {Date} calendarGridStart - The first day of the calendar grid (e.g., the Monday from generateCalendarData()).
* @returns {number[]} Array indicating how many days the appointment spans per row.
*
* Example:
* - Appointment: 01.03.2025 (Saturday) to 01.03.2025:
* -> getAppointmentRowsUsingCalendar(new Date("2025-03-01"), new Date("2025-03-01"), calendarGridStart)
* returns: [1] (since it occupies only one day in the first row, starting at column 6).
*
* - Appointment: 01.03.2025 to 03.03.2025:
* -> returns: [2, 1] (first row: Saturday and Sunday, second row: Monday).
*/
function getAppointmentRowsUsingCalendar(
appointmentStart,
appointmentEnd,
calendarGridStart,
) {
const oneDayMs = 24 * 60 * 60 * 1000;
// Calculate the offset (in days) from the calendar start to the appointment start
const offset = Math.floor((appointmentStart - calendarGridStart) / oneDayMs);
// Determine the column index in the calendar row (Monday = 0, ..., Sunday = 6)
let startColumn = offset % 7;
if (startColumn < 0) {
startColumn += 7;
}
// Calculate the total number of days for the appointment (including start and end date)
const totalDays =
Math.floor((appointmentEnd - appointmentStart) / oneDayMs) + 1;
// The first calendar block can accommodate at most (7 - startColumn) days.
const firstRowDays = Math.min(totalDays, 7 - startColumn);
const rows = [firstRowDays];
let remainingDays = totalDays - firstRowDays;
// Handle full weeks (7 days per row)
while (remainingDays > 7) {
rows.push(7);
remainingDays -= 7;
}
// Handle the last row if there are any remaining days
if (remainingDays > 0) {
rows.push(remainingDays);
}
return rows;
}
/**
* @private
* @param format
* @returns {*[]}
*/
function getWeekdays(format = "long") {
const locale = getLocaleOfDocument();
const weekdays = [];
for (let i = 1; i < 8; i++) {
const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
weekdays.push(
new Intl.DateTimeFormat(locale, { weekday: format }).format(date),
);
}
return weekdays;
}
/**
* Assigns a "line" property to the provided segments (with "startIndex" and "columns").
* It checks for horizontal overlaps within each calendar row (7 boxes).
* Always assigns the lowest available "line".
*
* @private
*
* @param {Array} segments - Array of segments, e.g.
* [
* {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-11","startIndex":15},
* {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-17","startIndex":21},
* {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-24","startIndex":28},
* {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-31","startIndex":35}
* ]
* @returns {Array} The segments with assigned "line" property
*/
function assignLinesToSegments(segments) {
const groups = {};
segments.forEach((segment) => {
const week = Math.floor(segment.startIndex / 7);
if (!groups[week]) {
groups[week] = [];
}
groups[week].push(segment);
});
Object.keys(groups).forEach((weekKey) => {
const weekSegments = groups[weekKey];
weekSegments.sort((a, b) => a.startIndex - b.startIndex);
const lineEnds = [];
weekSegments.forEach((segment) => {
const segStart = segment.startIndex;
const segEnd = segment.startIndex + segment.columns - 1;
let placed = false;
for (let line = 0; line < lineEnds.length; line++) {
if (segStart >= lineEnds[line] + 1) {
segment.line = line;
lineEnds[line] = segEnd;
placed = true;
break;
}
}
if (!placed) {
segment.line = lineEnds.length;
lineEnds.push(segEnd);
}
});
});
return segments;
}
/**
* @private
*/
function initDataSource() {
setTimeout(() => {
if (!this[datasourceLinkedElementSymbol]) {
const selector = this.getOption("datasource.selector");
if (isString(selector)) {
const element = findElementWithSelectorUpwards(this, selector);
if (element === null) {
addErrorAttribute(
this,
"the selector must match exactly one element",
);
return;
}
if (!(element instanceof Datasource)) {
addErrorAttribute(this, "the element must be a datasource");
return;
}
this[datasourceLinkedElementSymbol] = element;
element.datasource.attachObserver(
new Observer(handleDataSourceChanges.bind(this)),
);
handleDataSourceChanges.call(this);
placeAppointments.call(this);
} else {
addErrorAttribute(
this,
"the datasource selector is missing or invalid",
);
}
}
}, 10);
}
function placeAppointments() {
const self = this;
const currentWithOfGridCell =
this[calendarElementSymbol].getBoundingClientRect().width / 7;
const appointments = this.getOption("data");
const segments = [];
let maxLineHeight = 0;
appointments.forEach((appointment) => {
if (!appointment?.startDate || !appointment?.endDate) {
addErrorAttribute(this, "Missing start or end date in appointment");
return;
}
const startDate = appointment?.startDate;
let container = self.shadowRoot.querySelector(
`[data-monster-day="${startDate}"]`,
);
if (!container) {
addErrorAttribute(
this,
"Invalid, missing or out of range date in appointment" + startDate,
);
return;
}
// calc length of appointment
const start = new Date(startDate);
const end = new Date(appointment?.endDate);
const calendarDays = this.getOption("calendarDays");
const appointmentRows = getAppointmentRowsUsingCalendar(
start,
end,
calendarDays[0].date,
);
let date = appointment.startDate;
const label =
start !== end
? `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`
: start.toLocaleDateString();
for (let i = 0; i < appointmentRows.length; i++) {
const cols = appointmentRows[i];
const calendarStartDate = new Date(calendarDays[0].date); // First day of the calendar grid
const appointmentDate = new Date(date);
const startIndex = Math.floor(
(appointmentDate - calendarStartDate) / (24 * 60 * 60 * 1000),
);
segments.push({
columns: cols,
label: label,
start: date,
startIndex: startIndex,
appointment: appointment,
});
maxLineHeight = Math.max(maxLineHeight, getTextHeight.call(this, label));
const nextKeyDate = new Date(start.setDate(start.getDate() + cols));
date =
nextKeyDate.getFullYear() +
"-" +
("00" + (nextKeyDate.getMonth() + 1)).slice(-2) +
"-" +
("00" + nextKeyDate.getDate()).slice(-2);
}
});
let container = null;
const sortedSegments = assignLinesToSegments(segments);
for (let i = 0; i < sortedSegments.length; i++) {
const segment = sortedSegments[i];
container = self.shadowRoot.querySelector(
`[data-monster-day="${segment.start}"]`,
);
if (!container) {
addErrorAttribute(
this,
"Invalid, missing or out of range date in appointment" + segment.start,
);
return;
}
const appointmentSegment = document.createElement(
"monster-appointment-segment",
);
appointmentSegment.className = "appointment-segment";
appointmentSegment.style.backgroundColor = segment.appointment.color;
// search a color that is readable on the background color
const rgb = appointmentSegment.style.backgroundColor.match(/\d+/g);
const brightness = Math.round(
(parseInt(rgb[0]) * 299 +
parseInt(rgb[1]) * 587 +
parseInt(rgb[2]) * 114) /
1000,
);
if (brightness > 125) {
appointmentSegment.style.color = "#000000";
} else {
appointmentSegment.style.color = "#ffffff";
}
appointmentSegment.style.width = `${currentWithOfGridCell * segment.columns}px`;
appointmentSegment.style.height = maxLineHeight + "px";
appointmentSegment.style.top = `${segment.line * maxLineHeight + maxLineHeight + 10}px`;
appointmentSegment.setOption("labels.text", segment.label);
container.appendChild(appointmentSegment);
}
}
/**
* Generates two arrays: one for the calendar grid (42 days) and one for the weekday headers (7 days).
* The grid always starts on the Monday of the week that contains the 1st of the given month.
*
* @returns {Object} An object containing:
* - calendarDays: Array of 42 objects, each representing a day.
* - calendarWeekdays: Array of 7 objects, each representing a weekday header.
*/
function generateCalendarData() {
let selectedDate = this.getOption("startDate");
if (!(selectedDate instanceof Date)) {
if (typeof selectedDate === "string") {
try {
selectedDate = new Date(selectedDate);
} catch (e) {
addErrorAttribute(this, "Invalid calendar date");
return { calendarDays, calendarWeekdays };
}
} else {
addErrorAttribute(this, "Invalid calendar date");
return { calendarDays, calendarWeekdays };
}
}
const calendarDays = [];
let calendarWeekdays = [];
if (!(selectedDate instanceof Date)) {
addErrorAttribute(this, "Invalid calendar date");
return { calendarDays, calendarWeekdays };
}
// Get the year and month from the provided date
const year = selectedDate.getFullYear();
const month = selectedDate.getMonth(); // 0-based index (0 = January)
// Create a Date object for the 1st of the given month
const firstDayOfMonth = new Date(year, month, 1);
// Determine the weekday index of the 1st day, ensuring Monday = 0
const weekdayIndex = (firstDayOfMonth.getDay() + 6) % 7;
// Calculate the start date: move backward to the Monday of the starting week
const startDate = new Date(firstDayOfMonth);
startDate.setDate(firstDayOfMonth.getDate() - weekdayIndex);
// Generate 42 days (6 weeks × 7 days)
for (let i = 0; i < 42; i++) {
const current = new Date(startDate);
current.setDate(startDate.getDate() + i);
const label = current.getDate().toString();
calendarDays.push({
date: current,
//day: current.getDate(),
month: current.getMonth() + 1, // 1-based month (1-12)
year: current.getFullYear(),
isCurrentMonth: current.getMonth() === month,
label: label,
index: i,
day:
current.getFullYear() +
"-" +
("00" + (current.getMonth() + 1)).slice(-2) +
"-" +
("00" + current.getDate()).slice(-2),
classes:
"day-cell " +
(current.getMonth() === month ? "current-month" : "other-month") +
(current.getDay() === 0 || current.getDay() === 6 ? " weekend" : "") +
(current.toDateString() === new Date().toDateString() ? " today" : ""),
});
}
// Generate weekday header array (Monday through Sunday)
let format = this.getOption("locale.weekdayFormat");
if (!["long", "short", "narrow"].includes(format)) {
addErrorAttribute(this, "Invalid weekday format option " + format);
format = "short";
}
const weekdayNames = getWeekdays(format);
calendarWeekdays = weekdayNames.map((name, index) => {
return {
label: name,
index: index,
};
});
return { calendarDays, calendarWeekdays };
}
/**
* @private
* @return {initEventHandler}
* @fires monster-calendar-clicked
*/
function initEventHandler() {
const self = this;
this.attachObserver(
new Observer(() => {
placeAppointments.call(this);
}),
);
return this;
}
function getTextHeight(text) {
// Ein unsichtbares div erstellen
const div = document.createElement("div");
div.style.position = "absolute";
div.style.whiteSpace = "nowrap";
div.style.visibility = "hidden";
div.textContent = text;
this.shadowRoot.appendChild(div);
const height = div.clientHeight;
this.shadowRoot.removeChild(div);
return height;
}
/**
* @private
* @return {void}
*/
function initControlReferences() {
this[calendarElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="control"]`,
);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<template id="cell">
<div data-monster-attributes="class path:cell.classes,
data-monster-index path:cell.index">
<div data-monster-replace="path:cell.label"></div>
<div data-monster-role="appointment-container"
data-monster-attributes="data-monster-day path:cell.day,
data-monster-calendar-index path:cell.index"></div>
</div>
</template>
<template id="calendar-weekday-header">
<div data-monster-attributes="class path:calendar-weekday-header.classes,
data-monster-index path:calendar-weekday-header.index"
data-monster-replace="path:calendar-weekday-header.label"></div>
</template>
<div data-monster-role="control" part="control">
<div class="weekday-header">
<div data-monster-role="weekdays"
data-monster-insert="calendar-weekday-header path:calendarWeekdays"></div>
<div class="calendar-body">
<div data-monster-role="appointments">
<slot></slot>
</div>
<div data-monster-role="cells" data-monster-insert="cell path:calendarDays"></div>
</div>
</div>
`;
}
registerCustomElement(MonthCalendar);
@import "../../style/control.pcss";
@import "../../style/accessibility.pcss";
@import "../../style/button.pcss";
@import "../../style/border.pcss";
@import "../../style/typography.pcss";
@import "../../style/theme.pcss";
@import "../../style/color.pcss";
:host {
background-color: red;
color: white;
position: absolute;
left : 0;
height: 1rem;
width: 10rem;
bottom: 0;
/*z-index: var(--monster-z-index-sticky);*/
}
\ No newline at end of file
@import "../../style/control.pcss";
@import "../../style/accessibility.pcss";
@import "../../style/button.pcss";
@import "../../style/border.pcss";
@import "../../style/typography.pcss";
@import "../../style/theme.pcss";
@import "../../style/color.pcss";
[data-monster-role="control"] {
overflow: hidden;
}
[data-monster-role="weekdays"] {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: 1fr;
}
[data-monster-role="weekdays"] > div {
display: flex;
align-items: center;
justify-content: start;
padding: 0.3em;
position: relative;
cursor: pointer;
transition: background-color 0.3s;
background-color: var(--monster-bg-color-primary-1);
color: var(--monster-color-primary-1);
}
[data-monster-role="cells"] {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(6, 1fr);
gap: 1px;
background-color: var(--monster-theme-control-border-color);
border-radius: var(--monster-theme-control-border-radius);
border-width: var(--monster-theme-control-border-width);
border-color: var(--monster-theme-control-border-color);
border-style: var(--monster-theme-control-border-style);
}
div.day-cell {
display: flex;
align-items: start;
justify-content: start;
box-sizing: border-box;
padding: 0.3em;
position: relative;
cursor: pointer;
transition: background-color 0.3s;
background-color: var(--monster-bg-color-primary-2);
color: var(--monster-color-primary-2);
aspect-ratio: 1 / 1;
}
div.current-month {
background-color: var(--monster-bg-color-primary-1);
color : var(--monster-color-primary-1);
}
div.weekend {
background-color: var(--monster-bg-color-tertiary-2);
color : var(--monster-color-tertiary-2);
}
div.today {
background-color: var(--monster-bg-color-primary-4);
color : var(--monster-color-primary-4);
}
[data-monster-role=appointment-container] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
\ No newline at end of file
This diff is collapsed.
/**
* 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 + "");
}
This diff is collapsed.
......@@ -245,7 +245,7 @@ function fetchData(init, key, callback) {
if (body.length > 100) {
body = `${body.substring(0, 97)}...`;
}
throw new DataFetchError(
getInternalLocalizationMessage(
`i18n{the-response-does-not-contain-a-valid-json::actual=${body}}`,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment