kalendly 0.2.0 → 0.2.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/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() {
@@ -661,6 +688,9 @@ var init_CalendarElement = __esm({
661
688
  const title = this.getAttribute("title");
662
689
  const minYear = this.minYear;
663
690
  const maxYear = this.maxYear;
691
+ const availabilityMode = this.getAttribute("availability-mode");
692
+ const selectable = this.hasAttribute("selectable");
693
+ const isLoading = this.loading;
664
694
  const today = /* @__PURE__ */ new Date();
665
695
  const todayMonth = today.getMonth();
666
696
  const todayYear = today.getFullYear();
@@ -770,6 +800,37 @@ var init_CalendarElement = __esm({
770
800
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
771
801
  const renderEvent = this._renderEvent || defaultRenderEvent;
772
802
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
803
+ const renderTimeGrid = (events, date) => {
804
+ const isHourBooked = (hour) => events.some((event) => {
805
+ if (!event.startTime || !event.endTime) return true;
806
+ const [startH] = event.startTime.split(":").map(Number);
807
+ const [endH] = event.endTime.split(":").map(Number);
808
+ return hour >= startH && hour < endH;
809
+ });
810
+ const slots = Array.from({ length: 24 }, (_, hour) => {
811
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
812
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
813
+ const booked = isHourBooked(hour);
814
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
815
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
816
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
817
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
818
+ const slotClasses = [
819
+ "time-grid__slot",
820
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
821
+ isRangeStart ? "time-grid__slot--range-start" : "",
822
+ isRangeEnd ? "time-grid__slot--range-end" : "",
823
+ isInRange ? "time-grid__slot--in-range" : ""
824
+ ].filter(Boolean).join(" ");
825
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
826
+ return `
827
+ <div class="${slotClasses}" ${slotAttrs}>
828
+ <span class="time-grid__label">${startTime}</span>
829
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
830
+ </div>`;
831
+ });
832
+ return `<div class="time-grid">${slots.join("")}</div>`;
833
+ };
773
834
  const html = `
774
835
  ${title ? `
775
836
  <div class="page--title">
@@ -859,30 +920,52 @@ var init_CalendarElement = __esm({
859
920
  </tr>
860
921
  </thead>
861
922
  <tbody data-calendar-body>
862
- ${viewModel.calendarDates.map(
923
+ ${isLoading ? Array.from(
924
+ { length: 6 },
925
+ () => `<tr>${Array.from(
926
+ { length: 7 },
927
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
928
+ ).join("")}</tr>`
929
+ ).join("") : viewModel.calendarDates.map(
863
930
  (week) => `
864
- <tr>
865
- ${week.map((calendarDate, dayIndex) => {
931
+ <tr>
932
+ ${week.map((calendarDate, dayIndex) => {
866
933
  const classes = getCellClasses(calendarDate);
934
+ if (availabilityMode && calendarDate.isCurrentMonth) {
935
+ classes.push(
936
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
937
+ );
938
+ }
939
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
940
+ const d = calendarDate.date;
941
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
942
+ classes.push("availability--range-start");
943
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
944
+ classes.push("availability--range-end");
945
+ if (this._rangeStart && this._rangeEnd) {
946
+ if (d > this._rangeStart && d < this._rangeEnd)
947
+ classes.push("availability--in-range");
948
+ }
949
+ }
867
950
  const dateString = calendarDate.date.toISOString();
868
951
  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
- `;
952
+ <td
953
+ class="${classes.join(" ")}"
954
+ data-date="${dateString}"
955
+ data-day-index="${dayIndex}"
956
+ data-clickable="true"
957
+ >
958
+ ${calendarDate.date.getDate()}
959
+ </td>
960
+ `;
878
961
  }).join("")}
879
- </tr>
880
- `
962
+ </tr>
963
+ `
881
964
  ).join("")}
882
965
  </tbody>
883
966
  </table>
884
967
 
885
- ${viewModel.selectedDate ? `
968
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
886
969
  <div class="date-popup ${viewModel.popupPositionClass}">
887
970
  <div class="popup-header">
888
971
  <h2>${viewModel.scheduleDay}</h2>
@@ -896,7 +979,7 @@ var init_CalendarElement = __esm({
896
979
  ` : ""}
897
980
 
898
981
  <div class="events-container">
899
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
982
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
900
983
  </div>
901
984
  </div>
902
985
  ` : ""}
@@ -918,6 +1001,59 @@ var init_CalendarElement = __esm({
918
1001
  e.stopPropagation();
919
1002
  const date = new Date(cell.dataset.date);
920
1003
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
1004
+ const availMode = this.getAttribute("availability-mode");
1005
+ const isSelectable = this.hasAttribute("selectable");
1006
+ if (availMode === "day" && isSelectable) {
1007
+ const isBooked = cell.classList.contains("availability--booked");
1008
+ if (!isBooked) {
1009
+ let startDate;
1010
+ let endDate;
1011
+ if (this._rangeEnd !== null) {
1012
+ this._rangeStart = date;
1013
+ this._rangeEnd = null;
1014
+ startDate = date;
1015
+ endDate = date;
1016
+ } else if (this._rangeStart === null) {
1017
+ this._rangeStart = date;
1018
+ startDate = date;
1019
+ endDate = date;
1020
+ } else {
1021
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
1022
+ const cursor = new Date(s);
1023
+ cursor.setDate(cursor.getDate() + 1);
1024
+ let blocked = false;
1025
+ while (cursor < e2) {
1026
+ if (this.engine.getEventsForDate(cursor).length > 0) {
1027
+ blocked = true;
1028
+ break;
1029
+ }
1030
+ cursor.setDate(cursor.getDate() + 1);
1031
+ }
1032
+ if (blocked) {
1033
+ this._rangeStart = date;
1034
+ this._rangeEnd = null;
1035
+ } else {
1036
+ this._rangeStart = s;
1037
+ this._rangeEnd = e2;
1038
+ }
1039
+ startDate = this._rangeStart;
1040
+ endDate = this._rangeEnd ?? this._rangeStart;
1041
+ }
1042
+ this.dispatchEvent(
1043
+ new CustomEvent("cal-availability-select", {
1044
+ bubbles: true,
1045
+ composed: true,
1046
+ detail: { startDate, endDate }
1047
+ })
1048
+ );
1049
+ }
1050
+ }
1051
+ if (availMode === "time") {
1052
+ this._timeRangeDate = null;
1053
+ this._timeRangeStart = null;
1054
+ this._timeRangeEnd = null;
1055
+ this._timeRangeComplete = false;
1056
+ }
921
1057
  this.engine.handleDateClick(date, dayIndex);
922
1058
  this.dispatchEvent(
923
1059
  new CustomEvent("cal-date-select", {
@@ -933,19 +1069,29 @@ var init_CalendarElement = __esm({
933
1069
  const action = actionEl.dataset.action;
934
1070
  e.stopPropagation();
935
1071
  switch (action) {
936
- case "previous":
1072
+ case "previous": {
1073
+ const vm = this.engine.getViewModel();
1074
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1075
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1076
+ this.dispatchMonthChange(tYear, tMonth);
937
1077
  this.actions.previous();
938
- this.dispatchMonthChange();
939
1078
  break;
940
- case "next":
1079
+ }
1080
+ case "next": {
1081
+ const vm = this.engine.getViewModel();
1082
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1083
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1084
+ this.dispatchMonthChange(tYear, tMonth);
941
1085
  this.actions.next();
942
- this.dispatchMonthChange();
943
1086
  break;
944
- case "today":
1087
+ }
1088
+ case "today": {
1089
+ const t = /* @__PURE__ */ new Date();
1090
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
945
1091
  this.actions.goToToday();
946
1092
  this.pickerOpen = false;
947
- this.dispatchMonthChange();
948
1093
  break;
1094
+ }
949
1095
  case "toggle-picker":
950
1096
  this.pickerOpen = !this.pickerOpen;
951
1097
  if (this.pickerOpen) {
@@ -960,8 +1106,8 @@ var init_CalendarElement = __esm({
960
1106
  const newYear = vm.currentYear - 1;
961
1107
  this.yearInput = String(newYear);
962
1108
  this.updatePickerYear(newYear);
1109
+ this.dispatchMonthChange(newYear, vm.currentMonth);
963
1110
  this.actions.jump(newYear, vm.currentMonth);
964
- this.dispatchMonthChange();
965
1111
  }
966
1112
  break;
967
1113
  }
@@ -971,21 +1117,60 @@ var init_CalendarElement = __esm({
971
1117
  const newYear = vm.currentYear + 1;
972
1118
  this.yearInput = String(newYear);
973
1119
  this.updatePickerYear(newYear);
1120
+ this.dispatchMonthChange(newYear, vm.currentMonth);
974
1121
  this.actions.jump(newYear, vm.currentMonth);
975
- this.dispatchMonthChange();
976
1122
  }
977
1123
  break;
978
1124
  }
979
1125
  case "select-month": {
980
1126
  const month = parseInt(actionEl.dataset.month || "0", 10);
981
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1127
+ const year = this.engine.getViewModel().currentYear;
1128
+ this.dispatchMonthChange(year, month);
1129
+ this.actions.jump(year, month);
982
1130
  this.pickerOpen = false;
983
- this.dispatchMonthChange();
984
1131
  break;
985
1132
  }
986
1133
  case "close-popup":
1134
+ this._timeRangeDate = null;
1135
+ this._timeRangeStart = null;
1136
+ this._timeRangeEnd = null;
1137
+ this._timeRangeComplete = false;
987
1138
  this.engine.clearSelection();
988
1139
  break;
1140
+ case "select-slot": {
1141
+ const slotStart = actionEl.dataset.startTime;
1142
+ const slotEnd = actionEl.dataset.endTime;
1143
+ const slotDate = new Date(actionEl.dataset.date);
1144
+ if (this._timeRangeComplete) {
1145
+ this._timeRangeDate = slotDate;
1146
+ this._timeRangeStart = slotStart;
1147
+ this._timeRangeEnd = slotEnd;
1148
+ this._timeRangeComplete = false;
1149
+ } else if (this._timeRangeStart === null) {
1150
+ this._timeRangeDate = slotDate;
1151
+ this._timeRangeStart = slotStart;
1152
+ this._timeRangeEnd = slotEnd;
1153
+ } else {
1154
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1155
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1156
+ this._timeRangeStart = newStart;
1157
+ this._timeRangeEnd = newEnd;
1158
+ this._timeRangeComplete = true;
1159
+ }
1160
+ this.render();
1161
+ this.dispatchEvent(
1162
+ new CustomEvent("cal-availability-select", {
1163
+ bubbles: true,
1164
+ composed: true,
1165
+ detail: {
1166
+ date: this._timeRangeDate,
1167
+ startTime: this._timeRangeStart,
1168
+ endTime: this._timeRangeEnd
1169
+ }
1170
+ })
1171
+ );
1172
+ break;
1173
+ }
989
1174
  }
990
1175
  };
991
1176
  this.delegatedInputHandler = (e) => {
@@ -1010,8 +1195,8 @@ var init_CalendarElement = __esm({
1010
1195
  target.value = this.yearInput;
1011
1196
  target.classList.remove("invalid");
1012
1197
  } else if (year !== vm.currentYear) {
1198
+ this.dispatchMonthChange(year, vm.currentMonth);
1013
1199
  this.actions.jump(year, vm.currentMonth);
1014
- this.dispatchMonthChange();
1015
1200
  }
1016
1201
  };
1017
1202
  this.delegatedKeydownHandler = (e) => {
@@ -1050,14 +1235,17 @@ var init_CalendarElement = __esm({
1050
1235
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
1051
1236
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
1052
1237
  }
1053
- dispatchMonthChange() {
1238
+ dispatchMonthChange(year, month) {
1054
1239
  if (!this.engine) return;
1055
1240
  const vm = this.engine.getViewModel();
1056
1241
  this.dispatchEvent(
1057
1242
  new CustomEvent("cal-month-change", {
1058
1243
  bubbles: true,
1059
1244
  composed: true,
1060
- detail: { year: vm.currentYear, month: vm.currentMonth }
1245
+ detail: {
1246
+ year: year ?? vm.currentYear,
1247
+ month: month ?? vm.currentMonth
1248
+ }
1061
1249
  })
1062
1250
  );
1063
1251
  }
@@ -1072,7 +1260,10 @@ var init_CalendarElement = __esm({
1072
1260
  return this.engine?.getViewModel().selectedDate ?? null;
1073
1261
  }
1074
1262
  goToDate(date) {
1075
- this.actions?.jump(date.getFullYear(), date.getMonth());
1263
+ const year = date.getFullYear();
1264
+ const month = date.getMonth();
1265
+ this.dispatchMonthChange(year, month);
1266
+ this.actions?.jump(year, month);
1076
1267
  }
1077
1268
  getEngine() {
1078
1269
  if (!this.engine)
@@ -1086,7 +1277,10 @@ var init_CalendarElement = __esm({
1086
1277
  "max-year",
1087
1278
  "week-starts-on",
1088
1279
  "title",
1089
- "use-short-month-names"
1280
+ "use-short-month-names",
1281
+ "availability-mode",
1282
+ "selectable",
1283
+ "loading"
1090
1284
  ];
1091
1285
  }
1092
1286
  });