@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.
@@ -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 { CustomControl } from "../../dom/customcontrol.mjs";
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 { isFunction, isString } from "../../types/is.mjs";
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 { Appointment as AppointmentAppointment } from "./timeline/appointment.mjs";
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[week]) {
387
- groups[week] = [];
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
- Object.keys(groups).forEach((weekKey) => {
393
- const weekSegments = groups[weekKey];
394
-
395
- weekSegments.sort((a, b) => a.startIndex - b.startIndex);
396
-
397
- const lineEnds = [];
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
- weekSegments.forEach((segment) => {
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
- let placed = false;
403
-
404
- for (let line = 0; line < lineEnds.length; line++) {
405
- if (segStart >= lineEnds[line] + 1) {
406
- segment.line = line;
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
- if (!placed) {
414
- segment.line = lineEnds.length;
415
- lineEnds.push(segEnd);
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 > linesThatCanBeShown) {
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
- const self = this;
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 = div.clientHeight;
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