@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.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/source/components/content/camera.mjs +339 -329
- package/source/components/content/stylesheet/camera-capture.mjs +13 -6
- package/source/components/datatable/status.mjs +175 -177
- package/source/components/form/reload.mjs +1 -2
- package/source/components/form/util/fetch.mjs +5 -2
- package/source/components/layout/popper.mjs +1 -1
- package/source/components/layout/slider.mjs +1 -1
- package/source/components/time/month-calendar.mjs +819 -0
- package/source/components/time/style/month-calendar.pcss +100 -0
- package/source/components/time/stylesheet/month-calendar.mjs +31 -0
- package/source/components/time/timeline/collection.mjs +205 -0
- package/source/components/time/timeline/item.mjs +184 -0
- package/source/components/time/timeline/segment.mjs +169 -0
- package/source/components/time/timeline/style/segment.pcss +18 -0
- package/source/components/time/timeline/stylesheet/segment.mjs +38 -0
- package/source/data/datasource/server/restapi/data-fetch-error.mjs +3 -3
- package/source/data/datasource/server/restapi.mjs +1 -1
- package/source/data/transformer.mjs +60 -0
- package/source/monster.mjs +4 -0
- package/source/text/bracketed-key-value-hash.mjs +187 -187
- package/source/types/base.mjs +6 -5
- package/source/types/basewithoptions.mjs +4 -1
- package/source/types/internal.mjs +1 -1
- package/source/types/version.mjs +1 -1
- package/test/cases/monster.mjs +1 -1
- package/test/web/test.html +2 -2
- 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);
|