@schukai/monster 5.1.2 → 5.2.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 +8 -0
- package/package.json +1 -1
- package/source/components/time/month-calendar.mjs +553 -42
- package/source/components/time/style/month-calendar.pcss +26 -0
- package/source/components/time/stylesheet/month-calendar.mjs +1 -1
- package/source/types/version.mjs +1 -1
- package/test/cases/components/time/month-calendar.mjs +392 -2
|
@@ -11,25 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { instanceSymbol } from "../../constants.mjs";
|
|
14
|
-
import { addAttributeToken } from "../../dom/attributes.mjs";
|
|
15
14
|
import {
|
|
16
15
|
ATTRIBUTE_ERRORMESSAGE,
|
|
17
16
|
ATTRIBUTE_ROLE,
|
|
18
17
|
} from "../../dom/constants.mjs";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
CustomElement,
|
|
22
|
-
getSlottedElements,
|
|
23
|
-
initMethodSymbol,
|
|
24
|
-
} from "../../dom/customelement.mjs";
|
|
18
|
+
import { CustomElement, initMethodSymbol } from "../../dom/customelement.mjs";
|
|
25
19
|
import {
|
|
26
20
|
assembleMethodSymbol,
|
|
27
21
|
registerCustomElement,
|
|
28
22
|
} from "../../dom/customelement.mjs";
|
|
29
23
|
import { findTargetElementFromEvent } from "../../dom/events.mjs";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
import { fireCustomEvent } from "../../dom/events.mjs";
|
|
24
|
+
import { isString } from "../../types/is.mjs";
|
|
33
25
|
import { getLocaleOfDocument } from "../../dom/locale.mjs";
|
|
34
26
|
import { addErrorAttribute, removeErrorAttribute } from "../../dom/error.mjs";
|
|
35
27
|
import { MonthCalendarStyleSheet } from "./stylesheet/month-calendar.mjs";
|
|
@@ -43,7 +35,7 @@ import { Datasource } from "../datatable/datasource.mjs";
|
|
|
43
35
|
import { Observer } from "../../types/observer.mjs";
|
|
44
36
|
import { positionPopper } from "../form/util/floating-ui.mjs";
|
|
45
37
|
import { Segment as AppointmentSegment } from "./timeline/segment.mjs";
|
|
46
|
-
import
|
|
38
|
+
import "./timeline/appointment.mjs";
|
|
47
39
|
|
|
48
40
|
export { MonthCalendar };
|
|
49
41
|
|
|
@@ -66,6 +58,11 @@ const lastValidDateSymbol = Symbol("lastValidDate");
|
|
|
66
58
|
const calendarDateInvalidSymbol = Symbol("calendarDateInvalid");
|
|
67
59
|
const appointmentObserverSymbol = Symbol("appointmentObserver");
|
|
68
60
|
const boundDaysSymbol = Symbol("boundDays");
|
|
61
|
+
const appointmentMoveHandlersBoundSymbol = Symbol(
|
|
62
|
+
"appointmentMoveHandlersBound",
|
|
63
|
+
);
|
|
64
|
+
const appointmentMoveSymbol = Symbol("appointmentMove");
|
|
65
|
+
const pointerCandidateSymbol = Symbol("pointerCandidate");
|
|
69
66
|
|
|
70
67
|
/**
|
|
71
68
|
* A Calendar
|
|
@@ -77,6 +74,7 @@ const boundDaysSymbol = Symbol("boundDays");
|
|
|
77
74
|
* @since 3.112.0
|
|
78
75
|
* @copyright Volker Schukai
|
|
79
76
|
* @summary A month calendar for appointments and date-based planning, with optional datasource-driven event loading.
|
|
77
|
+
* @fires monster-appointment-move - Cancelable before the local data option is updated. Detail contains the original and moved appointment, stable id/index, source and target dates, day delta and input type.
|
|
80
78
|
*/
|
|
81
79
|
class MonthCalendar extends CustomElement {
|
|
82
80
|
/**
|
|
@@ -135,6 +133,7 @@ class MonthCalendar extends CustomElement {
|
|
|
135
133
|
* @property {Object} features Features for the control
|
|
136
134
|
* @property {boolean} features.showWeekend Show the weekend
|
|
137
135
|
* @property {boolean} features.monthOneLine Show the month in one line
|
|
136
|
+
* @property {boolean} features.appointmentMove Allow appointments to be moved by drag and drop or keyboard
|
|
138
137
|
* @property {Object} actions Actions for the control
|
|
139
138
|
* @property {Object} locale Locale for the control
|
|
140
139
|
* @property {string} locale.weekdayFormat Weekday format
|
|
@@ -156,13 +155,14 @@ class MonthCalendar extends CustomElement {
|
|
|
156
155
|
templates: {
|
|
157
156
|
main: getTemplate(),
|
|
158
157
|
},
|
|
159
|
-
labels: getTranslations(),
|
|
158
|
+
labels: Object.assign(getTranslations(), getMoveTranslations()),
|
|
160
159
|
classes: {},
|
|
161
160
|
|
|
162
161
|
disabled: false,
|
|
163
162
|
features: {
|
|
164
163
|
showWeekend: true,
|
|
165
164
|
monthOneLine: false,
|
|
165
|
+
appointmentMove: false,
|
|
166
166
|
},
|
|
167
167
|
|
|
168
168
|
actions: {},
|
|
@@ -306,6 +306,36 @@ function getTranslations() {
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
/**
|
|
310
|
+
* @private
|
|
311
|
+
* @return {object}
|
|
312
|
+
*/
|
|
313
|
+
function getMoveTranslations() {
|
|
314
|
+
const locale = getLocaleOfDocument();
|
|
315
|
+
if (locale.language === "de") {
|
|
316
|
+
return {
|
|
317
|
+
moveAppointment: "Termin verschieben: {label}",
|
|
318
|
+
moveInstructions:
|
|
319
|
+
"Leertaste zum Aufnehmen, Pfeiltasten zum Verschieben, Eingabetaste zum Ablegen und Escape zum Abbrechen.",
|
|
320
|
+
movePicked: "{label} aufgenommen. Zieldatum {date}.",
|
|
321
|
+
moveTarget: "Zieldatum {date}.",
|
|
322
|
+
moveCommitted: "{label} wurde auf {date} verschoben.",
|
|
323
|
+
moveCancelled: "Verschieben von {label} abgebrochen.",
|
|
324
|
+
moveInvalid: "Der Termin kann nicht auf {date} verschoben werden.",
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
moveAppointment: "Move appointment: {label}",
|
|
329
|
+
moveInstructions:
|
|
330
|
+
"Press Space to pick up, use the arrow keys to move, press Enter to drop, or Escape to cancel.",
|
|
331
|
+
movePicked: "Picked up {label}. Target date {date}.",
|
|
332
|
+
moveTarget: "Target date {date}.",
|
|
333
|
+
moveCommitted: "Moved {label} to {date}.",
|
|
334
|
+
moveCancelled: "Cancelled moving {label}.",
|
|
335
|
+
moveInvalid: "The appointment cannot be moved to {date}.",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
309
339
|
/**
|
|
310
340
|
* Splits an appointment across the visible calendar rows.
|
|
311
341
|
* Weekend dates are naturally omitted when the calendar contains weekdays only.
|
|
@@ -380,42 +410,50 @@ function getWeekdays(format = "long") {
|
|
|
380
410
|
* @return {Array} The segments with assigned "line" property
|
|
381
411
|
*/
|
|
382
412
|
function assignLinesToSegments(segments, columnCount = 7) {
|
|
383
|
-
const groups =
|
|
413
|
+
const groups = new Map();
|
|
384
414
|
segments.forEach((segment) => {
|
|
385
415
|
const week = Math.floor(segment.startIndex / columnCount);
|
|
386
|
-
if (!groups[
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
groups[week].push(segment);
|
|
416
|
+
if (!groups.has(week)) groups.set(week, []);
|
|
417
|
+
groups.get(week).push(segment);
|
|
390
418
|
});
|
|
391
419
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
420
|
+
const preferredLines = new Map();
|
|
421
|
+
const chronologicalWeeks = [...groups.entries()].sort(
|
|
422
|
+
([weekA], [weekB]) => weekA - weekB,
|
|
423
|
+
);
|
|
424
|
+
for (const [, weekSegments] of chronologicalWeeks) {
|
|
425
|
+
weekSegments.sort(
|
|
426
|
+
(a, b) =>
|
|
427
|
+
a.startIndex - b.startIndex ||
|
|
428
|
+
b.columns - a.columns ||
|
|
429
|
+
a.appointmentIndex - b.appointmentIndex,
|
|
430
|
+
);
|
|
398
431
|
|
|
399
|
-
|
|
432
|
+
const lines = [];
|
|
433
|
+
for (const segment of weekSegments) {
|
|
400
434
|
const segStart = segment.startIndex;
|
|
401
435
|
const segEnd = segment.startIndex + segment.columns - 1;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
lineEnds[line] = segEnd;
|
|
408
|
-
placed = true;
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
436
|
+
const preferred = preferredLines.get(segment.appointmentIndex);
|
|
437
|
+
const candidates = [];
|
|
438
|
+
if (Number.isInteger(preferred)) candidates.push(preferred);
|
|
439
|
+
for (let line = 0; line <= lines.length; line++) {
|
|
440
|
+
if (!candidates.includes(line)) candidates.push(line);
|
|
411
441
|
}
|
|
412
442
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
443
|
+
for (const line of candidates) {
|
|
444
|
+
const occupied = lines[line] || [];
|
|
445
|
+
const overlaps = occupied.some(
|
|
446
|
+
(range) => segStart <= range.end && segEnd >= range.start,
|
|
447
|
+
);
|
|
448
|
+
if (overlaps) continue;
|
|
449
|
+
segment.line = line;
|
|
450
|
+
occupied.push({ start: segStart, end: segEnd });
|
|
451
|
+
lines[line] = occupied;
|
|
452
|
+
preferredLines.set(segment.appointmentIndex, line);
|
|
453
|
+
break;
|
|
416
454
|
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
419
457
|
|
|
420
458
|
return segments;
|
|
421
459
|
}
|
|
@@ -505,7 +543,7 @@ function placeAppointments() {
|
|
|
505
543
|
day.appointments = app[k];
|
|
506
544
|
});
|
|
507
545
|
|
|
508
|
-
appointments.forEach((appointment) => {
|
|
546
|
+
appointments.forEach((appointment, appointmentIndex) => {
|
|
509
547
|
if (!appointment?.startDate || !appointment?.endDate) {
|
|
510
548
|
addErrorAttribute(this, "Missing start or end date in appointment");
|
|
511
549
|
return;
|
|
@@ -553,6 +591,7 @@ function placeAppointments() {
|
|
|
553
591
|
...segment,
|
|
554
592
|
label: label,
|
|
555
593
|
appointment: appointment,
|
|
594
|
+
appointmentIndex,
|
|
556
595
|
});
|
|
557
596
|
|
|
558
597
|
maxLineHeight = Math.max(maxLineHeight, getTextHeight.call(this, label));
|
|
@@ -599,7 +638,7 @@ function placeAppointments() {
|
|
|
599
638
|
containerHeight - maxLineHeight - marginAppointmentContainer;
|
|
600
639
|
let linesThatCanBeShown = Math.floor(availableHeight / maxLineHeight);
|
|
601
640
|
|
|
602
|
-
if (segment.line
|
|
641
|
+
if (segment.line >= linesThatCanBeShown) {
|
|
603
642
|
daysWithMoreAppointments.add(segment.start);
|
|
604
643
|
|
|
605
644
|
const calendarDays = this.getOption("calendarDays");
|
|
@@ -650,6 +689,7 @@ function placeAppointments() {
|
|
|
650
689
|
appointmentSegment.style.top = `${segment.line * maxLineHeight + h.maxHeaderHeight}px`;
|
|
651
690
|
|
|
652
691
|
appointmentSegment.setOption("labels.text", segment.label);
|
|
692
|
+
configureAppointmentSegment.call(this, appointmentSegment, segment);
|
|
653
693
|
|
|
654
694
|
container.appendChild(appointmentSegment);
|
|
655
695
|
}
|
|
@@ -703,6 +743,7 @@ function placeAppointments() {
|
|
|
703
743
|
appointmentSegment.style.height = maxLineHeight + "px";
|
|
704
744
|
|
|
705
745
|
appointmentSegment.setOption("labels.text", appointment.label);
|
|
746
|
+
configureAppointmentSegment.call(this, appointmentSegment, appointment);
|
|
706
747
|
|
|
707
748
|
moreAppointmentsContainer.appendChild(appointmentSegment);
|
|
708
749
|
}
|
|
@@ -971,12 +1012,478 @@ function getAppointmentsPerDay(appointments, start, end) {
|
|
|
971
1012
|
return appointmentsMap;
|
|
972
1013
|
}
|
|
973
1014
|
|
|
1015
|
+
function formatMoveLabel(template, values) {
|
|
1016
|
+
let value = String(template || "");
|
|
1017
|
+
for (const [key, replacement] of Object.entries(values)) {
|
|
1018
|
+
value = value.replaceAll(`{${key}}`, String(replacement));
|
|
1019
|
+
}
|
|
1020
|
+
return value;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function formatCalendarDay(value) {
|
|
1024
|
+
const date = parseCalendarDate(value);
|
|
1025
|
+
if (date === null) return "";
|
|
1026
|
+
return (
|
|
1027
|
+
date.getFullYear() +
|
|
1028
|
+
"-" +
|
|
1029
|
+
("00" + (date.getMonth() + 1)).slice(-2) +
|
|
1030
|
+
"-" +
|
|
1031
|
+
("00" + date.getDate()).slice(-2)
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function isAppointmentMoveEnabled() {
|
|
1036
|
+
return (
|
|
1037
|
+
this.getOption("features.appointmentMove") === true &&
|
|
1038
|
+
this.getOption("disabled") !== true
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function configureAppointmentSegment(element, segment) {
|
|
1043
|
+
const enabled = isAppointmentMoveEnabled.call(this);
|
|
1044
|
+
const label = segment.label || "Appointment";
|
|
1045
|
+
element.setAttribute("data-monster-role", "appointment-segment");
|
|
1046
|
+
element.setAttribute(
|
|
1047
|
+
"data-monster-appointment-index",
|
|
1048
|
+
String(segment.appointmentIndex),
|
|
1049
|
+
);
|
|
1050
|
+
element.setAttribute("data-monster-segment-start", segment.start);
|
|
1051
|
+
if (!enabled) return;
|
|
1052
|
+
|
|
1053
|
+
element.draggable = true;
|
|
1054
|
+
element.tabIndex = 0;
|
|
1055
|
+
element.setAttribute("role", "button");
|
|
1056
|
+
element.setAttribute(
|
|
1057
|
+
"aria-keyshortcuts",
|
|
1058
|
+
"Space Enter ArrowLeft ArrowRight ArrowUp ArrowDown Escape",
|
|
1059
|
+
);
|
|
1060
|
+
const moveLabel = formatMoveLabel(this.getOption("labels.moveAppointment"), {
|
|
1061
|
+
label,
|
|
1062
|
+
});
|
|
1063
|
+
element.setAttribute(
|
|
1064
|
+
"aria-label",
|
|
1065
|
+
`${moveLabel}. ${this.getOption("labels.moveInstructions")}`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function getAppointmentSegmentFromEvent(event) {
|
|
1070
|
+
return findTargetElementFromEvent(event, "data-monster-appointment-index");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function getCalendarDayByKey(dayKey) {
|
|
1074
|
+
return this.getOption("calendarDays").find((day) => day.day === dayKey);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function getCalendarDayAtPoint(clientX, clientY, event) {
|
|
1078
|
+
const cells = this[calendarBodyElementSymbol]?.querySelector(
|
|
1079
|
+
'[data-monster-role="cells"]',
|
|
1080
|
+
);
|
|
1081
|
+
if (cells instanceof HTMLElement) {
|
|
1082
|
+
const rect = cells.getBoundingClientRect();
|
|
1083
|
+
const columns = this.getOption("features.showWeekend") === false ? 5 : 7;
|
|
1084
|
+
const days = this.getOption("calendarDays");
|
|
1085
|
+
const rows = Math.ceil(days.length / columns);
|
|
1086
|
+
if (
|
|
1087
|
+
rect.width > 0 &&
|
|
1088
|
+
rect.height > 0 &&
|
|
1089
|
+
clientX >= rect.left &&
|
|
1090
|
+
clientX <= rect.right &&
|
|
1091
|
+
clientY >= rect.top &&
|
|
1092
|
+
clientY <= rect.bottom
|
|
1093
|
+
) {
|
|
1094
|
+
const column = Math.min(
|
|
1095
|
+
columns - 1,
|
|
1096
|
+
Math.floor((clientX - rect.left) / (rect.width / columns)),
|
|
1097
|
+
);
|
|
1098
|
+
const row = Math.min(
|
|
1099
|
+
rows - 1,
|
|
1100
|
+
Math.floor((clientY - rect.top) / (rect.height / rows)),
|
|
1101
|
+
);
|
|
1102
|
+
return days[row * columns + column];
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (event) {
|
|
1107
|
+
const cell = findTargetElementFromEvent(
|
|
1108
|
+
event,
|
|
1109
|
+
"data-monster-role",
|
|
1110
|
+
"day-cell",
|
|
1111
|
+
);
|
|
1112
|
+
if (cell) return getCalendarDayByKey.call(this, cell.dataset.monsterDay);
|
|
1113
|
+
}
|
|
1114
|
+
return undefined;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function announceAppointmentMove(message) {
|
|
1118
|
+
const status = this.shadowRoot.querySelector(
|
|
1119
|
+
'[data-monster-role="move-status"]',
|
|
1120
|
+
);
|
|
1121
|
+
if (status) status.textContent = message;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function clearAppointmentMove() {
|
|
1125
|
+
this.shadowRoot
|
|
1126
|
+
.querySelectorAll("[data-monster-drop-target]")
|
|
1127
|
+
.forEach((element) => {
|
|
1128
|
+
element.removeAttribute("data-monster-drop-target");
|
|
1129
|
+
});
|
|
1130
|
+
this.shadowRoot
|
|
1131
|
+
.querySelectorAll("[data-monster-dragging]")
|
|
1132
|
+
.forEach((element) => {
|
|
1133
|
+
element.removeAttribute("data-monster-dragging");
|
|
1134
|
+
});
|
|
1135
|
+
delete this[appointmentMoveSymbol];
|
|
1136
|
+
delete this[pointerCandidateSymbol];
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function setAppointmentMoveTarget(day) {
|
|
1140
|
+
const move = this[appointmentMoveSymbol];
|
|
1141
|
+
if (!move || !day) return;
|
|
1142
|
+
this.shadowRoot
|
|
1143
|
+
.querySelectorAll("[data-monster-drop-target]")
|
|
1144
|
+
.forEach((element) => {
|
|
1145
|
+
element.removeAttribute("data-monster-drop-target");
|
|
1146
|
+
});
|
|
1147
|
+
const cell = this.shadowRoot.querySelector(
|
|
1148
|
+
`[data-monster-role="day-cell"][data-monster-day="${day.day}"]`,
|
|
1149
|
+
);
|
|
1150
|
+
cell?.setAttribute("data-monster-drop-target", "true");
|
|
1151
|
+
move.targetDay = day.day;
|
|
1152
|
+
announceAppointmentMove.call(
|
|
1153
|
+
this,
|
|
1154
|
+
formatMoveLabel(this.getOption("labels.moveTarget"), { date: day.day }),
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function beginAppointmentMove(segment, anchorDay, inputType) {
|
|
1159
|
+
if (!isAppointmentMoveEnabled.call(this) || !segment || !anchorDay)
|
|
1160
|
+
return false;
|
|
1161
|
+
const appointmentIndex = Number(
|
|
1162
|
+
segment.getAttribute("data-monster-appointment-index"),
|
|
1163
|
+
);
|
|
1164
|
+
const appointment = this.getOption("data")[appointmentIndex];
|
|
1165
|
+
if (!appointment) return false;
|
|
1166
|
+
|
|
1167
|
+
const label =
|
|
1168
|
+
appointment.label || segment.getOption?.("labels.text") || "Appointment";
|
|
1169
|
+
this[appointmentMoveSymbol] = {
|
|
1170
|
+
appointmentIndex,
|
|
1171
|
+
anchorDay: anchorDay.day,
|
|
1172
|
+
targetDay: anchorDay.day,
|
|
1173
|
+
inputType,
|
|
1174
|
+
label,
|
|
1175
|
+
};
|
|
1176
|
+
this.shadowRoot
|
|
1177
|
+
.querySelectorAll(`[data-monster-appointment-index="${appointmentIndex}"]`)
|
|
1178
|
+
.forEach((element) => {
|
|
1179
|
+
element.setAttribute("data-monster-dragging", "true");
|
|
1180
|
+
});
|
|
1181
|
+
setAppointmentMoveTarget.call(this, anchorDay);
|
|
1182
|
+
announceAppointmentMove.call(
|
|
1183
|
+
this,
|
|
1184
|
+
formatMoveLabel(this.getOption("labels.movePicked"), {
|
|
1185
|
+
label,
|
|
1186
|
+
date: anchorDay.day,
|
|
1187
|
+
}),
|
|
1188
|
+
);
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function getCalendarDayDelta(fromValue, toValue) {
|
|
1193
|
+
const from = parseCalendarDate(fromValue);
|
|
1194
|
+
const to = parseCalendarDate(toValue);
|
|
1195
|
+
if (from === null || to === null) return null;
|
|
1196
|
+
const fromUTC = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate());
|
|
1197
|
+
const toUTC = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate());
|
|
1198
|
+
return Math.round((toUTC - fromUTC) / 86400000);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function shiftAppointmentDate(value, dayDelta) {
|
|
1202
|
+
const date = parseCalendarDate(value);
|
|
1203
|
+
if (date === null) return null;
|
|
1204
|
+
date.setDate(date.getDate() + dayDelta);
|
|
1205
|
+
return value instanceof Date ? date : formatCalendarDay(date);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function completeAppointmentMove(inputType) {
|
|
1209
|
+
const move = this[appointmentMoveSymbol];
|
|
1210
|
+
if (!move) return false;
|
|
1211
|
+
const appointments = this.getOption("data");
|
|
1212
|
+
const appointment = appointments[move.appointmentIndex];
|
|
1213
|
+
const dayDelta = getCalendarDayDelta(move.anchorDay, move.targetDay);
|
|
1214
|
+
if (!appointment || dayDelta === null) {
|
|
1215
|
+
clearAppointmentMove.call(this);
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (dayDelta === 0) {
|
|
1220
|
+
clearAppointmentMove.call(this);
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const startDate = shiftAppointmentDate(appointment.startDate, dayDelta);
|
|
1225
|
+
const endDate = shiftAppointmentDate(appointment.endDate, dayDelta);
|
|
1226
|
+
const calendarDays = this.getOption("calendarDays");
|
|
1227
|
+
const firstDay = calendarDays[0]?.date;
|
|
1228
|
+
const lastDay = calendarDays.at(-1)?.date;
|
|
1229
|
+
const movedStart = parseCalendarDate(startDate);
|
|
1230
|
+
const movedEnd = parseCalendarDate(endDate);
|
|
1231
|
+
if (
|
|
1232
|
+
movedStart === null ||
|
|
1233
|
+
movedEnd === null ||
|
|
1234
|
+
!firstDay ||
|
|
1235
|
+
!lastDay ||
|
|
1236
|
+
movedStart < firstDay ||
|
|
1237
|
+
movedEnd > lastDay
|
|
1238
|
+
) {
|
|
1239
|
+
announceAppointmentMove.call(
|
|
1240
|
+
this,
|
|
1241
|
+
formatMoveLabel(this.getOption("labels.moveInvalid"), {
|
|
1242
|
+
date: move.targetDay,
|
|
1243
|
+
}),
|
|
1244
|
+
);
|
|
1245
|
+
clearAppointmentMove.call(this);
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const movedAppointment = { ...appointment, startDate, endDate };
|
|
1250
|
+
const detail = {
|
|
1251
|
+
appointment,
|
|
1252
|
+
movedAppointment,
|
|
1253
|
+
appointmentId: appointment.id ?? null,
|
|
1254
|
+
appointmentIndex: move.appointmentIndex,
|
|
1255
|
+
dayDelta,
|
|
1256
|
+
inputType: inputType || move.inputType,
|
|
1257
|
+
source: {
|
|
1258
|
+
anchorDate: move.anchorDay,
|
|
1259
|
+
startDate: appointment.startDate,
|
|
1260
|
+
endDate: appointment.endDate,
|
|
1261
|
+
},
|
|
1262
|
+
target: {
|
|
1263
|
+
anchorDate: move.targetDay,
|
|
1264
|
+
startDate,
|
|
1265
|
+
endDate,
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
const accepted = this.dispatchEvent(
|
|
1269
|
+
new CustomEvent("monster-appointment-move", {
|
|
1270
|
+
bubbles: true,
|
|
1271
|
+
cancelable: true,
|
|
1272
|
+
composed: true,
|
|
1273
|
+
detail,
|
|
1274
|
+
}),
|
|
1275
|
+
);
|
|
1276
|
+
if (!accepted) {
|
|
1277
|
+
announceAppointmentMove.call(
|
|
1278
|
+
this,
|
|
1279
|
+
formatMoveLabel(this.getOption("labels.moveCancelled"), {
|
|
1280
|
+
label: move.label,
|
|
1281
|
+
}),
|
|
1282
|
+
);
|
|
1283
|
+
clearAppointmentMove.call(this);
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const nextAppointments = appointments.map((value, index) =>
|
|
1288
|
+
index === move.appointmentIndex ? movedAppointment : value,
|
|
1289
|
+
);
|
|
1290
|
+
const focusIndex = move.appointmentIndex;
|
|
1291
|
+
const label = move.label;
|
|
1292
|
+
const targetDay = move.targetDay;
|
|
1293
|
+
clearAppointmentMove.call(this);
|
|
1294
|
+
this.setOption("data", nextAppointments);
|
|
1295
|
+
announceAppointmentMove.call(
|
|
1296
|
+
this,
|
|
1297
|
+
formatMoveLabel(this.getOption("labels.moveCommitted"), {
|
|
1298
|
+
label,
|
|
1299
|
+
date: targetDay,
|
|
1300
|
+
}),
|
|
1301
|
+
);
|
|
1302
|
+
setTimeout(() => {
|
|
1303
|
+
this.shadowRoot
|
|
1304
|
+
.querySelector(`[data-monster-appointment-index="${focusIndex}"]`)
|
|
1305
|
+
?.focus();
|
|
1306
|
+
}, 0);
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function initAppointmentMoveHandlers() {
|
|
1311
|
+
if (this[appointmentMoveHandlersBoundSymbol]) return;
|
|
1312
|
+
this[appointmentMoveHandlersBoundSymbol] = true;
|
|
1313
|
+
const calendar = this[calendarElementSymbol];
|
|
1314
|
+
|
|
1315
|
+
calendar.addEventListener("pointerdown", (event) => {
|
|
1316
|
+
const segment = getAppointmentSegmentFromEvent(event);
|
|
1317
|
+
if (!isAppointmentMoveEnabled.call(this) || !segment) return;
|
|
1318
|
+
const anchorDay =
|
|
1319
|
+
getCalendarDayAtPoint.call(this, event.clientX, event.clientY, event) ||
|
|
1320
|
+
getCalendarDayByKey.call(
|
|
1321
|
+
this,
|
|
1322
|
+
segment.getAttribute("data-monster-segment-start"),
|
|
1323
|
+
);
|
|
1324
|
+
this[pointerCandidateSymbol] = {
|
|
1325
|
+
segment,
|
|
1326
|
+
anchorDay,
|
|
1327
|
+
pointerId: event.pointerId,
|
|
1328
|
+
pointerType: event.pointerType,
|
|
1329
|
+
startX: event.clientX,
|
|
1330
|
+
startY: event.clientY,
|
|
1331
|
+
};
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
calendar.addEventListener("pointermove", (event) => {
|
|
1335
|
+
const candidate = this[pointerCandidateSymbol];
|
|
1336
|
+
if (
|
|
1337
|
+
!candidate ||
|
|
1338
|
+
candidate.pointerType === "mouse" ||
|
|
1339
|
+
candidate.pointerId !== event.pointerId
|
|
1340
|
+
)
|
|
1341
|
+
return;
|
|
1342
|
+
if (!this[appointmentMoveSymbol]) {
|
|
1343
|
+
const distance = Math.hypot(
|
|
1344
|
+
event.clientX - candidate.startX,
|
|
1345
|
+
event.clientY - candidate.startY,
|
|
1346
|
+
);
|
|
1347
|
+
if (distance < 8) return;
|
|
1348
|
+
if (
|
|
1349
|
+
!beginAppointmentMove.call(
|
|
1350
|
+
this,
|
|
1351
|
+
candidate.segment,
|
|
1352
|
+
candidate.anchorDay,
|
|
1353
|
+
"pointer",
|
|
1354
|
+
)
|
|
1355
|
+
)
|
|
1356
|
+
return;
|
|
1357
|
+
try {
|
|
1358
|
+
candidate.segment.setPointerCapture?.(event.pointerId);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (error?.name !== "NotFoundError") throw error;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
event.preventDefault();
|
|
1364
|
+
const target = getCalendarDayAtPoint.call(
|
|
1365
|
+
this,
|
|
1366
|
+
event.clientX,
|
|
1367
|
+
event.clientY,
|
|
1368
|
+
event,
|
|
1369
|
+
);
|
|
1370
|
+
if (target) setAppointmentMoveTarget.call(this, target);
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
calendar.addEventListener("pointerup", (event) => {
|
|
1374
|
+
const candidate = this[pointerCandidateSymbol];
|
|
1375
|
+
if (!candidate || candidate.pointerId !== event.pointerId) return;
|
|
1376
|
+
if (this[appointmentMoveSymbol]?.inputType === "pointer")
|
|
1377
|
+
completeAppointmentMove.call(this, "pointer");
|
|
1378
|
+
else delete this[pointerCandidateSymbol];
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
calendar.addEventListener("pointercancel", () => {
|
|
1382
|
+
clearAppointmentMove.call(this);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
calendar.addEventListener("dragstart", (event) => {
|
|
1386
|
+
const segment = getAppointmentSegmentFromEvent(event);
|
|
1387
|
+
if (!isAppointmentMoveEnabled.call(this) || !segment) return;
|
|
1388
|
+
const candidate = this[pointerCandidateSymbol];
|
|
1389
|
+
const anchorDay =
|
|
1390
|
+
(candidate?.segment === segment && candidate.anchorDay) ||
|
|
1391
|
+
getCalendarDayByKey.call(
|
|
1392
|
+
this,
|
|
1393
|
+
segment.getAttribute("data-monster-segment-start"),
|
|
1394
|
+
);
|
|
1395
|
+
if (!beginAppointmentMove.call(this, segment, anchorDay, "drag")) return;
|
|
1396
|
+
event.dataTransfer?.setData(
|
|
1397
|
+
"text/plain",
|
|
1398
|
+
segment.getAttribute("data-monster-appointment-index"),
|
|
1399
|
+
);
|
|
1400
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
calendar.addEventListener("dragover", (event) => {
|
|
1404
|
+
if (this[appointmentMoveSymbol]?.inputType !== "drag") return;
|
|
1405
|
+
const target = getCalendarDayAtPoint.call(
|
|
1406
|
+
this,
|
|
1407
|
+
event.clientX,
|
|
1408
|
+
event.clientY,
|
|
1409
|
+
event,
|
|
1410
|
+
);
|
|
1411
|
+
if (!target) return;
|
|
1412
|
+
event.preventDefault();
|
|
1413
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
1414
|
+
setAppointmentMoveTarget.call(this, target);
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
calendar.addEventListener("drop", (event) => {
|
|
1418
|
+
if (this[appointmentMoveSymbol]?.inputType !== "drag") return;
|
|
1419
|
+
event.preventDefault();
|
|
1420
|
+
completeAppointmentMove.call(this, "drag");
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
calendar.addEventListener("dragend", () => {
|
|
1424
|
+
if (this[appointmentMoveSymbol]?.inputType === "drag")
|
|
1425
|
+
clearAppointmentMove.call(this);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
calendar.addEventListener("keydown", (event) => {
|
|
1429
|
+
const segment = getAppointmentSegmentFromEvent(event);
|
|
1430
|
+
if (!isAppointmentMoveEnabled.call(this) || !segment) return;
|
|
1431
|
+
const move = this[appointmentMoveSymbol];
|
|
1432
|
+
if (!move) {
|
|
1433
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
1434
|
+
event.preventDefault();
|
|
1435
|
+
const anchorDay = getCalendarDayByKey.call(
|
|
1436
|
+
this,
|
|
1437
|
+
segment.getAttribute("data-monster-segment-start"),
|
|
1438
|
+
);
|
|
1439
|
+
beginAppointmentMove.call(this, segment, anchorDay, "keyboard");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (
|
|
1443
|
+
move.inputType !== "keyboard" ||
|
|
1444
|
+
Number(segment.getAttribute("data-monster-appointment-index")) !==
|
|
1445
|
+
move.appointmentIndex
|
|
1446
|
+
)
|
|
1447
|
+
return;
|
|
1448
|
+
|
|
1449
|
+
if (event.key === "Escape") {
|
|
1450
|
+
event.preventDefault();
|
|
1451
|
+
announceAppointmentMove.call(
|
|
1452
|
+
this,
|
|
1453
|
+
formatMoveLabel(this.getOption("labels.moveCancelled"), {
|
|
1454
|
+
label: move.label,
|
|
1455
|
+
}),
|
|
1456
|
+
);
|
|
1457
|
+
clearAppointmentMove.call(this);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (event.key === " " || event.key === "Enter") {
|
|
1461
|
+
event.preventDefault();
|
|
1462
|
+
completeAppointmentMove.call(this, "keyboard");
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const columns = this.getOption("features.showWeekend") === false ? 5 : 7;
|
|
1466
|
+
const offsets = {
|
|
1467
|
+
ArrowLeft: -1,
|
|
1468
|
+
ArrowRight: 1,
|
|
1469
|
+
ArrowUp: -columns,
|
|
1470
|
+
ArrowDown: columns,
|
|
1471
|
+
};
|
|
1472
|
+
if (!(event.key in offsets)) return;
|
|
1473
|
+
event.preventDefault();
|
|
1474
|
+
const days = this.getOption("calendarDays");
|
|
1475
|
+
const currentIndex = days.findIndex((day) => day.day === move.targetDay);
|
|
1476
|
+
const target = days[currentIndex + offsets[event.key]];
|
|
1477
|
+
if (target) setAppointmentMoveTarget.call(this, target);
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
974
1481
|
/**
|
|
975
1482
|
* @private
|
|
976
1483
|
* @return {initEventHandler}
|
|
977
1484
|
*/
|
|
978
1485
|
function initEventHandler() {
|
|
979
|
-
|
|
1486
|
+
initAppointmentMoveHandlers.call(this);
|
|
980
1487
|
|
|
981
1488
|
setTimeout(() => {
|
|
982
1489
|
if (!this[appointmentObserverSymbol]) {
|
|
@@ -1071,7 +1578,8 @@ function getTextHeight(text) {
|
|
|
1071
1578
|
div.textContent = text;
|
|
1072
1579
|
|
|
1073
1580
|
this.shadowRoot.appendChild(div);
|
|
1074
|
-
const height =
|
|
1581
|
+
const height =
|
|
1582
|
+
div.clientHeight || parseFloat(getComputedStyle(div).lineHeight) || 16;
|
|
1075
1583
|
this.shadowRoot.removeChild(div);
|
|
1076
1584
|
|
|
1077
1585
|
return height;
|
|
@@ -1133,6 +1641,9 @@ function getTemplate() {
|
|
|
1133
1641
|
<div data-monster-role="cells" data-monster-insert="cell path:calendarDays"></div>
|
|
1134
1642
|
</div>
|
|
1135
1643
|
</div>
|
|
1644
|
+
<div class="visually-hidden" data-monster-role="move-status"
|
|
1645
|
+
role="status" aria-live="polite" aria-atomic="true"></div>
|
|
1646
|
+
</div>
|
|
1136
1647
|
`;
|
|
1137
1648
|
}
|
|
1138
1649
|
|