@schukai/monster 3.111.0 → 3.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/source/components/content/camera.mjs +339 -329
  4. package/source/components/content/stylesheet/camera-capture.mjs +13 -6
  5. package/source/components/datatable/status.mjs +175 -177
  6. package/source/components/form/reload.mjs +1 -2
  7. package/source/components/form/util/fetch.mjs +5 -2
  8. package/source/components/layout/popper.mjs +1 -1
  9. package/source/components/layout/slider.mjs +1 -1
  10. package/source/components/time/month-calendar.mjs +819 -0
  11. package/source/components/time/style/month-calendar.pcss +100 -0
  12. package/source/components/time/stylesheet/month-calendar.mjs +31 -0
  13. package/source/components/time/timeline/collection.mjs +205 -0
  14. package/source/components/time/timeline/item.mjs +184 -0
  15. package/source/components/time/timeline/segment.mjs +169 -0
  16. package/source/components/time/timeline/style/segment.pcss +18 -0
  17. package/source/components/time/timeline/stylesheet/segment.mjs +38 -0
  18. package/source/data/datasource/server/restapi/data-fetch-error.mjs +3 -3
  19. package/source/data/datasource/server/restapi.mjs +1 -1
  20. package/source/data/transformer.mjs +60 -0
  21. package/source/monster.mjs +4 -0
  22. package/source/text/bracketed-key-value-hash.mjs +187 -187
  23. package/source/types/base.mjs +6 -5
  24. package/source/types/basewithoptions.mjs +4 -1
  25. package/source/types/internal.mjs +1 -1
  26. package/source/types/version.mjs +1 -1
  27. package/test/cases/monster.mjs +1 -1
  28. package/test/web/test.html +2 -2
  29. package/test/web/tests.js +1135 -976
