kalendly 0.2.0 → 0.2.2

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.js CHANGED
@@ -494,11 +494,12 @@ function defineCalendarElement(tagName = "kal-calendar") {
494
494
  customElements.define(tagName, CalendarElement);
495
495
  }
496
496
  }
497
- var CalendarElement;
497
+ var isSameDay2, CalendarElement;
498
498
  var init_CalendarElement = __esm({
499
499
  "src/web-components/CalendarElement.ts"() {
500
500
  "use strict";
501
501
  init_core();
502
+ isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
502
503
  CalendarElement = class extends HTMLElement {
503
504
  constructor() {
504
505
  super(...arguments);
@@ -520,6 +521,13 @@ var init_CalendarElement = __esm({
520
521
  this._categoryColors = null;
521
522
  this._renderEvent = null;
522
523
  this._renderNoEvents = null;
524
+ // Selection state (availability mode — range)
525
+ this._rangeStart = null;
526
+ this._rangeEnd = null;
527
+ this._timeRangeDate = null;
528
+ this._timeRangeStart = null;
529
+ this._timeRangeEnd = null;
530
+ this._timeRangeComplete = false;
523
531
  }
524
532
  get events() {
525
533
  return this._events;
@@ -548,6 +556,13 @@ var init_CalendarElement = __esm({
548
556
  this._renderNoEvents = val;
549
557
  if (this.engine) this.render();
550
558
  }
559
+ get loading() {
560
+ return this.hasAttribute("loading");
561
+ }
562
+ set loading(val) {
563
+ if (val) this.setAttribute("loading", "");
564
+ else this.removeAttribute("loading");
565
+ }
551
566
  connectedCallback() {
552
567
  this.classList.add("kalendly-calendar");
553
568
  this.initEngine();
@@ -558,8 +573,20 @@ var init_CalendarElement = __esm({
558
573
  this.classList.remove("kalendly-calendar");
559
574
  this.innerHTML = "";
560
575
  }
561
- attributeChangedCallback(_name, oldVal, newVal) {
576
+ attributeChangedCallback(name, oldVal, newVal) {
562
577
  if (oldVal === newVal) return;
578
+ if (name === "loading") {
579
+ this.render();
580
+ return;
581
+ }
582
+ if (name === "selectable" && newVal === null) {
583
+ this._rangeStart = null;
584
+ this._rangeEnd = null;
585
+ this._timeRangeDate = null;
586
+ this._timeRangeStart = null;
587
+ this._timeRangeEnd = null;
588
+ this._timeRangeComplete = false;
589
+ }
563
590
  this.reinit();
564
591
  }
565
592
  get minYear() {
@@ -651,6 +678,8 @@ var init_CalendarElement = __esm({
651
678
  root.style.setProperty("--calendar-today-outline", t.todayOutline);
652
679
  if (t.selectedBg)
653
680
  root.style.setProperty("--calendar-selected-bg", t.selectedBg);
681
+ if (t.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
682
+ if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
654
683
  if (t.eventIndicator)
655
684
  root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
656
685
  }
@@ -661,6 +690,9 @@ var init_CalendarElement = __esm({
661
690
  const title = this.getAttribute("title");
662
691
  const minYear = this.minYear;
663
692
  const maxYear = this.maxYear;
693
+ const availabilityMode = this.getAttribute("availability-mode");
694
+ const selectable = this.hasAttribute("selectable");
695
+ const isLoading = this.loading;
664
696
  const today = /* @__PURE__ */ new Date();
665
697
  const todayMonth = today.getMonth();
666
698
  const todayYear = today.getFullYear();
@@ -770,6 +802,37 @@ var init_CalendarElement = __esm({
770
802
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
771
803
  const renderEvent = this._renderEvent || defaultRenderEvent;
772
804
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
805
+ const renderTimeGrid = (events, date) => {
806
+ const isHourBooked = (hour) => events.some((event) => {
807
+ if (!event.startTime || !event.endTime) return true;
808
+ const [startH] = event.startTime.split(":").map(Number);
809
+ const [endH] = event.endTime.split(":").map(Number);
810
+ return hour >= startH && hour < endH;
811
+ });
812
+ const slots = Array.from({ length: 24 }, (_, hour) => {
813
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
814
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
815
+ const booked = isHourBooked(hour);
816
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
817
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
818
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
819
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
820
+ const slotClasses = [
821
+ "time-grid__slot",
822
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
823
+ isRangeStart ? "time-grid__slot--range-start" : "",
824
+ isRangeEnd ? "time-grid__slot--range-end" : "",
825
+ isInRange ? "time-grid__slot--in-range" : ""
826
+ ].filter(Boolean).join(" ");
827
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
828
+ return `
829
+ <div class="${slotClasses}" ${slotAttrs}>
830
+ <span class="time-grid__label">${startTime}</span>
831
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
832
+ </div>`;
833
+ });
834
+ return `<div class="time-grid">${slots.join("")}</div>`;
835
+ };
773
836
  const html = `
774
837
  ${title ? `
775
838
  <div class="page--title">
@@ -859,30 +922,52 @@ var init_CalendarElement = __esm({
859
922
  </tr>
860
923
  </thead>
861
924
  <tbody data-calendar-body>
862
- ${viewModel.calendarDates.map(
925
+ ${isLoading ? Array.from(
926
+ { length: 6 },
927
+ () => `<tr>${Array.from(
928
+ { length: 7 },
929
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
930
+ ).join("")}</tr>`
931
+ ).join("") : viewModel.calendarDates.map(
863
932
  (week) => `
864
- <tr>
865
- ${week.map((calendarDate, dayIndex) => {
933
+ <tr>
934
+ ${week.map((calendarDate, dayIndex) => {
866
935
  const classes = getCellClasses(calendarDate);
936
+ if (availabilityMode && calendarDate.isCurrentMonth) {
937
+ classes.push(
938
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
939
+ );
940
+ }
941
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
942
+ const d = calendarDate.date;
943
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
944
+ classes.push("availability--range-start");
945
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
946
+ classes.push("availability--range-end");
947
+ if (this._rangeStart && this._rangeEnd) {
948
+ if (d > this._rangeStart && d < this._rangeEnd)
949
+ classes.push("availability--in-range");
950
+ }
951
+ }
867
952
  const dateString = calendarDate.date.toISOString();
868
953
  return `
869
- <td
870
- class="${classes.join(" ")}"
871
- data-date="${dateString}"
872
- data-day-index="${dayIndex}"
873
- data-clickable="true"
874
- >
875
- ${calendarDate.date.getDate()}
876
- </td>
877
- `;
954
+ <td
955
+ class="${classes.join(" ")}"
956
+ data-date="${dateString}"
957
+ data-day-index="${dayIndex}"
958
+ data-clickable="true"
959
+ >
960
+ ${calendarDate.date.getDate()}
961
+ </td>
962
+ `;
878
963
  }).join("")}
879
- </tr>
880
- `
964
+ </tr>
965
+ `
881
966
  ).join("")}
882
967
  </tbody>
883
968
  </table>
884
969
 
885
- ${viewModel.selectedDate ? `
970
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
886
971
  <div class="date-popup ${viewModel.popupPositionClass}">
887
972
  <div class="popup-header">
888
973
  <h2>${viewModel.scheduleDay}</h2>
@@ -896,7 +981,7 @@ var init_CalendarElement = __esm({
896
981
  ` : ""}
897
982
 
898
983
  <div class="events-container">
899
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
984
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
900
985
  </div>
901
986
  </div>
902
987
  ` : ""}
@@ -918,6 +1003,59 @@ var init_CalendarElement = __esm({
918
1003
  e.stopPropagation();
919
1004
  const date = new Date(cell.dataset.date);
920
1005
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
1006
+ const availMode = this.getAttribute("availability-mode");
1007
+ const isSelectable = this.hasAttribute("selectable");
1008
+ if (availMode === "day" && isSelectable) {
1009
+ const isBooked = cell.classList.contains("availability--booked");
1010
+ if (!isBooked) {
1011
+ let startDate;
1012
+ let endDate;
1013
+ if (this._rangeEnd !== null) {
1014
+ this._rangeStart = date;
1015
+ this._rangeEnd = null;
1016
+ startDate = date;
1017
+ endDate = date;
1018
+ } else if (this._rangeStart === null) {
1019
+ this._rangeStart = date;
1020
+ startDate = date;
1021
+ endDate = date;
1022
+ } else {
1023
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
1024
+ const cursor = new Date(s);
1025
+ cursor.setDate(cursor.getDate() + 1);
1026
+ let blocked = false;
1027
+ while (cursor < e2) {
1028
+ if (this.engine.getEventsForDate(cursor).length > 0) {
1029
+ blocked = true;
1030
+ break;
1031
+ }
1032
+ cursor.setDate(cursor.getDate() + 1);
1033
+ }
1034
+ if (blocked) {
1035
+ this._rangeStart = date;
1036
+ this._rangeEnd = null;
1037
+ } else {
1038
+ this._rangeStart = s;
1039
+ this._rangeEnd = e2;
1040
+ }
1041
+ startDate = this._rangeStart;
1042
+ endDate = this._rangeEnd ?? this._rangeStart;
1043
+ }
1044
+ this.dispatchEvent(
1045
+ new CustomEvent("cal-availability-select", {
1046
+ bubbles: true,
1047
+ composed: true,
1048
+ detail: { startDate, endDate }
1049
+ })
1050
+ );
1051
+ }
1052
+ }
1053
+ if (availMode === "time") {
1054
+ this._timeRangeDate = null;
1055
+ this._timeRangeStart = null;
1056
+ this._timeRangeEnd = null;
1057
+ this._timeRangeComplete = false;
1058
+ }
921
1059
  this.engine.handleDateClick(date, dayIndex);
922
1060
  this.dispatchEvent(
923
1061
  new CustomEvent("cal-date-select", {
@@ -933,19 +1071,29 @@ var init_CalendarElement = __esm({
933
1071
  const action = actionEl.dataset.action;
934
1072
  e.stopPropagation();
935
1073
  switch (action) {
936
- case "previous":
1074
+ case "previous": {
1075
+ const vm = this.engine.getViewModel();
1076
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1077
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1078
+ this.dispatchMonthChange(tYear, tMonth);
937
1079
  this.actions.previous();
938
- this.dispatchMonthChange();
939
1080
  break;
940
- case "next":
1081
+ }
1082
+ case "next": {
1083
+ const vm = this.engine.getViewModel();
1084
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1085
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1086
+ this.dispatchMonthChange(tYear, tMonth);
941
1087
  this.actions.next();
942
- this.dispatchMonthChange();
943
1088
  break;
944
- case "today":
1089
+ }
1090
+ case "today": {
1091
+ const t = /* @__PURE__ */ new Date();
1092
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
945
1093
  this.actions.goToToday();
946
1094
  this.pickerOpen = false;
947
- this.dispatchMonthChange();
948
1095
  break;
1096
+ }
949
1097
  case "toggle-picker":
950
1098
  this.pickerOpen = !this.pickerOpen;
951
1099
  if (this.pickerOpen) {
@@ -960,8 +1108,8 @@ var init_CalendarElement = __esm({
960
1108
  const newYear = vm.currentYear - 1;
961
1109
  this.yearInput = String(newYear);
962
1110
  this.updatePickerYear(newYear);
1111
+ this.dispatchMonthChange(newYear, vm.currentMonth);
963
1112
  this.actions.jump(newYear, vm.currentMonth);
964
- this.dispatchMonthChange();
965
1113
  }
966
1114
  break;
967
1115
  }
@@ -971,21 +1119,60 @@ var init_CalendarElement = __esm({
971
1119
  const newYear = vm.currentYear + 1;
972
1120
  this.yearInput = String(newYear);
973
1121
  this.updatePickerYear(newYear);
1122
+ this.dispatchMonthChange(newYear, vm.currentMonth);
974
1123
  this.actions.jump(newYear, vm.currentMonth);
975
- this.dispatchMonthChange();
976
1124
  }
977
1125
  break;
978
1126
  }
979
1127
  case "select-month": {
980
1128
  const month = parseInt(actionEl.dataset.month || "0", 10);
981
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1129
+ const year = this.engine.getViewModel().currentYear;
1130
+ this.dispatchMonthChange(year, month);
1131
+ this.actions.jump(year, month);
982
1132
  this.pickerOpen = false;
983
- this.dispatchMonthChange();
984
1133
  break;
985
1134
  }
986
1135
  case "close-popup":
1136
+ this._timeRangeDate = null;
1137
+ this._timeRangeStart = null;
1138
+ this._timeRangeEnd = null;
1139
+ this._timeRangeComplete = false;
987
1140
  this.engine.clearSelection();
988
1141
  break;
1142
+ case "select-slot": {
1143
+ const slotStart = actionEl.dataset.startTime;
1144
+ const slotEnd = actionEl.dataset.endTime;
1145
+ const slotDate = new Date(actionEl.dataset.date);
1146
+ if (this._timeRangeComplete) {
1147
+ this._timeRangeDate = slotDate;
1148
+ this._timeRangeStart = slotStart;
1149
+ this._timeRangeEnd = slotEnd;
1150
+ this._timeRangeComplete = false;
1151
+ } else if (this._timeRangeStart === null) {
1152
+ this._timeRangeDate = slotDate;
1153
+ this._timeRangeStart = slotStart;
1154
+ this._timeRangeEnd = slotEnd;
1155
+ } else {
1156
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1157
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1158
+ this._timeRangeStart = newStart;
1159
+ this._timeRangeEnd = newEnd;
1160
+ this._timeRangeComplete = true;
1161
+ }
1162
+ this.render();
1163
+ this.dispatchEvent(
1164
+ new CustomEvent("cal-availability-select", {
1165
+ bubbles: true,
1166
+ composed: true,
1167
+ detail: {
1168
+ date: this._timeRangeDate,
1169
+ startTime: this._timeRangeStart,
1170
+ endTime: this._timeRangeEnd
1171
+ }
1172
+ })
1173
+ );
1174
+ break;
1175
+ }
989
1176
  }
990
1177
  };
991
1178
  this.delegatedInputHandler = (e) => {
@@ -1010,8 +1197,8 @@ var init_CalendarElement = __esm({
1010
1197
  target.value = this.yearInput;
1011
1198
  target.classList.remove("invalid");
1012
1199
  } else if (year !== vm.currentYear) {
1200
+ this.dispatchMonthChange(year, vm.currentMonth);
1013
1201
  this.actions.jump(year, vm.currentMonth);
1014
- this.dispatchMonthChange();
1015
1202
  }
1016
1203
  };
1017
1204
  this.delegatedKeydownHandler = (e) => {
@@ -1050,14 +1237,17 @@ var init_CalendarElement = __esm({
1050
1237
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
1051
1238
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
1052
1239
  }
1053
- dispatchMonthChange() {
1240
+ dispatchMonthChange(year, month) {
1054
1241
  if (!this.engine) return;
1055
1242
  const vm = this.engine.getViewModel();
1056
1243
  this.dispatchEvent(
1057
1244
  new CustomEvent("cal-month-change", {
1058
1245
  bubbles: true,
1059
1246
  composed: true,
1060
- detail: { year: vm.currentYear, month: vm.currentMonth }
1247
+ detail: {
1248
+ year: year ?? vm.currentYear,
1249
+ month: month ?? vm.currentMonth
1250
+ }
1061
1251
  })
1062
1252
  );
1063
1253
  }
@@ -1072,7 +1262,10 @@ var init_CalendarElement = __esm({
1072
1262
  return this.engine?.getViewModel().selectedDate ?? null;
1073
1263
  }
1074
1264
  goToDate(date) {
1075
- this.actions?.jump(date.getFullYear(), date.getMonth());
1265
+ const year = date.getFullYear();
1266
+ const month = date.getMonth();
1267
+ this.dispatchMonthChange(year, month);
1268
+ this.actions?.jump(year, month);
1076
1269
  }
1077
1270
  getEngine() {
1078
1271
  if (!this.engine)
@@ -1086,7 +1279,10 @@ var init_CalendarElement = __esm({
1086
1279
  "max-year",
1087
1280
  "week-starts-on",
1088
1281
  "title",
1089
- "use-short-month-names"
1282
+ "use-short-month-names",
1283
+ "availability-mode",
1284
+ "selectable",
1285
+ "loading"
1090
1286
  ];
1091
1287
  }
1092
1288
  });