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.mjs CHANGED
@@ -482,11 +482,12 @@ function defineCalendarElement(tagName = "kal-calendar") {
482
482
  customElements.define(tagName, CalendarElement);
483
483
  }
484
484
  }
485
- var CalendarElement;
485
+ var isSameDay2, CalendarElement;
486
486
  var init_CalendarElement = __esm({
487
487
  "src/web-components/CalendarElement.ts"() {
488
488
  "use strict";
489
489
  init_core();
490
+ isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
490
491
  CalendarElement = class extends HTMLElement {
491
492
  constructor() {
492
493
  super(...arguments);
@@ -508,6 +509,13 @@ var init_CalendarElement = __esm({
508
509
  this._categoryColors = null;
509
510
  this._renderEvent = null;
510
511
  this._renderNoEvents = null;
512
+ // Selection state (availability mode — range)
513
+ this._rangeStart = null;
514
+ this._rangeEnd = null;
515
+ this._timeRangeDate = null;
516
+ this._timeRangeStart = null;
517
+ this._timeRangeEnd = null;
518
+ this._timeRangeComplete = false;
511
519
  }
512
520
  get events() {
513
521
  return this._events;
@@ -536,6 +544,13 @@ var init_CalendarElement = __esm({
536
544
  this._renderNoEvents = val;
537
545
  if (this.engine) this.render();
538
546
  }
547
+ get loading() {
548
+ return this.hasAttribute("loading");
549
+ }
550
+ set loading(val) {
551
+ if (val) this.setAttribute("loading", "");
552
+ else this.removeAttribute("loading");
553
+ }
539
554
  connectedCallback() {
540
555
  this.classList.add("kalendly-calendar");
541
556
  this.initEngine();
@@ -546,8 +561,20 @@ var init_CalendarElement = __esm({
546
561
  this.classList.remove("kalendly-calendar");
547
562
  this.innerHTML = "";
548
563
  }
549
- attributeChangedCallback(_name, oldVal, newVal) {
564
+ attributeChangedCallback(name, oldVal, newVal) {
550
565
  if (oldVal === newVal) return;
566
+ if (name === "loading") {
567
+ this.render();
568
+ return;
569
+ }
570
+ if (name === "selectable" && newVal === null) {
571
+ this._rangeStart = null;
572
+ this._rangeEnd = null;
573
+ this._timeRangeDate = null;
574
+ this._timeRangeStart = null;
575
+ this._timeRangeEnd = null;
576
+ this._timeRangeComplete = false;
577
+ }
551
578
  this.reinit();
552
579
  }
553
580
  get minYear() {
@@ -649,6 +676,9 @@ var init_CalendarElement = __esm({
649
676
  const title = this.getAttribute("title");
650
677
  const minYear = this.minYear;
651
678
  const maxYear = this.maxYear;
679
+ const availabilityMode = this.getAttribute("availability-mode");
680
+ const selectable = this.hasAttribute("selectable");
681
+ const isLoading = this.loading;
652
682
  const today = /* @__PURE__ */ new Date();
653
683
  const todayMonth = today.getMonth();
654
684
  const todayYear = today.getFullYear();
@@ -758,6 +788,37 @@ var init_CalendarElement = __esm({
758
788
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
759
789
  const renderEvent = this._renderEvent || defaultRenderEvent;
760
790
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
791
+ const renderTimeGrid = (events, date) => {
792
+ const isHourBooked = (hour) => events.some((event) => {
793
+ if (!event.startTime || !event.endTime) return true;
794
+ const [startH] = event.startTime.split(":").map(Number);
795
+ const [endH] = event.endTime.split(":").map(Number);
796
+ return hour >= startH && hour < endH;
797
+ });
798
+ const slots = Array.from({ length: 24 }, (_, hour) => {
799
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
800
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
801
+ const booked = isHourBooked(hour);
802
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
803
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
804
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
805
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
806
+ const slotClasses = [
807
+ "time-grid__slot",
808
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
809
+ isRangeStart ? "time-grid__slot--range-start" : "",
810
+ isRangeEnd ? "time-grid__slot--range-end" : "",
811
+ isInRange ? "time-grid__slot--in-range" : ""
812
+ ].filter(Boolean).join(" ");
813
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
814
+ return `
815
+ <div class="${slotClasses}" ${slotAttrs}>
816
+ <span class="time-grid__label">${startTime}</span>
817
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
818
+ </div>`;
819
+ });
820
+ return `<div class="time-grid">${slots.join("")}</div>`;
821
+ };
761
822
  const html = `
762
823
  ${title ? `
763
824
  <div class="page--title">
@@ -847,30 +908,52 @@ var init_CalendarElement = __esm({
847
908
  </tr>
848
909
  </thead>
849
910
  <tbody data-calendar-body>
850
- ${viewModel.calendarDates.map(
911
+ ${isLoading ? Array.from(
912
+ { length: 6 },
913
+ () => `<tr>${Array.from(
914
+ { length: 7 },
915
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
916
+ ).join("")}</tr>`
917
+ ).join("") : viewModel.calendarDates.map(
851
918
  (week) => `
852
- <tr>
853
- ${week.map((calendarDate, dayIndex) => {
919
+ <tr>
920
+ ${week.map((calendarDate, dayIndex) => {
854
921
  const classes = getCellClasses(calendarDate);
922
+ if (availabilityMode && calendarDate.isCurrentMonth) {
923
+ classes.push(
924
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
925
+ );
926
+ }
927
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
928
+ const d = calendarDate.date;
929
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
930
+ classes.push("availability--range-start");
931
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
932
+ classes.push("availability--range-end");
933
+ if (this._rangeStart && this._rangeEnd) {
934
+ if (d > this._rangeStart && d < this._rangeEnd)
935
+ classes.push("availability--in-range");
936
+ }
937
+ }
855
938
  const dateString = calendarDate.date.toISOString();
856
939
  return `
857
- <td
858
- class="${classes.join(" ")}"
859
- data-date="${dateString}"
860
- data-day-index="${dayIndex}"
861
- data-clickable="true"
862
- >
863
- ${calendarDate.date.getDate()}
864
- </td>
865
- `;
940
+ <td
941
+ class="${classes.join(" ")}"
942
+ data-date="${dateString}"
943
+ data-day-index="${dayIndex}"
944
+ data-clickable="true"
945
+ >
946
+ ${calendarDate.date.getDate()}
947
+ </td>
948
+ `;
866
949
  }).join("")}
867
- </tr>
868
- `
950
+ </tr>
951
+ `
869
952
  ).join("")}
870
953
  </tbody>
871
954
  </table>
872
955
 
873
- ${viewModel.selectedDate ? `
956
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
874
957
  <div class="date-popup ${viewModel.popupPositionClass}">
875
958
  <div class="popup-header">
876
959
  <h2>${viewModel.scheduleDay}</h2>
@@ -884,7 +967,7 @@ var init_CalendarElement = __esm({
884
967
  ` : ""}
885
968
 
886
969
  <div class="events-container">
887
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
970
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
888
971
  </div>
889
972
  </div>
890
973
  ` : ""}
@@ -906,6 +989,59 @@ var init_CalendarElement = __esm({
906
989
  e.stopPropagation();
907
990
  const date = new Date(cell.dataset.date);
908
991
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
992
+ const availMode = this.getAttribute("availability-mode");
993
+ const isSelectable = this.hasAttribute("selectable");
994
+ if (availMode === "day" && isSelectable) {
995
+ const isBooked = cell.classList.contains("availability--booked");
996
+ if (!isBooked) {
997
+ let startDate;
998
+ let endDate;
999
+ if (this._rangeEnd !== null) {
1000
+ this._rangeStart = date;
1001
+ this._rangeEnd = null;
1002
+ startDate = date;
1003
+ endDate = date;
1004
+ } else if (this._rangeStart === null) {
1005
+ this._rangeStart = date;
1006
+ startDate = date;
1007
+ endDate = date;
1008
+ } else {
1009
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
1010
+ const cursor = new Date(s);
1011
+ cursor.setDate(cursor.getDate() + 1);
1012
+ let blocked = false;
1013
+ while (cursor < e2) {
1014
+ if (this.engine.getEventsForDate(cursor).length > 0) {
1015
+ blocked = true;
1016
+ break;
1017
+ }
1018
+ cursor.setDate(cursor.getDate() + 1);
1019
+ }
1020
+ if (blocked) {
1021
+ this._rangeStart = date;
1022
+ this._rangeEnd = null;
1023
+ } else {
1024
+ this._rangeStart = s;
1025
+ this._rangeEnd = e2;
1026
+ }
1027
+ startDate = this._rangeStart;
1028
+ endDate = this._rangeEnd ?? this._rangeStart;
1029
+ }
1030
+ this.dispatchEvent(
1031
+ new CustomEvent("cal-availability-select", {
1032
+ bubbles: true,
1033
+ composed: true,
1034
+ detail: { startDate, endDate }
1035
+ })
1036
+ );
1037
+ }
1038
+ }
1039
+ if (availMode === "time") {
1040
+ this._timeRangeDate = null;
1041
+ this._timeRangeStart = null;
1042
+ this._timeRangeEnd = null;
1043
+ this._timeRangeComplete = false;
1044
+ }
909
1045
  this.engine.handleDateClick(date, dayIndex);
910
1046
  this.dispatchEvent(
911
1047
  new CustomEvent("cal-date-select", {
@@ -921,19 +1057,29 @@ var init_CalendarElement = __esm({
921
1057
  const action = actionEl.dataset.action;
922
1058
  e.stopPropagation();
923
1059
  switch (action) {
924
- case "previous":
1060
+ case "previous": {
1061
+ const vm = this.engine.getViewModel();
1062
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1063
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1064
+ this.dispatchMonthChange(tYear, tMonth);
925
1065
  this.actions.previous();
926
- this.dispatchMonthChange();
927
1066
  break;
928
- case "next":
1067
+ }
1068
+ case "next": {
1069
+ const vm = this.engine.getViewModel();
1070
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1071
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1072
+ this.dispatchMonthChange(tYear, tMonth);
929
1073
  this.actions.next();
930
- this.dispatchMonthChange();
931
1074
  break;
932
- case "today":
1075
+ }
1076
+ case "today": {
1077
+ const t = /* @__PURE__ */ new Date();
1078
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
933
1079
  this.actions.goToToday();
934
1080
  this.pickerOpen = false;
935
- this.dispatchMonthChange();
936
1081
  break;
1082
+ }
937
1083
  case "toggle-picker":
938
1084
  this.pickerOpen = !this.pickerOpen;
939
1085
  if (this.pickerOpen) {
@@ -948,8 +1094,8 @@ var init_CalendarElement = __esm({
948
1094
  const newYear = vm.currentYear - 1;
949
1095
  this.yearInput = String(newYear);
950
1096
  this.updatePickerYear(newYear);
1097
+ this.dispatchMonthChange(newYear, vm.currentMonth);
951
1098
  this.actions.jump(newYear, vm.currentMonth);
952
- this.dispatchMonthChange();
953
1099
  }
954
1100
  break;
955
1101
  }
@@ -959,21 +1105,60 @@ var init_CalendarElement = __esm({
959
1105
  const newYear = vm.currentYear + 1;
960
1106
  this.yearInput = String(newYear);
961
1107
  this.updatePickerYear(newYear);
1108
+ this.dispatchMonthChange(newYear, vm.currentMonth);
962
1109
  this.actions.jump(newYear, vm.currentMonth);
963
- this.dispatchMonthChange();
964
1110
  }
965
1111
  break;
966
1112
  }
967
1113
  case "select-month": {
968
1114
  const month = parseInt(actionEl.dataset.month || "0", 10);
969
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1115
+ const year = this.engine.getViewModel().currentYear;
1116
+ this.dispatchMonthChange(year, month);
1117
+ this.actions.jump(year, month);
970
1118
  this.pickerOpen = false;
971
- this.dispatchMonthChange();
972
1119
  break;
973
1120
  }
974
1121
  case "close-popup":
1122
+ this._timeRangeDate = null;
1123
+ this._timeRangeStart = null;
1124
+ this._timeRangeEnd = null;
1125
+ this._timeRangeComplete = false;
975
1126
  this.engine.clearSelection();
976
1127
  break;
1128
+ case "select-slot": {
1129
+ const slotStart = actionEl.dataset.startTime;
1130
+ const slotEnd = actionEl.dataset.endTime;
1131
+ const slotDate = new Date(actionEl.dataset.date);
1132
+ if (this._timeRangeComplete) {
1133
+ this._timeRangeDate = slotDate;
1134
+ this._timeRangeStart = slotStart;
1135
+ this._timeRangeEnd = slotEnd;
1136
+ this._timeRangeComplete = false;
1137
+ } else if (this._timeRangeStart === null) {
1138
+ this._timeRangeDate = slotDate;
1139
+ this._timeRangeStart = slotStart;
1140
+ this._timeRangeEnd = slotEnd;
1141
+ } else {
1142
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1143
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1144
+ this._timeRangeStart = newStart;
1145
+ this._timeRangeEnd = newEnd;
1146
+ this._timeRangeComplete = true;
1147
+ }
1148
+ this.render();
1149
+ this.dispatchEvent(
1150
+ new CustomEvent("cal-availability-select", {
1151
+ bubbles: true,
1152
+ composed: true,
1153
+ detail: {
1154
+ date: this._timeRangeDate,
1155
+ startTime: this._timeRangeStart,
1156
+ endTime: this._timeRangeEnd
1157
+ }
1158
+ })
1159
+ );
1160
+ break;
1161
+ }
977
1162
  }
978
1163
  };
979
1164
  this.delegatedInputHandler = (e) => {
@@ -998,8 +1183,8 @@ var init_CalendarElement = __esm({
998
1183
  target.value = this.yearInput;
999
1184
  target.classList.remove("invalid");
1000
1185
  } else if (year !== vm.currentYear) {
1186
+ this.dispatchMonthChange(year, vm.currentMonth);
1001
1187
  this.actions.jump(year, vm.currentMonth);
1002
- this.dispatchMonthChange();
1003
1188
  }
1004
1189
  };
1005
1190
  this.delegatedKeydownHandler = (e) => {
@@ -1038,14 +1223,17 @@ var init_CalendarElement = __esm({
1038
1223
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
1039
1224
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
1040
1225
  }
1041
- dispatchMonthChange() {
1226
+ dispatchMonthChange(year, month) {
1042
1227
  if (!this.engine) return;
1043
1228
  const vm = this.engine.getViewModel();
1044
1229
  this.dispatchEvent(
1045
1230
  new CustomEvent("cal-month-change", {
1046
1231
  bubbles: true,
1047
1232
  composed: true,
1048
- detail: { year: vm.currentYear, month: vm.currentMonth }
1233
+ detail: {
1234
+ year: year ?? vm.currentYear,
1235
+ month: month ?? vm.currentMonth
1236
+ }
1049
1237
  })
1050
1238
  );
1051
1239
  }
@@ -1060,7 +1248,10 @@ var init_CalendarElement = __esm({
1060
1248
  return this.engine?.getViewModel().selectedDate ?? null;
1061
1249
  }
1062
1250
  goToDate(date) {
1063
- this.actions?.jump(date.getFullYear(), date.getMonth());
1251
+ const year = date.getFullYear();
1252
+ const month = date.getMonth();
1253
+ this.dispatchMonthChange(year, month);
1254
+ this.actions?.jump(year, month);
1064
1255
  }
1065
1256
  getEngine() {
1066
1257
  if (!this.engine)
@@ -1074,7 +1265,10 @@ var init_CalendarElement = __esm({
1074
1265
  "max-year",
1075
1266
  "week-starts-on",
1076
1267
  "title",
1077
- "use-short-month-names"
1268
+ "use-short-month-names",
1269
+ "availability-mode",
1270
+ "selectable",
1271
+ "loading"
1078
1272
  ];
1079
1273
  }
1080
1274
  });