@@ -0,0 +1,819 @@
1
+ /**
2
+ * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact schukai GmbH.
11
+ */
12
+
13
+ import { instanceSymbol } from "../../constants.mjs";
14
+ import { addAttributeToken } from "../../dom/attributes.mjs";
15
+ import {
16
+ ATTRIBUTE_ERRORMESSAGE,
17
+ ATTRIBUTE_ROLE,
18
+ } from "../../dom/constants.mjs";
19
+ import { CustomControl } from "../../dom/customcontrol.mjs";
20
+ import {
21
+ CustomElement,
22
+ getSlottedElements,
23
+ initMethodSymbol,
24
+ } from "../../dom/customelement.mjs";
25
+ import {
26
+ assembleMethodSymbol,
27
+ registerCustomElement,
28
+ } from "../../dom/customelement.mjs";
29
+ import { findTargetElementFromEvent } from "../../dom/events.mjs";
30
+ import { isFunction, isString } from "../../types/is.mjs";
31
+
32
+ import { fireCustomEvent } from "../../dom/events.mjs";
33
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
34
+ import { addErrorAttribute } from "../../dom/error.mjs";
35
+ import { MonthCalendarStyleSheet } from "./stylesheet/month-calendar.mjs";
36
+ import {
37
+ datasourceLinkedElementSymbol,
38
+ handleDataSourceChanges,
39
+ } from "../datatable/util.mjs";
40
+ import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
41
+ import { Datasource } from "../datatable/datasource.mjs";
42
+ import { Observer } from "../../types/observer.mjs";
43
+ import { positionPopper } from "../form/util/floating-ui.mjs";
44
+ import { Segment as AppointmentSegment } from "./timeline/segment.mjs";
45
+
46
+ export { MonthCalendar };
47
+
48
+ /**
49
+ * @private
50
+ * @type {symbol}
51
+ */
52
+ const calendarElementSymbol = Symbol("calendarElement");
53
+
54
+ /**
55
+ * @private
56
+ * @type {symbol}
57
+ */
58
+ const calendarBodyElementSymbol = Symbol("calenddarBodyElement");
59
+
60
+ /**
61
+ * A Calendar
62
+ *
63
+ * @fragments /fragments/components/time/calendar/
64
+ *
65
+ * @example /examples/components/time/calendar-simple
66
+ *
67
+ * @since 3.112.0
68
+ * @copyright schukai GmbH
69
+ * @summary A beautiful month Calendar that can display appointments. It is possible to use a datasource to load the appointments.
70
+ */
71
+ class MonthCalendar extends CustomElement {
72
+ /**
73
+ * This method is called by the `instanceof` operator.
74
+ * @returns {symbol}
75
+ */
76
+ static get [instanceSymbol]() {
77
+ return Symbol.for("@schukai/monster/components/time/calendar@@instance");
78
+ }
79
+
80
+ [initMethodSymbol]() {
81
+ super[initMethodSymbol]();
82
+
83
+ const def = generateCalendarData.call(this);
84
+ this.setOption("calendarDays", def.calendarDays);
85
+ this.setOption("calendarWeekdays", def.calendarWeekdays);
86
+ }
87
+
88
+ /**
89
+ *
90
+ * @return {Components.Time.Calendar
91
+ */
92
+ [assembleMethodSymbol]() {
93
+ super[assembleMethodSymbol]();
94
+ initControlReferences.call(this);
95
+ initDataSource.call(this);
96
+ initEventHandler.call(this);
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
102
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
103
+ *
104
+ * The individual configuration values can be found in the table.
105
+ *
106
+ * @property {Object} templates Template definitions
107
+ * @property {string} templates.main Main template
108
+ * @property {Object} labels Label definitions
109
+ * @property {Object} actions Callbacks
110
+ * @property {string} actions.click="throw Error" Callback when clicked
111
+ * @property {Object} features Features
112
+ * @property {Object} classes CSS classes
113
+ * @property {boolean} disabled=false Disabled state
114
+ */
115
+ get defaults() {
116
+
117
+ const startDate = new Date();
118
+ const startDateString = startDate.getFullYear() + "-" + ("00" + (startDate.getMonth() + 1)).slice(-2) + "-" + ("00" + startDate.getDate()).slice(-2);
119
+
120
+ return Object.assign({}, super.defaults, {
121
+ templates: {
122
+ main: getTemplate(),
123
+ },
124
+ labels: {},
125
+ classes: {},
126
+
127
+ disabled: false,
128
+ features: {
129
+ showWeekend: true,
130
+ monthOneLine: false,
131
+ },
132
+ actions: {},
133
+
134
+ locale: {
135
+ weekdayFormat: "short",
136
+ },
137
+
138
+ startDate: startDateString,
139
+ endDate: "",
140
+ calendarDays: [],
141
+ calendarWeekdays: [],
142
+
143
+ data: [],
144
+
145
+ datasource: {
146
+ selector: null,
147
+ },
148
+ });
149
+ }
150
+
151
+ /**
152
+ * This method is called when the component is created.
153
+ * @return {Promise}
154
+ */
155
+ refresh() {
156
+ // makes sure that handleDataSourceChanges is called
157
+ return new Promise((resolve) => {
158
+ this.setOption("data", {});
159
+ queueMicrotask(() => {
160
+ handleDataSourceChanges.call(this);
161
+ placeAppointments();
162
+ resolve();
163
+ });
164
+ });
165
+ }
166
+
167
+ /**
168
+ * @return {string}
169
+ */
170
+ static getTag() {
171
+ return "monster-month-calendar";
172
+ }
173
+
174
+ /**
175
+ * @return {CSSStyleSheet[]}
176
+ */
177
+ static getCSSStyleSheet() {
178
+ return [MonthCalendarStyleSheet];
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Calculates how many days of an appointment are distributed across calendar rows (weeks).
184
+ * Uses the start date of the calendar grid (e.g., from generateCalendarData()) as a reference.
185
+ *
186
+ * @param {Date} appointmentStart - Start date of the appointment.
187
+ * @param {Date} appointmentEnd - End date of the appointment (inclusive).
188
+ * @param {Date} calendarGridStart - The first day of the calendar grid (e.g., the Monday from generateCalendarData()).
189
+ * @returns {number[]} Array indicating how many days the appointment spans per row.
190
+ *
191
+ * Example:
192
+ * - Appointment: 01.03.2025 (Saturday) to 01.03.2025:
193
+ * -> getAppointmentRowsUsingCalendar(new Date("2025-03-01"), new Date("2025-03-01"), calendarGridStart)
194
+ * returns: [1] (since it occupies only one day in the first row, starting at column 6).
195
+ *
196
+ * - Appointment: 01.03.2025 to 03.03.2025:
197
+ * -> returns: [2, 1] (first row: Saturday and Sunday, second row: Monday).
198
+ */
199
+ function getAppointmentRowsUsingCalendar(
200
+ appointmentStart,
201
+ appointmentEnd,
202
+ calendarGridStart,
203
+ ) {
204
+ const oneDayMs = 24 * 60 * 60 * 1000;
205
+ // Calculate the offset (in days) from the calendar start to the appointment start
206
+ const offset = Math.floor((appointmentStart - calendarGridStart) / oneDayMs);
207
+ // Determine the column index in the calendar row (Monday = 0, ..., Sunday = 6)
208
+ let startColumn = offset % 7;
209
+ if (startColumn < 0) {
210
+ startColumn += 7;
211
+ }
212
+ // Calculate the total number of days for the appointment (including start and end date)
213
+ const totalDays =
214
+ Math.floor((appointmentEnd - appointmentStart) / oneDayMs) + 1;
215
+
216
+ // The first calendar block can accommodate at most (7 - startColumn) days.
217
+ const firstRowDays = Math.min(totalDays, 7 - startColumn);
218
+ const rows = [firstRowDays];
219
+
220
+ let remainingDays = totalDays - firstRowDays;
221
+ // Handle full weeks (7 days per row)
222
+ while (remainingDays > 7) {
223
+ rows.push(7);
224
+ remainingDays -= 7;
225
+ }
226
+ // Handle the last row if there are any remaining days
227
+ if (remainingDays > 0) {
228
+ rows.push(remainingDays);
229
+ }
230
+
231
+ return rows;
232
+ }
233
+
234
+ /**
235
+ * @private
236
+ * @param format
237
+ * @returns {*[]}
238
+ */
239
+ function getWeekdays(format = "long") {
240
+ const locale = getLocaleOfDocument();
241
+
242
+ const weekdays = [];
243
+ for (let i = 1; i < 8; i++) {
244
+ const date = new Date(1970, 0, 4 + i); // 4. Jan. 1970 = Sonntag
245
+ weekdays.push(
246
+ new Intl.DateTimeFormat(locale, { weekday: format }).format(date),
247
+ );
248
+ }
249
+ return weekdays;
250
+ }
251
+
252
+ /**
253
+ * Assigns a "line" property to the provided segments (with "startIndex" and "columns").
254
+ * It checks for horizontal overlaps within each calendar row (7 boxes).
255
+ * Always assigns the lowest available "line".
256
+ *
257
+ * @private
258
+ *
259
+ * @param {Array} segments - Array of segments, e.g.
260
+ * [
261
+ * {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-11","startIndex":15},
262
+ * {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-17","startIndex":21},
263
+ * {"columns":7,"label":"03/11/2025 - 04/05/2025","start":"2025-03-24","startIndex":28},
264
+ * {"columns":6,"label":"03/11/2025 - 04/05/2025","start":"2025-03-31","startIndex":35}
265
+ * ]
266
+ * @returns {Array} The segments with assigned "line" property
267
+ */
268
+ function assignLinesToSegments(segments) {
269
+ const groups = {};
270
+ segments.forEach((segment) => {
271
+ const week = Math.floor(segment.startIndex / 7);
272
+ if (!groups[week]) {
273
+ groups[week] = [];
274
+ }
275
+ groups[week].push(segment);
276
+ });
277
+
278
+ Object.keys(groups).forEach((weekKey) => {
279
+ const weekSegments = groups[weekKey];
280
+
281
+ weekSegments.sort((a, b) => a.startIndex - b.startIndex);
282
+
283
+ const lineEnds = [];
284
+
285
+ weekSegments.forEach((segment) => {
286
+ const segStart = segment.startIndex;
287
+ const segEnd = segment.startIndex + segment.columns - 1;
288
+ let placed = false;
289
+
290
+ for (let line = 0; line < lineEnds.length; line++) {
291
+ if (segStart >= lineEnds[line] + 1) {
292
+ segment.line = line;
293
+ lineEnds[line] = segEnd;
294
+ placed = true;
295
+ break;
296
+ }
297
+ }
298
+
299
+ if (!placed) {
300
+ segment.line = lineEnds.length;
301
+ lineEnds.push(segEnd);
302
+ }
303
+ });
304
+ });
305
+
306
+ return segments;
307
+ }
308
+
309
+ /**
310
+ * @private
311
+ */
312
+ function initDataSource() {
313
+ setTimeout(() => {
314
+ if (!this[datasourceLinkedElementSymbol]) {
315
+ const selector = this.getOption("datasource.selector");
316
+
317
+ if (isString(selector)) {
318
+ const element = findElementWithSelectorUpwards(this, selector);
319
+ if (element === null) {
320
+ addErrorAttribute(
321
+ this,
322
+ "the selector must match exactly one element",
323
+ );
324
+ return;
325
+ }
326
+
327
+ if (!(element instanceof Datasource)) {
328
+ addErrorAttribute(this, "the element must be a datasource");
329
+ return;
330
+ }
331
+
332
+ this[datasourceLinkedElementSymbol] = element;
333
+ element.datasource.attachObserver(
334
+ new Observer(handleDataSourceChanges.bind(this)),
335
+ );
336
+
337
+ handleDataSourceChanges.call(this);
338
+ placeAppointments.call(this);
339
+ } else {
340
+ addErrorAttribute(
341
+ this,
342
+ "the datasource selector is missing or invalid",
343
+ );
344
+ }
345
+ }
346
+ }, 10);
347
+ }
348
+
349
+ function placeAppointments() {
350
+ const self = this;
351
+
352
+ const currentWithOfGridCell =
353
+ this[calendarElementSymbol].getBoundingClientRect().width / 7;
354
+ const appointments = this.getOption("data");
355
+
356
+ const segments = [];
357
+ let maxLineHeight = 0;
358
+
359
+ const calendarDays = this.getOption("calendarDays");
360
+
361
+ const calenderStartDate = new Date(calendarDays[0].date);
362
+ const calenderEndDate = new Date(calendarDays[calendarDays.length - 1].date);
363
+
364
+ const app = getAppointmentsPerDay(
365
+ appointments,
366
+ calenderStartDate,
367
+ calenderEndDate,
368
+ );
369
+ calendarDays.forEach((day) => {
370
+ const k =
371
+ day.date.getFullYear() +
372
+ "-" +
373
+ ("00" + (day.date.getMonth() + 1)).slice(-2) +
374
+ "-" +
375
+ ("00" + day.date.getDate()).slice(-2);
376
+ day.appointments = app[k];
377
+ });
378
+
379
+ appointments.forEach((appointment) => {
380
+ if (!appointment?.startDate || !appointment?.endDate) {
381
+ addErrorAttribute(this, "Missing start or end date in appointment");
382
+ return;
383
+ }
384
+
385
+ const startDate = appointment?.startDate;
386
+ let container = self.shadowRoot.querySelector(
387
+ `[data-monster-day="${startDate}"]`,
388
+ );
389
+
390
+ if (!container) {
391
+ addErrorAttribute(
392
+ this,
393
+ "Invalid, missing or out of range date in appointment" + startDate,
394
+ );
395
+ return;
396
+ }
397
+
398
+ // calc length of appointment
399
+ const start = new Date(startDate);
400
+ const end = new Date(appointment?.endDate);
401
+
402
+ const appointmentRows = getAppointmentRowsUsingCalendar(
403
+ start,
404
+ end,
405
+ calendarDays[0].date,
406
+ );
407
+
408
+ let date = appointment.startDate;
409
+
410
+ const s =
411
+ start.getFullYear() +
412
+ "-" +
413
+ ("00" + (start.getMonth() + 1)).slice(-2) +
414
+ "-" +
415
+ ("00" + start.getDate()).slice(-2);
416
+
417
+ const e =
418
+ end.getFullYear() +
419
+ "-" +
420
+ ("00" + (end.getMonth() + 1)).slice(-2) +
421
+ "-" +
422
+ ("00" + end.getDate()).slice(-2);
423
+
424
+ let label;
425
+ if (appointment.label) {
426
+ label = appointment.label.replace(/\n/g, "<br>");
427
+ } else {
428
+ label =
429
+ s !== e
430
+ ? `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`
431
+ : start.toLocaleDateString();
432
+ }
433
+
434
+ for (let i = 0; i < appointmentRows.length; i++) {
435
+ const cols = appointmentRows[i];
436
+
437
+ const calendarStartDate = new Date(calendarDays[0].date); // First day of the calendar grid
438
+ const appointmentDate = new Date(date);
439
+ const startIndex = Math.floor(
440
+ (appointmentDate - calendarStartDate) / (24 * 60 * 60 * 1000),
441
+ );
442
+
443
+ segments.push({
444
+ columns: cols,
445
+ label: label,
446
+ start: date,
447
+ startIndex: startIndex,
448
+ appointment: appointment,
449
+ });
450
+
451
+ maxLineHeight = Math.max(maxLineHeight, getTextHeight.call(this, label));
452
+
453
+ const nextKeyDate = new Date(start.setDate(start.getDate() + cols));
454
+ date =
455
+ nextKeyDate.getFullYear() +
456
+ "-" +
457
+ ("00" + (nextKeyDate.getMonth() + 1)).slice(-2) +
458
+ "-" +
459
+ ("00" + nextKeyDate.getDate()).slice(-2);
460
+ }
461
+ });
462
+
463
+ let container = null;
464
+
465
+ const sortedSegments = assignLinesToSegments(segments);
466
+
467
+ for (let i = 0; i < sortedSegments.length; i++) {
468
+ const segment = sortedSegments[i];
469
+
470
+ if (segment.line > 3) {
471
+ continue;
472
+ } else {
473
+ }
474
+
475
+ container = self.shadowRoot.querySelector(
476
+ `[data-monster-day="${segment.start}"]`,
477
+ );
478
+
479
+ if (!container) {
480
+ addErrorAttribute(
481
+ this,
482
+ "Invalid, missing or out of range date in appointment" + segment.start,
483
+ );
484
+ continue;
485
+ }
486
+
487
+ const appointmentSegment = document.createElement(
488
+ "monster-appointment-segment",
489
+ );
490
+ appointmentSegment.className = "appointment-segment";
491
+ appointmentSegment.style.backgroundColor = segment.appointment.color;
492
+
493
+ // search a color that is readable on the background color
494
+ const rgb = appointmentSegment.style.backgroundColor.match(/\d+/g);
495
+ const brightness = Math.round(
496
+ (parseInt(rgb[0]) * 299 +
497
+ parseInt(rgb[1]) * 587 +
498
+ parseInt(rgb[2]) * 114) /
499
+ 1000,
500
+ );
501
+
502
+ if (brightness > 125) {
503
+ appointmentSegment.style.color = "#000000";
504
+ } else {
505
+ appointmentSegment.style.color = "#ffffff";
506
+ }
507
+
508
+ appointmentSegment.style.width = `${currentWithOfGridCell * segment.columns}px`;
509
+ appointmentSegment.style.height = maxLineHeight + "px";
510
+ appointmentSegment.style.top = `${segment.line * maxLineHeight + maxLineHeight + 4}px`;
511
+
512
+ appointmentSegment.setOption("labels.text", segment.label);
513
+
514
+ container.appendChild(appointmentSegment);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Generates two arrays: one for the calendar grid (42 days) and one for the weekday headers (7 days).
520
+ * The grid always starts on the Monday of the week that contains the first of the given month.
521
+ *
522
+ * @returns {Object} An object containing:
523
+ * - calendarDays: Array of 42 objects, each representing a day.
524
+ * - calendarWeekdays: Array of seven objects, each representing a weekday header.
525
+ */
526
+ function generateCalendarData() {
527
+ let selectedDate = this.getOption("startDate");
528
+ if (!(selectedDate instanceof Date)) {
529
+ if (typeof selectedDate === "string") {
530
+ try {
531
+ selectedDate = new Date(selectedDate);
532
+ } catch (e) {
533
+ addErrorAttribute(this, "Invalid calendar date");
534
+ return { calendarDays, calendarWeekdays };
535
+ }
536
+ } else {
537
+ addErrorAttribute(this, "Invalid calendar date");
538
+ return { calendarDays, calendarWeekdays };
539
+ }
540
+ }
541
+
542
+ const calendarDays = [];
543
+ let calendarWeekdays = [];
544
+
545
+ if (!(selectedDate instanceof Date)) {
546
+ addErrorAttribute(this, "Invalid calendar date");
547
+ return { calendarDays, calendarWeekdays };
548
+ }
549
+
550
+ // Get the year and month from the provided date
551
+ const year = selectedDate.getFullYear();
552
+ const month = selectedDate.getMonth(); // 0-based index (0 = January)
553
+
554
+ // Create a Date object for the 1st of the given month
555
+ const firstDayOfMonth = new Date(year, month, 1);
556
+
557
+ // Determine the weekday index of the 1st day, ensuring Monday = 0
558
+ const weekdayIndex = (firstDayOfMonth.getDay() + 6) % 7;
559
+
560
+ // Calculate the start date: move backward to the Monday of the starting week
561
+ const startDate = new Date(firstDayOfMonth);
562
+ startDate.setDate(firstDayOfMonth.getDate() - weekdayIndex);
563
+
564
+ // Generate 42 days (6 weeks × 7 days)
565
+ for (let i = 0; i < 42; i++) {
566
+ const current = new Date(startDate);
567
+ current.setDate(startDate.getDate() + i);
568
+
569
+ const label = current.getDate().toString();
570
+
571
+ const dayKey =
572
+ current.getFullYear() +
573
+ "-" +
574
+ ("00" + (current.getMonth() + 1)).slice(-2) +
575
+ "-" +
576
+ ("00" + current.getDate()).slice(-2);
577
+
578
+ calendarDays.push({
579
+ date: current,
580
+ //day: current.getDate(),
581
+ month: current.getMonth() + 1, // 1-based month (1-12)
582
+ year: current.getFullYear(),
583
+ isCurrentMonth: current.getMonth() === month,
584
+ label: label,
585
+ index: i,
586
+ day: dayKey,
587
+
588
+ classes:
589
+ "day-cell " +
590
+ (current.getMonth() === month ? "current-month" : "other-month") +
591
+ (current.getDay() === 0 || current.getDay() === 6 ? " weekend" : "") +
592
+ (current.toDateString() === new Date().toDateString() ? " today" : ""),
593
+ appointments: [],
594
+ });
595
+ }
596
+
597
+ // Generate weekday header array (Monday through Sunday)
598
+ let format = this.getOption("locale.weekdayFormat");
599
+ if (!["long", "short", "narrow"].includes(format)) {
600
+ addErrorAttribute(this, "Invalid weekday format option " + format);
601
+ format = "short";
602
+ }
603
+ const weekdayNames = getWeekdays(format);
604
+ calendarWeekdays = weekdayNames.map((name, index) => {
605
+ return {
606
+ label: name,
607
+ index: index,
608
+ };
609
+ });
610
+
611
+ return { calendarDays, calendarWeekdays };
612
+ }
613
+
614
+ /**
615
+ * Generates a map that contains an array of appointments for each day within the calendar range.
616
+ * Multi-day appointments will appear on each day they span.
617
+ *
618
+ * @param {Array} appointments - Array of appointment objects. Expected properties: "startDate" and "endDate".
619
+ * @param {Date|string} start - Calendar start date.
620
+ * @param {Date|string} end - Calendar end date.
621
+ * @returns {Object} A map in the format { "YYYY-MM-DD": [appointment1, appointment2, ...] }
622
+ */
623
+ function getAppointmentsPerDay(appointments, start, end) {
624
+ const appointmentsMap = {};
625
+
626
+ // Convert start and end to Date objects if needed
627
+ const startDate = start instanceof Date ? start : new Date(start);
628
+ const endDate = end instanceof Date ? end : new Date(end);
629
+
630
+ // Create an empty entry for each day in the calendar range
631
+ let current = new Date(startDate);
632
+ while (current <= endDate) {
633
+ const key = current.toISOString().slice(0, 10);
634
+ appointmentsMap[key] = [];
635
+ current.setDate(current.getDate() + 1);
636
+ }
637
+
638
+ // Assign each appointment to the corresponding days
639
+ appointments.forEach((appointment) => {
640
+ if (!appointment.startDate || !appointment.endDate) {
641
+ // Skip appointments with missing data
642
+ return;
643
+ }
644
+
645
+ const appStart = new Date(appointment.startDate);
646
+ const appEnd = new Date(appointment.endDate);
647
+
648
+ // Determine the effective start and end dates to ensure appointments outside the calendar are ignored
649
+ const effectiveStart = appStart < startDate ? startDate : appStart;
650
+ const effectiveEnd = appEnd > endDate ? endDate : appEnd;
651
+
652
+ let currentAppDate = new Date(effectiveStart);
653
+ while (currentAppDate <= effectiveEnd) {
654
+ const key = currentAppDate.toISOString().slice(0, 10);
655
+ if (appointmentsMap[key]) {
656
+ appointmentsMap[key].push(appointment);
657
+ }
658
+ currentAppDate.setDate(currentAppDate.getDate() + 1);
659
+ }
660
+ });
661
+
662
+ return appointmentsMap;
663
+ }
664
+
665
+ /**
666
+ * @private
667
+ * @return {initEventHandler}
668
+ * @fires monster-calendar-clicked
669
+ */
670
+ function initEventHandler() {
671
+ const self = this;
672
+
673
+ setTimeout(() => {
674
+ this.attachObserver(
675
+ new Observer(() => {
676
+ placeAppointments.call(this);
677
+ }),
678
+ );
679
+
680
+ this[calendarElementSymbol]
681
+ .querySelectorAll("[data-monster-role='day-cell']")
682
+ .forEach((element) => {
683
+ element.addEventListener("click", (event) => {
684
+
685
+ console.log(event.relatedTarget,'event1')
686
+ console.log(event.composedPath(),'event')
687
+
688
+ const hoveredElement = this.shadowRoot.elementFromPoint(event.clientX, event.clientY);
689
+ if (hoveredElement instanceof AppointmentSegment) {
690
+ return;
691
+ }
692
+
693
+ const element = findTargetElementFromEvent(
694
+ event,
695
+ "data-monster-role",
696
+ "day-cell",
697
+ );
698
+
699
+ if (!element) {
700
+ return;
701
+ }
702
+
703
+ const popper = element.querySelector(
704
+ '[data-monster-role="appointment-popper"]',
705
+ );
706
+
707
+ if (!popper) {
708
+ return;
709
+ }
710
+
711
+ positionPopper(element, popper, {
712
+ placement: "bottom",
713
+ });
714
+
715
+ //const appointments = getAppointmentsPerDay() || [];
716
+
717
+
718
+ popper.style.width = element.getBoundingClientRect().width + "px";
719
+ popper.style.zIndex = 1000;
720
+
721
+ popper.style.display = "block";
722
+ });
723
+
724
+ element.addEventListener("mouseleave", (event) => {
725
+ const element = findTargetElementFromEvent(
726
+ event,
727
+ "data-monster-role",
728
+ "day-cell",
729
+ );
730
+ if (!element) {
731
+ return;
732
+ }
733
+
734
+ element.classList.remove("hover");
735
+ const popper = element.querySelector(
736
+ '[data-monster-role="appointment-popper"]',
737
+ );
738
+ if (!popper) {
739
+ return;
740
+ }
741
+
742
+ setTimeout(() => {
743
+ popper.style.display = "none";
744
+ }, 0);
745
+ });
746
+ });
747
+ }, 0);
748
+
749
+ return this;
750
+ }
751
+
752
+ function getTextHeight(text) {
753
+ // Ein unsichtbares div erstellen
754
+ const div = document.createElement("div");
755
+ div.style.position = "absolute";
756
+ div.style.whiteSpace = "nowrap";
757
+ div.style.visibility = "hidden";
758
+ div.textContent = text;
759
+
760
+ this.shadowRoot.appendChild(div);
761
+ const height = div.clientHeight;
762
+ this.shadowRoot.removeChild(div);
763
+
764
+ return height;
765
+ }
766
+
767
+ /**
768
+ * @private
769
+ * @return {void}
770
+ */
771
+ function initControlReferences() {
772
+ this[calendarElementSymbol] = this.shadowRoot.querySelector(
773
+ `[${ATTRIBUTE_ROLE}="control"]`,
774
+ );
775
+
776
+ this[calendarBodyElementSymbol] = this.shadowRoot.querySelector(
777
+ `[${ATTRIBUTE_ROLE}="calendar-body"]`,
778
+ );
779
+ }
780
+
781
+ /**
782
+ * @private
783
+ * @return {string}
784
+ */
785
+ function getTemplate() {
786
+ // language=HTML
787
+ return `
788
+ <template id="cell">
789
+ <div data-monster-role="day-cell"
790
+ data-monster-attributes="class path:cell.classes,
791
+ data-monster-index path:cell.index">
792
+ <div data-monster-replace="path:cell.label"></div>
793
+ <div data-monster-role="appointment-container"
794
+ data-monster-attributes="data-monster-day path:cell.day,
795
+ data-monster-calendar-index path:cell.index"></div>
796
+ <div data-monster-role="appointment-popper"
797
+ class="popper" data-monster-replace="path:cell.appointments"></div>
798
+ </div>
799
+ </template>
800
+
801
+ <template id="calendar-weekday-header">
802
+ <div data-monster-attributes="class path:calendar-weekday-header.classes,
803
+ data-monster-index path:calendar-weekday-header.index"
804
+ data-monster-replace="path:calendar-weekday-header.label"></div>
805
+ </template>
806
+
807
+
808
+ <div data-monster-role="control" part="control">
809
+ <div class="weekday-header">
810
+ <div data-monster-role="weekdays"
811
+ data-monster-insert="calendar-weekday-header path:calendarWeekdays"></div>
812
+ <div class="calendar-body" data-monster-role="calendar-body">
813
+ <div data-monster-role="cells" data-monster-insert="cell path:calendarDays"></div>
814
+ </div>
815
+ </div>
816
+ `;
817
+ }
818
+
819
+ registerCustomElement(MonthCalendar);