kalendly 0.3.1 → 0.3.3

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/dist/index.umd.js CHANGED
@@ -78,6 +78,54 @@ 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 parseHourRanges(value, slotDuration) {
82
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
83
+ if (parts.length === 0) {
84
+ throw new Error(
85
+ `<kal-calendar> available-hours is empty. Omit the attribute to allow every hour, or name at least one HH:MM-HH:MM range.`
86
+ );
87
+ }
88
+ const ranges = parts.map((part) => {
89
+ const halves = part.split("-");
90
+ if (halves.length !== 2) {
91
+ throw new Error(
92
+ `<kal-calendar> available-hours range "${part}" is not HH:MM-HH:MM.`
93
+ );
94
+ }
95
+ const [start, end] = halves.map((half) => parseTimeToMinutes(half.trim()));
96
+ if (start === null || end === null) {
97
+ throw new Error(
98
+ `<kal-calendar> available-hours range "${part}" has an unreadable time. Use 24-hour HH:MM.`
99
+ );
100
+ }
101
+ if (start >= end) {
102
+ throw new Error(
103
+ `<kal-calendar> available-hours range "${part}" starts at or after it ends.`
104
+ );
105
+ }
106
+ if (start % slotDuration !== 0 || end % slotDuration !== 0) {
107
+ throw new Error(
108
+ `<kal-calendar> available-hours range "${part}" does not land on a ${slotDuration}-minute slot boundary.`
109
+ );
110
+ }
111
+ return [start, end];
112
+ });
113
+ if (mergeIntervals(ranges).length !== ranges.length) {
114
+ throw new Error(
115
+ `<kal-calendar> available-hours ranges overlap or touch: "${value}". Write each bookable window once.`
116
+ );
117
+ }
118
+ return ranges;
119
+ }
120
+ function isDateWithinWindow(date, min, max) {
121
+ const target = normalizeDate(date).getTime();
122
+ if (min && target < normalizeDate(min).getTime()) return false;
123
+ if (max && target > normalizeDate(max).getTime()) return false;
124
+ return true;
125
+ }
126
+ function isDayAllowed(date, days) {
127
+ return days === null || days.includes(date.getDay());
128
+ }
81
129
  function eventCoversDate(event, date) {
82
130
  const start = normalizeDate(new Date(event.date)).getTime();
83
131
  const target = normalizeDate(date).getTime();
@@ -451,12 +499,14 @@ var Kalendly = (() => {
451
499
  /**
452
500
  * Select a specific date
453
501
  */
454
- selectDate(date, dayIndex) {
502
+ selectDate(date, dayIndex, navigate = true) {
455
503
  this.state.selectedDate = date;
456
504
  this.state.selectedDayIndex = dayIndex ?? null;
457
505
  this.state.currentDate = date.getDate();
458
- this.state.currentMonth = date.getMonth();
459
- this.state.currentYear = date.getFullYear();
506
+ if (navigate) {
507
+ this.state.currentMonth = date.getMonth();
508
+ this.state.currentYear = date.getFullYear();
509
+ }
460
510
  this.updateTasks();
461
511
  this.notify();
462
512
  }
@@ -484,8 +534,8 @@ var Kalendly = (() => {
484
534
  /**
485
535
  * Handle date cell click
486
536
  */
487
- handleDateClick(date, dayIndex) {
488
- this.selectDate(date, dayIndex);
537
+ handleDateClick(date, dayIndex, options) {
538
+ this.selectDate(date, dayIndex, options?.navigate !== false);
489
539
  }
490
540
  /**
491
541
  * Check if date has events
@@ -714,6 +764,41 @@ var Kalendly = (() => {
714
764
  const val = this.getAttribute("max-year");
715
765
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() + 10;
716
766
  }
767
+ boundaryDate(attr) {
768
+ const val = this.getAttribute(attr);
769
+ if (!val) return null;
770
+ const parsed = new Date(val);
771
+ if (Number.isNaN(parsed.getTime())) {
772
+ throw new Error(
773
+ `<kal-calendar> ${attr} is unreadable: "${val}". Use a value Date can parse, the same as initial-date.`
774
+ );
775
+ }
776
+ return parsed;
777
+ }
778
+ get availableDays() {
779
+ const val = this.getAttribute("available-days");
780
+ if (val === null) return null;
781
+ const parts = val.split(",").map((part) => part.trim()).filter(Boolean);
782
+ if (parts.length === 0) {
783
+ throw new Error(
784
+ `<kal-calendar> available-days is empty. Omit the attribute to allow every day, or name at least one weekday.`
785
+ );
786
+ }
787
+ return parts.map((part) => {
788
+ const day = Number(part);
789
+ if (!Number.isInteger(day) || day < 0 || day > 6) {
790
+ throw new Error(
791
+ `<kal-calendar> available-days must list integers 0-6, 0 = Sunday. Got "${part}".`
792
+ );
793
+ }
794
+ return day;
795
+ });
796
+ }
797
+ get availableHours() {
798
+ const val = this.getAttribute("available-hours");
799
+ if (val === null) return null;
800
+ return parseHourRanges(val, this.slotDuration);
801
+ }
717
802
  initEngine() {
718
803
  const initialDateAttr = this.getAttribute("initial-date");
719
804
  const initialDate = initialDateAttr ? new Date(initialDateAttr) : void 0;
@@ -832,8 +917,26 @@ var Kalendly = (() => {
832
917
  if (declared.size === 0) return "open";
833
918
  return this.knownBuckets.find((bucket) => declared.has(bucket));
834
919
  }
920
+ assertHorizonOrdered() {
921
+ const min = this.boundaryDate("min-date");
922
+ const max = this.boundaryDate("max-date");
923
+ if (min && max && min.getTime() > max.getTime()) {
924
+ throw new Error(
925
+ `<kal-calendar> min-date is after max-date: "${this.getAttribute("min-date")}" > "${this.getAttribute("max-date")}".`
926
+ );
927
+ }
928
+ }
929
+ // Whether the vendor offers this day at all, before any event is considered.
930
+ isDateOffered(date) {
931
+ return isDateWithinWindow(
932
+ date,
933
+ this.boundaryDate("min-date"),
934
+ this.boundaryDate("max-date")
935
+ ) && isDayAllowed(date, this.availableDays);
936
+ }
835
937
  isDateSelectable(date) {
836
938
  if (!this.engine) return false;
939
+ if (!this.isDateOffered(date)) return false;
837
940
  if (this._selectableStatuses) {
838
941
  return this._selectableStatuses.includes(this.resolveBucket(date));
839
942
  }
@@ -853,6 +956,11 @@ var Kalendly = (() => {
853
956
  this.assertStatusesDeclared();
854
957
  this.assertStatusesKnown();
855
958
  }
959
+ this.boundaryDate("min-date");
960
+ this.boundaryDate("max-date");
961
+ void this.availableDays;
962
+ void this.availableHours;
963
+ this.assertHorizonOrdered();
856
964
  const today = /* @__PURE__ */ new Date();
857
965
  const todayMonth = today.getMonth();
858
966
  const todayYear = today.getFullYear();
@@ -896,7 +1004,7 @@ var Kalendly = (() => {
896
1004
  return `
897
1005
  <div class="event-card" style="border-left-color: ${borderColor}">
898
1006
  <div class="event-header">
899
- <div class="event-title">${escapeHtml(event.name)}</div>
1007
+ ${event.name ? `<div class="event-title">${escapeHtml(event.name)}</div>` : ""}
900
1008
  <div class="event-badges">
901
1009
  ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
902
1010
  ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
@@ -972,25 +1080,31 @@ var Kalendly = (() => {
972
1080
  ];
973
1081
  this.warnMissingEndTime(events);
974
1082
  const booked = bookedSlots(events, date, slotDuration);
1083
+ const hours = this.availableHours;
975
1084
  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;
1085
+ const slotStart = index * slotDuration;
1086
+ const slotEnd = slotStart + slotDuration;
1087
+ const startTime = formatMinutes(slotStart);
1088
+ const endTime = formatMinutes(slotEnd);
1089
+ const offered = hours === null || hours.some(([from, to]) => slotStart >= from && slotEnd <= to);
1090
+ const inTimeRange = offered && !isBooked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
979
1091
  const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
980
1092
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
981
1093
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
982
1094
  const slotClasses = [
983
1095
  "time-grid-slot",
1096
+ offered ? "" : "time-grid-slot-out-of-range",
984
1097
  isBooked ? "time-grid-slot-blocked" : "time-grid-slot-open",
985
1098
  isRangeStart ? "time-grid-slot-range-start" : "",
986
1099
  isRangeEnd ? "time-grid-slot-range-end" : "",
987
1100
  isInRange ? "time-grid-slot-in-range" : ""
988
1101
  ].filter(Boolean).join(" ");
989
- const slotAttrs = `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
1102
+ const slotAttrs = `${offered ? 'data-action="select-slot" ' : ""}data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}" data-booked="${isBooked}"`;
1103
+ const status = !offered ? "Closed" : isBooked ? "Booked" : "Available";
990
1104
  return `
991
1105
  <div class="${slotClasses}" ${slotAttrs}>
992
1106
  <span class="time-grid-label">${startTime}</span>
993
- <span class="time-grid-status">${isBooked ? "Booked" : "Available"}</span>
1107
+ <span class="time-grid-status">${status}</span>
994
1108
  </div>`;
995
1109
  });
996
1110
  const gridClasses = selectable ? "time-grid time-grid-selectable" : "time-grid";
@@ -1025,7 +1139,7 @@ var Kalendly = (() => {
1025
1139
  )) {
1026
1140
  classes.push("calendar-cell-selected");
1027
1141
  }
1028
- if (availabilityMode && calendarDate.isCurrentMonth) {
1142
+ if (availabilityMode) {
1029
1143
  const bucket = this.resolveBucket(
1030
1144
  calendarDate.date
1031
1145
  );
@@ -1046,7 +1160,7 @@ var Kalendly = (() => {
1046
1160
  `aria-label="${escapeHtml(bucket)}"`
1047
1161
  );
1048
1162
  }
1049
- if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
1163
+ if (availabilityMode === "day" && selectable) {
1050
1164
  const d = calendarDate.date;
1051
1165
  if (this._rangeStart && isSameDay2(d, this._rangeStart))
1052
1166
  classes.push("availability-range-start");
@@ -1058,12 +1172,18 @@ var Kalendly = (() => {
1058
1172
  }
1059
1173
  }
1060
1174
  const dateString = calendarDate.date.toISOString();
1175
+ const offered = this.isDateOffered(
1176
+ calendarDate.date
1177
+ );
1178
+ if (!offered) {
1179
+ classes.push("calendar-cell-out-of-range");
1180
+ }
1061
1181
  return `
1062
1182
  <td
1063
1183
  class="${classes.join(" ")}"
1064
1184
  data-date="${dateString}"
1065
1185
  data-day-index="${dayIndex}"
1066
- data-clickable="true"
1186
+ ${offered ? 'data-clickable="true"' : ""}
1067
1187
  ${cellAttrs.join(" ")}
1068
1188
  >
1069
1189
  ${calendarDate.date.getDate()}
@@ -1251,7 +1371,7 @@ var Kalendly = (() => {
1251
1371
  this._timeRangeEnd = null;
1252
1372
  this._timeRangeComplete = false;
1253
1373
  }
1254
- this.engine.handleDateClick(date, dayIndex);
1374
+ this.engine.handleDateClick(date, dayIndex, { navigate: false });
1255
1375
  this.dispatchEvent(
1256
1376
  new CustomEvent("cal-date-select", {
1257
1377
  bubbles: true,
@@ -1489,6 +1609,10 @@ var Kalendly = (() => {
1489
1609
  "initial-date",
1490
1610
  "min-year",
1491
1611
  "max-year",
1612
+ "min-date",
1613
+ "max-date",
1614
+ "available-days",
1615
+ "available-hours",
1492
1616
  "week-starts-on",
1493
1617
  "heading",
1494
1618
  "months",
@@ -1510,6 +1634,18 @@ var Kalendly = (() => {
1510
1634
  borderColor: "--calendar-border-color",
1511
1635
  todayOutline: "--calendar-today-outline",
1512
1636
  selectedBg: "--calendar-selected-bg",
1637
+ outOfRangeBg: "--calendar-out-of-range-bg",
1638
+ outOfRangeFg: "--calendar-out-of-range-fg",
1639
+ navArrowFg: "--calendar-nav-arrow-fg",
1640
+ navArrowBg: "--calendar-nav-arrow-bg",
1641
+ navArrowBorder: "--calendar-nav-arrow-border",
1642
+ navArrowHoverFg: "--calendar-nav-arrow-hover-fg",
1643
+ navArrowHoverBg: "--calendar-nav-arrow-hover-bg",
1644
+ inputInvalidBg: "--calendar-input-invalid-bg",
1645
+ popupHeaderFg: "--calendar-popup-header-fg",
1646
+ popupCloseFg: "--calendar-popup-close-fg",
1647
+ popupCloseBg: "--calendar-popup-close-bg",
1648
+ popupCloseHoverBg: "--calendar-popup-close-hover-bg",
1513
1649
  headerBg: "--calendar-header-bg",
1514
1650
  popupBg: "--calendar-popup-bg",
1515
1651
  pickerBg: "--calendar-picker-bg",