@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,029 lines (876 loc) • 29.4 kB
JavaScript
/**
* 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";
import { positionPopper } from "../form/util/floating-ui.mjs";
import { Segment as AppointmentSegment } from "./timeline/segment.mjs";
import { Appointment as AppointmentAppointment } from "./timeline/appointment.mjs";
export { MonthCalendar };
/**
* @private
* @type {symbol}
*/
const calendarElementSymbol = Symbol("calendarElement");
/**
* @private
* @type {symbol}
*/
const calendarBodyElementSymbol = Symbol("calendarBodyElement");
/**
* A Calendar
*
* @fragments /fragments/components/time/calendar/
*
* @example /examples/components/time/calendar-simple
*
* @since 3.112.0
* @copyright schukai GmbH
* @summary A beautiful month Calendar that can display appointments. It is possible to use a datasource to load the appointments.
*/
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]();
setTimeout(() => {
initControlReferences.call(this);
initEventHandler.call(this);
// refresh the labels for updater
this.setOption("labels.__update", true);
initDataSource.call(this);
}, 0);
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 Labels for the control
* @property {Object} classes Classes for the control
* @property {boolean} disabled Disables the control
* @property {Object} features Features for the control
* @property {boolean} features.showWeekend Show the weekend
* @property {boolean} features.monthOneLine Show the month in one line
* @property {Object} actions Actions for the control
* @property {Object} locale Locale for the control
* @property {string} locale.weekdayFormat Weekday format
* @property {string} startDate Start date
* @property {string} endDate End date
* @property {Object} datasource Datasource
* @property {string} datasource.selector Datasource selector
*/
get defaults() {
const startDate = new Date();
const startDateString =
startDate.getFullYear() +
"-" +
("00" + (startDate.getMonth() + 1)).slice(-2) +
"-" +
("00" + startDate.getDate()).slice(-2);
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: getTranslations(),
classes: {},
disabled: false,
features: {
showWeekend: true,
monthOneLine: false,
},
actions: {},
locale: {
weekdayFormat: "short",
},
startDate: startDateString,
endDate: "",
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];
}
}
/**
* @private
* @returns {object}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return {
more: "weitere Termin",
};
default:
return {
more: "more appointments",
};
}
}
/**
* 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);
}
/**
* Places and organizes appointments within a calendar grid according to their start and end dates,
* ensuring proper alignment and rendering within the calendar day containers.
*
* Appointments are evaluated to determine their appropriate positions, lengths, and styles based on calendar
* configuration and available rendering space.
*
* Errors are logged for invalid appointment data or when
* rendering issues are encountered.
*
* @private
* @return {void} No return value; the method operates on the DOM and internal state of the calendar element to render appointments visually.
*/
function placeAppointments() {
const self = this;
const currentWithOfGridCell =
this[calendarElementSymbol].getBoundingClientRect().width / 7;
const appointments = this.getOption("data");
const segments = [];
let maxLineHeight = 0;
const calendarDays = this.getOption("calendarDays");
const calenderStartDate = new Date(calendarDays[0].date);
const calenderEndDate = new Date(calendarDays[calendarDays.length - 1].date);
const app = getAppointmentsPerDay(
appointments,
calenderStartDate,
calenderEndDate,
);
calendarDays.forEach((day) => {
const k =
day.date.getFullYear() +
"-" +
("00" + (day.date.getMonth() + 1)).slice(-2) +
"-" +
("00" + day.date.getDate()).slice(-2);
day.appointments = app[k];
});
appointments.forEach((appointment) => {
if (!appointment?.startDate || !appointment?.endDate) {
addErrorAttribute(this, "Missing start or end date in appointment");
return;
}
const startDate = appointment?.startDate;
// calc length of appointment
const start = new Date(startDate);
const end = new Date(appointment?.endDate);
const appointmentRows = getAppointmentRowsUsingCalendar(
start,
end,
calendarDays[0].date,
);
let date = appointment.startDate;
const s =
start.getFullYear() +
"-" +
("00" + (start.getMonth() + 1)).slice(-2) +
"-" +
("00" + start.getDate()).slice(-2);
const e =
end.getFullYear() +
"-" +
("00" + (end.getMonth() + 1)).slice(-2) +
"-" +
("00" + end.getDate()).slice(-2);
let label;
if (appointment.label) {
label = appointment.label.replace(/\n/g, "<br>");
} else {
label =
s !== e
? `${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 k = new Date(start.setDate(start.getDate() + cols));
// calc from k to next day
date =
k.getFullYear() +
"-" +
("00" + (k.getMonth() + 1)).slice(-2) +
"-" +
("00" + k.getDate()).slice(-2);
}
});
let container = null;
const sortedSegments = assignLinesToSegments(segments);
const h = calcHeaderAndFooterHeight.call(this);
const marginAppointmentContainer = h.maxFooterHeight + h.maxHeaderHeight;
const popper = self.shadowRoot.querySelectorAll(
"monster-appointment-segment",
);
if (popper) {
for (const p of popper) {
p.remove();
}
}
const daysWithMoreAppointments = new Set();
const moreAppointments = new Map();
for (let i = 0; i < sortedSegments.length; i++) {
const segment = sortedSegments[i];
container = self.shadowRoot.querySelector(
`[data-monster-day="${segment.start}"]`,
);
if (!container) {
continue;
}
let containerHeight = container?.getBoundingClientRect()?.height || 0;
if (containerHeight === 0) {
addErrorAttribute(this, "Unable to retrieve container height");
continue;
}
let availableHeight =
containerHeight - maxLineHeight - marginAppointmentContainer;
let linesThatCanBeShown = Math.floor(availableHeight / maxLineHeight);
if (segment.line > linesThatCanBeShown) {
daysWithMoreAppointments.add(segment.start);
const calendarDays = this.getOption("calendarDays");
const currentDay = calendarDays.find((day) => day.day === segment.start);
if (currentDay) {
if (!moreAppointments.has(currentDay.index)) {
moreAppointments.set(currentDay.index, []);
}
moreAppointments.get(currentDay.index).push(segment);
}
continue;
}
if (!container) {
addErrorAttribute(
this,
"Invalid, missing or out of range date in appointment" + segment.start,
);
continue;
}
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 + h.maxHeaderHeight}px`;
appointmentSegment.setOption("labels.text", segment.label);
container.appendChild(appointmentSegment);
}
// mark days with more appointments, show a hint / link
for (const day of daysWithMoreAppointments) {
for (const cell of this.getOption("calendarDays")) {
if (cell.day === day) {
this.setOption(
"calendarDays." + cell.index + ".classes.more-appointments",
"visible",
);
}
}
}
// get popper
for (const [index, appointments] of moreAppointments) {
const moreAppointmentsContainer = this.shadowRoot.querySelector(
`[data-monster-role=day-cell][data-monster-index="${index}"] > [data-monster-role="appointment-popper"] `,
);
if (!moreAppointmentsContainer) {
continue;
}
moreAppointmentsContainer.innerHTML = "";
for (const appointment of appointments) {
const appointmentSegment = document.createElement("monster-appointment");
appointmentSegment.style.backgroundColor = appointment.appointment.color;
appointmentSegment.style.position = "relative";
appointmentSegment.style.top = "unset";
// 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 = "100%";
appointmentSegment.style.height = maxLineHeight + "px";
appointmentSegment.setOption("labels.text", appointment.label);
moreAppointmentsContainer.appendChild(appointmentSegment);
}
}
}
/**
* @private
* @returns {{maxHeaderHeight: number, maxFooterHeight: number}}
*/
function calcHeaderAndFooterHeight() {
let maxHeaderHeight = 0;
let maxFooterHeight = 0;
const days = this.getOption("calendarDays");
for (const day of days) {
const current = day.date;
const dayKey =
current.getFullYear() +
"-" +
("00" + (current.getMonth() + 1)).slice(-2) +
"-" +
("00" + current.getDate()).slice(-2);
const cell = this.shadowRoot.querySelector(
`[data-monster-day="${dayKey}"]`,
);
if (!(cell instanceof HTMLDivElement)) {
continue;
}
const header = cell.querySelector("[data-monster-role='day-header']");
if (header instanceof HTMLDivElement) {
maxHeaderHeight = Math.max(
maxHeaderHeight,
header.getBoundingClientRect().height +
parseFloat(getComputedStyle(header).paddingTop) +
parseFloat(getComputedStyle(header).paddingBottom) +
parseFloat(getComputedStyle(header).marginTop) +
parseFloat(getComputedStyle(header).marginBottom) +
parseFloat(getComputedStyle(header.parentElement).paddingTop) +
parseFloat(getComputedStyle(header.parentElement).paddingBottom) +
parseFloat(getComputedStyle(header.parentElement).marginTop),
);
}
const footer = cell.querySelector("[data-monster-role='day-footer']");
if (footer instanceof HTMLDivElement) {
maxFooterHeight = Math.max(
maxFooterHeight,
footer.getBoundingClientRect().height +
parseFloat(getComputedStyle(footer).paddingTop) +
parseFloat(getComputedStyle(footer).paddingBottom) +
parseFloat(getComputedStyle(footer).marginTop) +
parseFloat(getComputedStyle(footer).marginBottom) +
parseFloat(getComputedStyle(footer.parentElement).paddingTop) +
parseFloat(getComputedStyle(footer.parentElement).paddingBottom) +
parseFloat(getComputedStyle(footer.parentElement).marginTop),
);
}
}
return { maxHeaderHeight, maxFooterHeight };
}
/**
* 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 first of the given month.
*
* @returns {Object} An object containing:
* - calendarDays: Array of 42 objects, each representing a day.
* - calendarWeekdays: Array of seven 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();
const dayKey =
current.getFullYear() +
"-" +
("00" + (current.getMonth() + 1)).slice(-2) +
"-" +
("00" + current.getDate()).slice(-2);
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: dayKey,
classes: {
"more-appointments": "hidden",
cell:
"day-cell " +
(current.getMonth() === month ? "current-month" : "other-month") +
(current.getDay() === 0 || current.getDay() === 6 ? " weekend" : "") +
(current.toDateString() === new Date().toDateString()
? " today"
: ""),
},
appointments: [],
});
}
// 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 };
}
/**
* Generates a map that contains an array of appointments for each day within the calendar range.
* Multi-day appointments will appear on each day they span.
*
* @param {Array} appointments - Array of appointment objects. Expected properties: "startDate" and "endDate".
* @param {Date|string} start - Calendar start date.
* @param {Date|string} end - Calendar end date.
* @returns {Object} A map in the format { "YYYY-MM-DD": [appointment1, appointment2, ...] }
*/
function getAppointmentsPerDay(appointments, start, end) {
const appointmentsMap = {};
// Convert start and end to Date objects if needed
const startDate = start instanceof Date ? start : new Date(start);
const endDate = end instanceof Date ? end : new Date(end);
// Create an empty entry for each day in the calendar range
let current = new Date(startDate);
while (current <= endDate) {
const key = current.toISOString().slice(0, 10);
appointmentsMap[key] = [];
current.setDate(current.getDate() + 1);
}
// Assign each appointment to the corresponding days
appointments.forEach((appointment) => {
if (!appointment.startDate || !appointment.endDate) {
// Skip appointments with missing data
return;
}
const appStart = new Date(appointment.startDate);
const appEnd = new Date(appointment.endDate);
// Determine the effective start and end dates to ensure appointments outside the calendar are ignored
const effectiveStart = appStart < startDate ? startDate : appStart;
const effectiveEnd = appEnd > endDate ? endDate : appEnd;
let currentAppDate = new Date(effectiveStart);
while (currentAppDate <= effectiveEnd) {
const key = currentAppDate.toISOString().slice(0, 10);
if (appointmentsMap[key]) {
appointmentsMap[key].push(appointment);
}
currentAppDate.setDate(currentAppDate.getDate() + 1);
}
});
return appointmentsMap;
}
/**
* @private
* @return {initEventHandler}
*/
function initEventHandler() {
const self = this;
setTimeout(() => {
this.attachObserver(
new Observer(() => {
placeAppointments.call(this);
}),
);
this[calendarElementSymbol]
.querySelectorAll("[data-monster-role='day-cell']")
.forEach((element) => {
element.addEventListener("click", (event) => {
const hoveredElement = this.shadowRoot.elementFromPoint(
event.clientX,
event.clientY,
);
if (hoveredElement instanceof AppointmentSegment) {
return;
}
const element = findTargetElementFromEvent(
event,
"data-monster-role",
"more-appointments",
);
if (!element) {
return;
}
// parent is footer, and parent of footer is cell
const popper = element.parentElement.parentElement.querySelector(
'[data-monster-role="appointment-popper"]',
);
if (!popper) {
return;
}
//const appointments = getAppointmentsPerDay() || [];
const container = element.parentElement.parentElement;
positionPopper(container, popper, {
placement: "bottom",
});
popper.style.width = container.getBoundingClientRect().width + "px";
popper.style.zIndex = 1000;
popper.style.display = "block";
});
element.addEventListener("mouseleave", (event) => {
const element = findTargetElementFromEvent(
event,
"data-monster-role",
"day-cell",
);
if (!element) {
return;
}
element.classList.remove("hover");
const popper = element.querySelector(
'[data-monster-role="appointment-popper"]',
);
if (!popper) {
return;
}
setTimeout(() => {
popper.style.display = "none";
}, 0);
});
});
}, 0);
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"]`,
);
this[calendarBodyElementSymbol] = this.shadowRoot.querySelector(
`[${ATTRIBUTE_ROLE}="calendar-body"]`,
);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<template id="cell">
<div data-monster-role="day-cell"
data-monster-attributes="class path:cell.classes.cell,
data-monster-day path:cell.day,
data-monster-index path:cell.index">
<div class="header" data-monster-role="day-header">
<div data-monster-replace="path:cell.label"></div>
</div>
<div class="footer" data-monster-role="day-footer">
<div data-monster-replace="path:labels.more"
data-monster-attributes="class path:cell.classes.more-appointments"
data-monster-role="more-appointments"></div>
</div>
<div data-monster-role="appointment-popper"
class="popper"></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" data-monster-role="calendar-body">
<div data-monster-role="cells" data-monster-insert="cell path:calendarDays"></div>
</div>
</div>
`;
}
registerCustomElement(MonthCalendar);