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.umd.js
CHANGED
|
@@ -78,12 +78,28 @@ var Kalendly = (() => {
|
|
|
78
78
|
const max = maxYear ?? currentYear + 10;
|
|
79
79
|
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
80
80
|
}
|
|
81
|
+
function eventCoversDate(event, date) {
|
|
82
|
+
const start = normalizeDate(new Date(event.date)).getTime();
|
|
83
|
+
const target = normalizeDate(date).getTime();
|
|
84
|
+
if (Number.isNaN(start)) return false;
|
|
85
|
+
if (event.endDate === void 0 || event.endDate === null) {
|
|
86
|
+
return start === target;
|
|
87
|
+
}
|
|
88
|
+
const end = normalizeDate(new Date(event.endDate)).getTime();
|
|
89
|
+
if (Number.isNaN(end)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`<kal-calendar> event ${event.id} has an unreadable endDate: ${String(event.endDate)}.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (end < start) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`<kal-calendar> event ${event.id} has an endDate before its date: ${String(event.endDate)} < ${String(event.date)}.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return target >= start && target <= end;
|
|
100
|
+
}
|
|
81
101
|
function getEventsForDate(events, date) {
|
|
82
|
-
|
|
83
|
-
return events.filter((event) => {
|
|
84
|
-
const eventDate = normalizeDate(new Date(event.date));
|
|
85
|
-
return eventDate.getTime() === normalizedTargetDate.getTime();
|
|
86
|
-
});
|
|
102
|
+
return events.filter((event) => eventCoversDate(event, date));
|
|
87
103
|
}
|
|
88
104
|
function generateCalendarDates(year, month, events = [], weekStartsOn = 0) {
|
|
89
105
|
const firstDay = new Date(year, month, 1);
|
|
@@ -222,6 +238,68 @@ var Kalendly = (() => {
|
|
|
222
238
|
const trimmed = String(value).trim();
|
|
223
239
|
return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
|
|
224
240
|
}
|
|
241
|
+
var MINUTES_PER_DAY = 1440;
|
|
242
|
+
var DEFAULT_SLOT_DURATION = 60;
|
|
243
|
+
function parseTimeToMinutes(time) {
|
|
244
|
+
if (typeof time !== "string") return null;
|
|
245
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
246
|
+
if (!match) return null;
|
|
247
|
+
const hours = Number(match[1]);
|
|
248
|
+
const mins = Number(match[2]);
|
|
249
|
+
if (hours > 24 || mins > 59 || hours === 24 && mins > 0) return null;
|
|
250
|
+
return hours * 60 + mins;
|
|
251
|
+
}
|
|
252
|
+
function formatMinutes(total) {
|
|
253
|
+
const hours = Math.floor(total / 60);
|
|
254
|
+
const mins = total % 60;
|
|
255
|
+
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
256
|
+
}
|
|
257
|
+
function eventInterval(event, slotDuration, onDay) {
|
|
258
|
+
const day = normalizeDate(onDay).getTime();
|
|
259
|
+
if (Number.isNaN(day)) return null;
|
|
260
|
+
const base = day / 6e4;
|
|
261
|
+
const wholeDay = [base, base + MINUTES_PER_DAY];
|
|
262
|
+
if (event.allDay || !event.startTime) return wholeDay;
|
|
263
|
+
const from = parseTimeToMinutes(event.startTime);
|
|
264
|
+
if (from === null) return wholeDay;
|
|
265
|
+
if (event.endTime === void 0 || event.endTime === null) {
|
|
266
|
+
return [base + from, base + from + slotDuration];
|
|
267
|
+
}
|
|
268
|
+
const to = parseTimeToMinutes(event.endTime);
|
|
269
|
+
if (to === null) return wholeDay;
|
|
270
|
+
return [base + from, base + (to <= from ? to + MINUTES_PER_DAY : to)];
|
|
271
|
+
}
|
|
272
|
+
function mergeIntervals(intervals) {
|
|
273
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
274
|
+
return sorted.reduce((merged, [start, end]) => {
|
|
275
|
+
const last = merged[merged.length - 1];
|
|
276
|
+
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
|
277
|
+
else merged.push([start, end]);
|
|
278
|
+
return merged;
|
|
279
|
+
}, []);
|
|
280
|
+
}
|
|
281
|
+
function bookedSlots(events, date, slotDuration = DEFAULT_SLOT_DURATION) {
|
|
282
|
+
const base = normalizeDate(date).getTime() / 6e4;
|
|
283
|
+
const previousDay = new Date(date);
|
|
284
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
285
|
+
const intervals = [];
|
|
286
|
+
for (const event of new Set(events)) {
|
|
287
|
+
for (const day of [previousDay, date]) {
|
|
288
|
+
if (!eventCoversDate(event, day)) continue;
|
|
289
|
+
const interval = eventInterval(event, slotDuration, day);
|
|
290
|
+
if (interval) intervals.push(interval);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const merged = mergeIntervals(intervals);
|
|
294
|
+
return Array.from(
|
|
295
|
+
{ length: Math.floor(MINUTES_PER_DAY / slotDuration) },
|
|
296
|
+
(_, index) => {
|
|
297
|
+
const start = base + index * slotDuration;
|
|
298
|
+
const end = start + slotDuration;
|
|
299
|
+
return merged.some(([from, to]) => start < to && end > from);
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
}
|
|
225
303
|
|
|
226
304
|
// src/core/calendar-engine.ts
|
|
227
305
|
var CalendarEngine = class {
|
|
@@ -600,6 +678,29 @@ var Kalendly = (() => {
|
|
|
600
678
|
}
|
|
601
679
|
return title;
|
|
602
680
|
}
|
|
681
|
+
get slotDuration() {
|
|
682
|
+
const raw = this.getAttribute("slot-duration");
|
|
683
|
+
if (raw === null) return DEFAULT_SLOT_DURATION;
|
|
684
|
+
const minutes = Number(raw);
|
|
685
|
+
if (Number.isInteger(minutes) && minutes > 0 && MINUTES_PER_DAY % minutes === 0) {
|
|
686
|
+
return minutes;
|
|
687
|
+
}
|
|
688
|
+
console.warn(
|
|
689
|
+
`<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}.`
|
|
690
|
+
);
|
|
691
|
+
return DEFAULT_SLOT_DURATION;
|
|
692
|
+
}
|
|
693
|
+
warnMissingEndTime(events) {
|
|
694
|
+
for (const event of events) {
|
|
695
|
+
if (!event.startTime || event.endTime) continue;
|
|
696
|
+
const key = String(event.id);
|
|
697
|
+
if (_CalendarElement.warnedMissingEnd.has(key)) continue;
|
|
698
|
+
_CalendarElement.warnedMissingEnd.add(key);
|
|
699
|
+
console.warn(
|
|
700
|
+
`<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.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
603
704
|
get monthCount() {
|
|
604
705
|
const requested = Number(this.getAttribute("months") ?? 1);
|
|
605
706
|
if (!Number.isInteger(requested) || requested < 1) return 1;
|
|
@@ -861,42 +962,39 @@ var Kalendly = (() => {
|
|
|
861
962
|
const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
|
|
862
963
|
const renderEvent = this._renderEvent || defaultRenderEvent;
|
|
863
964
|
const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
|
|
864
|
-
const renderTimeGrid = (
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const startTime = `${String(hour).padStart(2, "0")}:00`;
|
|
879
|
-
const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
|
|
880
|
-
const booked = isHourBooked(hour);
|
|
881
|
-
const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
|
|
965
|
+
const renderTimeGrid = (date) => {
|
|
966
|
+
const slotDuration = this.slotDuration;
|
|
967
|
+
const previousDay = new Date(date);
|
|
968
|
+
previousDay.setDate(previousDay.getDate() - 1);
|
|
969
|
+
const events = [
|
|
970
|
+
...this.engine.getEventsForDate(previousDay),
|
|
971
|
+
...this.engine.getEventsForDate(date)
|
|
972
|
+
];
|
|
973
|
+
this.warnMissingEndTime(events);
|
|
974
|
+
const booked = bookedSlots(events, date, slotDuration);
|
|
975
|
+
const slots = booked.map((isBooked, index) => {
|
|
976
|
+
const startTime = formatMinutes(index * slotDuration);
|
|
977
|
+
const endTime = formatMinutes((index + 1) * slotDuration);
|
|
978
|
+
const inTimeRange = !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
|
|
882
979
|
const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
|
|
883
980
|
const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
|
|
884
981
|
const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
|
|
885
982
|
const slotClasses = [
|
|
886
983
|
"time-grid-slot",
|
|
887
|
-
|
|
984
|
+
isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
|
|
888
985
|
isRangeStart ? "time-grid-slot-range-start" : "",
|
|
889
986
|
isRangeEnd ? "time-grid-slot-range-end" : "",
|
|
890
987
|
isInRange ? "time-grid-slot-in-range" : ""
|
|
891
988
|
].filter(Boolean).join(" ");
|
|
892
|
-
const slotAttrs =
|
|
989
|
+
const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
|
|
893
990
|
return `
|
|
894
991
|
<div class="${slotClasses}" ${slotAttrs}>
|
|
895
992
|
<span class="time-grid-label">${startTime}</span>
|
|
896
|
-
<span class="time-grid-status">${
|
|
993
|
+
<span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
|
|
897
994
|
</div>`;
|
|
898
995
|
});
|
|
899
|
-
|
|
996
|
+
const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
|
|
997
|
+
return `<div class="${gridClasses}">${slots.join("")}</div>`;
|
|
900
998
|
};
|
|
901
999
|
const multiMonth = viewModel.panes.length > 1;
|
|
902
1000
|
const renderPane = (pane) => `
|
|
@@ -921,6 +1019,12 @@ var Kalendly = (() => {
|
|
|
921
1019
|
${week.map((calendarDate, dayIndex) => {
|
|
922
1020
|
const classes = getCellClasses(calendarDate);
|
|
923
1021
|
const cellAttrs = [];
|
|
1022
|
+
if (viewModel.selectedDate && isSameDay2(
|
|
1023
|
+
calendarDate.date,
|
|
1024
|
+
viewModel.selectedDate
|
|
1025
|
+
)) {
|
|
1026
|
+
classes.push("calendar-cell-selected");
|
|
1027
|
+
}
|
|
924
1028
|
if (availabilityMode && calendarDate.isCurrentMonth) {
|
|
925
1029
|
const bucket = this.resolveBucket(
|
|
926
1030
|
calendarDate.date
|
|
@@ -1073,7 +1177,7 @@ var Kalendly = (() => {
|
|
|
1073
1177
|
` : ""}
|
|
1074
1178
|
|
|
1075
1179
|
<div class="events-container">
|
|
1076
|
-
${availabilityMode === "time" ? renderTimeGrid(viewModel.
|
|
1180
|
+
${availabilityMode === "time" ? renderTimeGrid(viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
|
|
1077
1181
|
</div>
|
|
1078
1182
|
</div>
|
|
1079
1183
|
` : ""}
|
|
@@ -1234,6 +1338,20 @@ var Kalendly = (() => {
|
|
|
1234
1338
|
const slotStart = actionEl.dataset.startTime;
|
|
1235
1339
|
const slotEnd = actionEl.dataset.endTime;
|
|
1236
1340
|
const slotDate = new Date(actionEl.dataset.date);
|
|
1341
|
+
const slotBooked = actionEl.dataset.booked === "true";
|
|
1342
|
+
this.dispatchEvent(
|
|
1343
|
+
new CustomEvent("cal-slot-select", {
|
|
1344
|
+
bubbles: true,
|
|
1345
|
+
composed: true,
|
|
1346
|
+
detail: {
|
|
1347
|
+
date: slotDate,
|
|
1348
|
+
startTime: slotStart,
|
|
1349
|
+
endTime: slotEnd,
|
|
1350
|
+
booked: slotBooked
|
|
1351
|
+
}
|
|
1352
|
+
})
|
|
1353
|
+
);
|
|
1354
|
+
if (slotBooked || !this.hasAttribute("selectable")) break;
|
|
1237
1355
|
if (this._timeRangeComplete) {
|
|
1238
1356
|
this._timeRangeDate = slotDate;
|
|
1239
1357
|
this._timeRangeStart = slotStart;
|
|
@@ -1374,6 +1492,7 @@ var Kalendly = (() => {
|
|
|
1374
1492
|
"week-starts-on",
|
|
1375
1493
|
"heading",
|
|
1376
1494
|
"months",
|
|
1495
|
+
"slot-duration",
|
|
1377
1496
|
"title",
|
|
1378
1497
|
"use-short-month-names",
|
|
1379
1498
|
"availability-mode",
|
|
@@ -1425,6 +1544,7 @@ var Kalendly = (() => {
|
|
|
1425
1544
|
badgeTentativeBg: "--calendar-badge-tentative-bg",
|
|
1426
1545
|
badgeTentativeText: "--calendar-badge-tentative-text"
|
|
1427
1546
|
};
|
|
1547
|
+
_CalendarElement.warnedMissingEnd = /* @__PURE__ */ new Set();
|
|
1428
1548
|
_CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
|
|
1429
1549
|
_CalendarElement.titleDeprecationWarned = false;
|
|
1430
1550
|
var CalendarElement = _CalendarElement;
|