kalendly 0.3.0 → 0.3.1
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/README.md +104 -19
- package/dist/core/index.d.mts +10 -1
- package/dist/core/index.d.ts +10 -1
- package/dist/core/index.js +99 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +91 -5
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +165 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +157 -29
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +148 -28
- package/dist/index.umd.js.map +1 -1
- package/dist/styles/calendar.css +16 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,12 +43,28 @@ function generateYears(minYear, maxYear) {
|
|
|
43
43
|
const max = maxYear ?? currentYear + 10;
|
|
44
44
|
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
45
45
|
}
|
|
46
|
+
function eventCoversDate(event, date) {
|
|
47
|
+
const start = normalizeDate(new Date(event.date)).getTime();
|
|
48
|
+
const target = normalizeDate(date).getTime();
|
|
49
|
+
if (Number.isNaN(start)) return false;
|
|
50
|
+
if (event.endDate === void 0 || event.endDate === null) {
|
|
51
|
+
return start === target;
|
|
52
|
+
}
|
|
53
|
+
const end = normalizeDate(new Date(event.endDate)).getTime();
|
|
54
|
+
if (Number.isNaN(end)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`<kal-calendar> event ${event.id} has an unreadable endDate: ${String(event.endDate)}.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (end < start) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`<kal-calendar> event ${event.id} has an endDate before its date: ${String(event.endDate)} < ${String(event.date)}.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return target >= start && target <= end;
|
|
65
|
+
}
|
|
46
66
|
function getEventsForDate(events, date) {
|
|
47
|
-
|
|
48
|
-
return events.filter((event) => {
|
|
49
|
-
const eventDate = normalizeDate(new Date(event.date));
|
|
50
|
-
return eventDate.getTime() === normalizedTargetDate.getTime();
|
|
51
|
-
});
|
|
67
|
+
return events.filter((event) => eventCoversDate(event, date));
|
|
52
68
|
}
|
|
53
69
|
function hasEvents(events, date) {
|
|
54
70
|
return getEventsForDate(events, date).length > 0;
|
|
@@ -192,7 +208,67 @@ function safeColor(value) {
|
|
|
192
208
|
const trimmed = String(value).trim();
|
|
193
209
|
return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
|
|
194
210
|
}
|
|
195
|
-
|
|
211
|
+
function parseTimeToMinutes(time) {
|
|
212
|
+
if (typeof time !== "string") return null;
|
|
213
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
214
|
+
if (!match) return null;
|
|
215
|
+
const hours = Number(match[1]);
|
|
216
|
+
const mins = Number(match[2]);
|
|
217
|
+
if (hours > 24 || mins > 59 || hours === 24 && mins > 0) return null;
|
|
218
|
+
return hours * 60 + mins;
|
|
219
|
+
}
|
|
220
|
+
function formatMinutes(total) {
|
|
221
|
+
const hours = Math.floor(total / 60);
|
|
222
|
+
const mins = total % 60;
|
|
223
|
+
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
224
|
+
}
|
|
225
|
+
function eventInterval(event, slotDuration, onDay) {
|
|
226
|
+
const day = normalizeDate(onDay).getTime();
|
|
227
|
+
if (Number.isNaN(day)) return null;
|
|
228
|
+
const base = day / 6e4;
|
|
229
|
+
const wholeDay = [base, base + MINUTES_PER_DAY];
|
|
230
|
+
if (event.allDay || !event.startTime) return wholeDay;
|
|
231
|
+
const from = parseTimeToMinutes(event.startTime);
|
|
232
|
+
if (from === null) return wholeDay;
|
|
233
|
+
if (event.endTime === void 0 || event.endTime === null) {
|
|
234
|
+
return [base + from, base + from + slotDuration];
|
|
235
|
+
}
|
|
236
|
+
const to = parseTimeToMinutes(event.endTime);
|
|
237
|
+
if (to === null) return wholeDay;
|
|
238
|
+
return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];
|
|
239
|
+
}
|
|
240
|
+
function mergeIntervals(intervals) {
|
|
241
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
242
|
+
return sorted.reduce((merged, [start, end]) => {
|
|
243
|
+
const last = merged[merged.length - 1];
|
|
244
|
+
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
|
245
|
+
else merged.push([start, end]);
|
|
246
|
+
return merged;
|
|
247
|
+
}, []);
|
|
248
|
+
}
|
|
249
|
+
function bookedSlots(events, date, slotDuration = DEFAULT_SLOT_DURATION) {
|
|
250
|
+
const base = normalizeDate(date).getTime() / 6e4;
|
|
251
|
+
const previousDay = new Date(date);
|
|
252
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
253
|
+
const intervals = [];
|
|
254
|
+
for (const event of new Set(events)) {
|
|
255
|
+
for (const day of [previousDay, date]) {
|
|
256
|
+
if (!eventCoversDate(event, day)) continue;
|
|
257
|
+
const interval = eventInterval(event, slotDuration, day);
|
|
258
|
+
if (interval) intervals.push(interval);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const merged = mergeIntervals(intervals);
|
|
262
|
+
return Array.from(
|
|
263
|
+
{ length: Math.floor(MINUTES_PER_DAY / slotDuration) },
|
|
264
|
+
(_, index) => {
|
|
265
|
+
const start = base + index * slotDuration;
|
|
266
|
+
const end = start + slotDuration;
|
|
267
|
+
return merged.some(([from, to]) => start < to && end > from);
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES, MINUTES_PER_DAY, DEFAULT_SLOT_DURATION;
|
|
196
272
|
var init_utils = __esm({
|
|
197
273
|
"src/core/utils.ts"() {
|
|
198
274
|
"use strict";
|
|
@@ -248,6 +324,8 @@ var init_utils = __esm({
|
|
|
248
324
|
'"': """,
|
|
249
325
|
"'": "'"
|
|
250
326
|
};
|
|
327
|
+
MINUTES_PER_DAY = 1440;
|
|
328
|
+
DEFAULT_SLOT_DURATION = 60;
|
|
251
329
|
}
|
|
252
330
|
});
|
|
253
331
|
|
|
@@ -661,6 +739,29 @@ var init_CalendarElement = __esm({
|
|
|
661
739
|
}
|
|
662
740
|
return title;
|
|
663
741
|
}
|
|
742
|
+
get slotDuration() {
|
|
743
|
+
const raw = this.getAttribute("slot-duration");
|
|
744
|
+
if (raw === null) return DEFAULT_SLOT_DURATION;
|
|
745
|
+
const minutes = Number(raw);
|
|
746
|
+
if (Number.isInteger(minutes) && minutes > 0 && MINUTES_PER_DAY % minutes === 0) {
|
|
747
|
+
return minutes;
|
|
748
|
+
}
|
|
749
|
+
console.warn(
|
|
750
|
+
`<kal-calendar> slot-duration="${raw}" must be a positive whole number of minutes that divides ${MINUTES_PER_DAY} \u2014 falling back to ${DEFAULT_SLOT_DURATION}.`
|
|
751
|
+
);
|
|
752
|
+
return DEFAULT_SLOT_DURATION;
|
|
753
|
+
}
|
|
754
|
+
warnMissingEndTime(events) {
|
|
755
|
+
for (const event of events) {
|
|
756
|
+
if (!event.startTime || event.endTime) continue;
|
|
757
|
+
const key = String(event.id);
|
|
758
|
+
if (_CalendarElement.warnedMissingEnd.has(key)) continue;
|
|
759
|
+
_CalendarElement.warnedMissingEnd.add(key);
|
|
760
|
+
console.warn(
|
|
761
|
+
`<kal-calendar> event ${key} has startTime but no endTime \u2014 occupying one ${this.slotDuration}-minute slot. Send an endTime to say how long it runs.`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
664
765
|
get monthCount() {
|
|
665
766
|
const requested = Number(this.getAttribute("months") ?? 1);
|
|
666
767
|
if (!Number.isInteger(requested) || requested < 1) return 1;
|
|
@@ -922,42 +1023,39 @@ var init_CalendarElement = __esm({
|
|
|
922
1023
|
const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
|
|
923
1024
|
const renderEvent = this._renderEvent || defaultRenderEvent;
|
|
924
1025
|
const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
|
|
925
|
-
const renderTimeGrid = (
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const startTime = `${String(hour).padStart(2, "0")}:00`;
|
|
940
|
-
const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
|
|
941
|
-
const booked = isHourBooked(hour);
|
|
942
|
-
const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
|
|
1026
|
+
const renderTimeGrid = (date) => {
|
|
1027
|
+
const slotDuration = this.slotDuration;
|
|
1028
|
+
const previousDay = new Date(date);
|
|
1029
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
1030
|
+
const events = [
|
|
1031
|
+
...this.engine.getEventsForDate(previousDay),
|
|
1032
|
+
...this.engine.getEventsForDate(date)
|
|
1033
|
+
];
|
|
1034
|
+
this.warnMissingEndTime(events);
|
|
1035
|
+
const booked = bookedSlots(events, date, slotDuration);
|
|
1036
|
+
const slots = booked.map((isBooked, index) => {
|
|
1037
|
+
const startTime = formatMinutes(index * slotDuration);
|
|
1038
|
+
const endTime = formatMinutes((index + 1) * slotDuration);
|
|
1039
|
+
const inTimeRange = !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
|
|
943
1040
|
const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
|
|
944
1041
|
const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
|
|
945
1042
|
const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
|
|
946
1043
|
const slotClasses = [
|
|
947
1044
|
"time-grid-slot",
|
|
948
|
-
|
|
1045
|
+
isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
|
|
949
1046
|
isRangeStart ? "time-grid-slot-range-start" : "",
|
|
950
1047
|
isRangeEnd ? "time-grid-slot-range-end" : "",
|
|
951
1048
|
isInRange ? "time-grid-slot-in-range" : ""
|
|
952
1049
|
].filter(Boolean).join(" ");
|
|
953
|
-
const slotAttrs =
|
|
1050
|
+
const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
|
|
954
1051
|
return `
|
|
955
1052
|
<div class="${slotClasses}" ${slotAttrs}>
|
|
956
1053
|
<span class="time-grid-label">${startTime}</span>
|
|
957
|
-
<span class="time-grid-status">${
|
|
1054
|
+
<span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
|
|
958
1055
|
</div>`;
|
|
959
1056
|
});
|
|
960
|
-
|
|
1057
|
+
const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
|
|
1058
|
+
return `<div class="${gridClasses}">${slots.join("")}</div>`;
|
|
961
1059
|
};
|
|
962
1060
|
const multiMonth = viewModel.panes.length > 1;
|
|
963
1061
|
const renderPane = (pane) => `
|
|
@@ -982,6 +1080,12 @@ var init_CalendarElement = __esm({
|
|
|
982
1080
|
${week.map((calendarDate, dayIndex) => {
|
|
983
1081
|
const classes = getCellClasses(calendarDate);
|
|
984
1082
|
const cellAttrs = [];
|
|
1083
|
+
if (viewModel.selectedDate && isSameDay2(
|
|
1084
|
+
calendarDate.date,
|
|
1085
|
+
viewModel.selectedDate
|
|
1086
|
+
)) {
|
|
1087
|
+
classes.push("calendar-cell-selected");
|
|
1088
|
+
}
|
|
985
1089
|
if (availabilityMode && calendarDate.isCurrentMonth) {
|
|
986
1090
|
const bucket = this.resolveBucket(
|
|
987
1091
|
calendarDate.date
|
|
@@ -1134,7 +1238,7 @@ var init_CalendarElement = __esm({
|
|
|
1134
1238
|
` : ""}
|
|
1135
1239
|
|
|
1136
1240
|
<div class="events-container">
|
|
1137
|
-
${availabilityMode === "time" ? renderTimeGrid(viewModel.
|
|
1241
|
+
${availabilityMode === "time" ? renderTimeGrid(viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
|
|
1138
1242
|
</div>
|
|
1139
1243
|
</div>
|
|
1140
1244
|
` : ""}
|
|
@@ -1295,6 +1399,20 @@ var init_CalendarElement = __esm({
|
|
|
1295
1399
|
const slotStart = actionEl.dataset.startTime;
|
|
1296
1400
|
const slotEnd = actionEl.dataset.endTime;
|
|
1297
1401
|
const slotDate = new Date(actionEl.dataset.date);
|
|
1402
|
+
const slotBooked = actionEl.dataset.booked === "true";
|
|
1403
|
+
this.dispatchEvent(
|
|
1404
|
+
new CustomEvent("cal-slot-select", {
|
|
1405
|
+
bubbles: true,
|
|
1406
|
+
composed: true,
|
|
1407
|
+
detail: {
|
|
1408
|
+
date: slotDate,
|
|
1409
|
+
startTime: slotStart,
|
|
1410
|
+
endTime: slotEnd,
|
|
1411
|
+
booked: slotBooked
|
|
1412
|
+
}
|
|
1413
|
+
})
|
|
1414
|
+
);
|
|
1415
|
+
if (slotBooked || !this.hasAttribute("selectable")) break;
|
|
1298
1416
|
if (this._timeRangeComplete) {
|
|
1299
1417
|
this._timeRangeDate = slotDate;
|
|
1300
1418
|
this._timeRangeStart = slotStart;
|
|
@@ -1435,6 +1553,7 @@ var init_CalendarElement = __esm({
|
|
|
1435
1553
|
"week-starts-on",
|
|
1436
1554
|
"heading",
|
|
1437
1555
|
"months",
|
|
1556
|
+
"slot-duration",
|
|
1438
1557
|
"title",
|
|
1439
1558
|
"use-short-month-names",
|
|
1440
1559
|
"availability-mode",
|
|
@@ -1486,6 +1605,7 @@ var init_CalendarElement = __esm({
|
|
|
1486
1605
|
badgeTentativeBg: "--calendar-badge-tentative-bg",
|
|
1487
1606
|
badgeTentativeText: "--calendar-badge-tentative-text"
|
|
1488
1607
|
};
|
|
1608
|
+
_CalendarElement.warnedMissingEnd = /* @__PURE__ */ new Set();
|
|
1489
1609
|
_CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
|
|
1490
1610
|
_CalendarElement.titleDeprecationWarned = false;
|
|
1491
1611
|
CalendarElement = _CalendarElement;
|
|
@@ -1499,12 +1619,18 @@ __export(web_components_exports, {
|
|
|
1499
1619
|
CalendarEngine: () => CalendarEngine,
|
|
1500
1620
|
DAYS: () => DAYS,
|
|
1501
1621
|
DEFAULT_CATEGORY_COLORS: () => DEFAULT_CATEGORY_COLORS,
|
|
1622
|
+
DEFAULT_SLOT_DURATION: () => DEFAULT_SLOT_DURATION,
|
|
1623
|
+
MINUTES_PER_DAY: () => MINUTES_PER_DAY,
|
|
1502
1624
|
MONTHS: () => MONTHS,
|
|
1503
1625
|
MONTHS_FULL: () => MONTHS_FULL,
|
|
1626
|
+
bookedSlots: () => bookedSlots,
|
|
1504
1627
|
defineCalendarElement: () => defineCalendarElement,
|
|
1505
1628
|
escapeHtml: () => escapeHtml,
|
|
1629
|
+
eventCoversDate: () => eventCoversDate,
|
|
1630
|
+
eventInterval: () => eventInterval,
|
|
1506
1631
|
formatAttendees: () => formatAttendees,
|
|
1507
1632
|
formatDateForDisplay: () => formatDateForDisplay,
|
|
1633
|
+
formatMinutes: () => formatMinutes,
|
|
1508
1634
|
formatTimeRange: () => formatTimeRange,
|
|
1509
1635
|
generateCalendarDates: () => generateCalendarDates,
|
|
1510
1636
|
generateYears: () => generateYears,
|
|
@@ -1518,7 +1644,9 @@ __export(web_components_exports, {
|
|
|
1518
1644
|
isToday: () => isToday,
|
|
1519
1645
|
isValidHexColor: () => isValidHexColor,
|
|
1520
1646
|
mergeCategoryColors: () => mergeCategoryColors,
|
|
1647
|
+
mergeIntervals: () => mergeIntervals,
|
|
1521
1648
|
normalizeDate: () => normalizeDate,
|
|
1649
|
+
parseTimeToMinutes: () => parseTimeToMinutes,
|
|
1522
1650
|
safeColor: () => safeColor,
|
|
1523
1651
|
safeUrl: () => safeUrl,
|
|
1524
1652
|
slugifyToken: () => slugifyToken,
|
|
@@ -1540,12 +1668,18 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
|
|
|
1540
1668
|
CalendarEngine,
|
|
1541
1669
|
DAYS,
|
|
1542
1670
|
DEFAULT_CATEGORY_COLORS,
|
|
1671
|
+
DEFAULT_SLOT_DURATION,
|
|
1672
|
+
MINUTES_PER_DAY,
|
|
1543
1673
|
MONTHS,
|
|
1544
1674
|
MONTHS_FULL,
|
|
1675
|
+
bookedSlots,
|
|
1545
1676
|
defineCalendarElement,
|
|
1546
1677
|
escapeHtml,
|
|
1678
|
+
eventCoversDate,
|
|
1679
|
+
eventInterval,
|
|
1547
1680
|
formatAttendees,
|
|
1548
1681
|
formatDateForDisplay,
|
|
1682
|
+
formatMinutes,
|
|
1549
1683
|
formatTimeRange,
|
|
1550
1684
|
generateCalendarDates,
|
|
1551
1685
|
generateYears,
|
|
@@ -1559,7 +1693,9 @@ if (typeof customElements !== "undefined" && !customElements.get("kal-calendar")
|
|
|
1559
1693
|
isToday,
|
|
1560
1694
|
isValidHexColor,
|
|
1561
1695
|
mergeCategoryColors,
|
|
1696
|
+
mergeIntervals,
|
|
1562
1697
|
normalizeDate,
|
|
1698
|
+
parseTimeToMinutes,
|
|
1563
1699
|
safeColor,
|
|
1564
1700
|
safeUrl,
|
|
1565
1701
|
slugifyToken,
|