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.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() {
@@ -639,6 +666,8 @@ var init_CalendarElement = __esm({
639
666
  root.style.setProperty("--calendar-today-outline", t.todayOutline);
640
667
  if (t.selectedBg)
641
668
  root.style.setProperty("--calendar-selected-bg", t.selectedBg);
669
+ if (t.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
670
+ if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
642
671
  if (t.eventIndicator)
643
672
  root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
644
673
  }
@@ -649,6 +678,9 @@ var init_CalendarElement = __esm({
649
678
  const title = this.getAttribute("title");
650
679
  const minYear = this.minYear;
651
680
  const maxYear = this.maxYear;
681
+ const availabilityMode = this.getAttribute("availability-mode");
682
+ const selectable = this.hasAttribute("selectable");
683
+ const isLoading = this.loading;
652
684
  const today = /* @__PURE__ */ new Date();
653
685
  const todayMonth = today.getMonth();
654
686
  const todayYear = today.getFullYear();
@@ -758,6 +790,37 @@ var init_CalendarElement = __esm({
758
790
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
759
791
  const renderEvent = this._renderEvent || defaultRenderEvent;
760
792
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
793
+ const renderTimeGrid = (events, date) => {
794
+ const isHourBooked = (hour) => events.some((event) => {
795
+ if (!event.startTime || !event.endTime) return true;
796
+ const [startH] = event.startTime.split(":").map(Number);
797
+ const [endH] = event.endTime.split(":").map(Number);
798
+ return hour >= startH && hour < endH;
799
+ });
800
+ const slots = Array.from({ length: 24 }, (_, hour) => {
801
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
802
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
803
+ const booked = isHourBooked(hour);
804
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
805
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
806
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
807
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
808
+ const slotClasses = [
809
+ "time-grid__slot",
810
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
811
+ isRangeStart ? "time-grid__slot--range-start" : "",
812
+ isRangeEnd ? "time-grid__slot--range-end" : "",
813
+ isInRange ? "time-grid__slot--in-range" : ""
814
+ ].filter(Boolean).join(" ");
815
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
816
+ return `
817
+ <div class="${slotClasses}" ${slotAttrs}>
818
+ <span class="time-grid__label">${startTime}</span>
819
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
820
+ </div>`;
821
+ });
822
+ return `<div class="time-grid">${slots.join("")}</div>`;
823
+ };
761
824
  const html = `
762
825
  ${title ? `
763
826
  <div class="page--title">
@@ -847,30 +910,52 @@ var init_CalendarElement = __esm({
847
910
  </tr>
848
911
  </thead>
849
912
  <tbody data-calendar-body>
850
- ${viewModel.calendarDates.map(
913
+ ${isLoading ? Array.from(
914
+ { length: 6 },
915
+ () => `<tr>${Array.from(
916
+ { length: 7 },
917
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
918
+ ).join("")}</tr>`
919
+ ).join("") : viewModel.calendarDates.map(
851
920
  (week) => `
852
- <tr>
853
- ${week.map((calendarDate, dayIndex) => {
921
+ <tr>
922
+ ${week.map((calendarDate, dayIndex) => {
854
923
  const classes = getCellClasses(calendarDate);
924
+ if (availabilityMode && calendarDate.isCurrentMonth) {
925
+ classes.push(
926
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
927
+ );
928
+ }
929
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
930
+ const d = calendarDate.date;
931
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
932
+ classes.push("availability--range-start");
933
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
934
+ classes.push("availability--range-end");
935
+ if (this._rangeStart && this._rangeEnd) {
936
+ if (d > this._rangeStart && d < this._rangeEnd)
937
+ classes.push("availability--in-range");
938
+ }
939
+ }
855
940
  const dateString = calendarDate.date.toISOString();
856
941
  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
- `;
942
+ <td
943
+ class="${classes.join(" ")}"
944
+ data-date="${dateString}"
945
+ data-day-index="${dayIndex}"
946
+ data-clickable="true"
947
+ >
948
+ ${calendarDate.date.getDate()}
949
+ </td>
950
+ `;
866
951
  }).join("")}
867
- </tr>
868
- `
952
+ </tr>
953
+ `
869
954
  ).join("")}
870
955
  </tbody>
871
956
  </table>
872
957
 
873
- ${viewModel.selectedDate ? `
958
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
874
959
  <div class="date-popup ${viewModel.popupPositionClass}">
875
960
  <div class="popup-header">
876
961
  <h2>${viewModel.scheduleDay}</h2>
@@ -884,7 +969,7 @@ var init_CalendarElement = __esm({
884
969
  ` : ""}
885
970
 
886
971
  <div class="events-container">
887
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
972
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
888
973
  </div>
889
974
  </div>
890
975
  ` : ""}
@@ -906,6 +991,59 @@ var init_CalendarElement = __esm({
906
991
  e.stopPropagation();
907
992
  const date = new Date(cell.dataset.date);
908
993
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
994
+ const availMode = this.getAttribute("availability-mode");
995
+ const isSelectable = this.hasAttribute("selectable");
996
+ if (availMode === "day" && isSelectable) {
997
+ const isBooked = cell.classList.contains("availability--booked");
998
+ if (!isBooked) {
999
+ let startDate;
1000
+ let endDate;
1001
+ if (this._rangeEnd !== null) {
1002
+ this._rangeStart = date;
1003
+ this._rangeEnd = null;
1004
+ startDate = date;
1005
+ endDate = date;
1006
+ } else if (this._rangeStart === null) {
1007
+ this._rangeStart = date;
1008
+ startDate = date;
1009
+ endDate = date;
1010
+ } else {
1011
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
1012
+ const cursor = new Date(s);
1013
+ cursor.setDate(cursor.getDate() + 1);
1014
+ let blocked = false;
1015
+ while (cursor < e2) {
1016
+ if (this.engine.getEventsForDate(cursor).length > 0) {
1017
+ blocked = true;
1018
+ break;
1019
+ }
1020
+ cursor.setDate(cursor.getDate() + 1);
1021
+ }
1022
+ if (blocked) {
1023
+ this._rangeStart = date;
1024
+ this._rangeEnd = null;
1025
+ } else {
1026
+ this._rangeStart = s;
1027
+ this._rangeEnd = e2;
1028
+ }
1029
+ startDate = this._rangeStart;
1030
+ endDate = this._rangeEnd ?? this._rangeStart;
1031
+ }
1032
+ this.dispatchEvent(
1033
+ new CustomEvent("cal-availability-select", {
1034
+ bubbles: true,
1035
+ composed: true,
1036
+ detail: { startDate, endDate }
1037
+ })
1038
+ );
1039
+ }
1040
+ }
1041
+ if (availMode === "time") {
1042
+ this._timeRangeDate = null;
1043
+ this._timeRangeStart = null;
1044
+ this._timeRangeEnd = null;
1045
+ this._timeRangeComplete = false;
1046
+ }
909
1047
  this.engine.handleDateClick(date, dayIndex);
910
1048
  this.dispatchEvent(
911
1049
  new CustomEvent("cal-date-select", {
@@ -921,19 +1059,29 @@ var init_CalendarElement = __esm({
921
1059
  const action = actionEl.dataset.action;
922
1060
  e.stopPropagation();
923
1061
  switch (action) {
924
- case "previous":
1062
+ case "previous": {
1063
+ const vm = this.engine.getViewModel();
1064
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1065
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1066
+ this.dispatchMonthChange(tYear, tMonth);
925
1067
  this.actions.previous();
926
- this.dispatchMonthChange();
927
1068
  break;
928
- case "next":
1069
+ }
1070
+ case "next": {
1071
+ const vm = this.engine.getViewModel();
1072
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1073
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1074
+ this.dispatchMonthChange(tYear, tMonth);
929
1075
  this.actions.next();
930
- this.dispatchMonthChange();
931
1076
  break;
932
- case "today":
1077
+ }
1078
+ case "today": {
1079
+ const t = /* @__PURE__ */ new Date();
1080
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
933
1081
  this.actions.goToToday();
934
1082
  this.pickerOpen = false;
935
- this.dispatchMonthChange();
936
1083
  break;
1084
+ }
937
1085
  case "toggle-picker":
938
1086
  this.pickerOpen = !this.pickerOpen;
939
1087
  if (this.pickerOpen) {
@@ -948,8 +1096,8 @@ var init_CalendarElement = __esm({
948
1096
  const newYear = vm.currentYear - 1;
949
1097
  this.yearInput = String(newYear);
950
1098
  this.updatePickerYear(newYear);
1099
+ this.dispatchMonthChange(newYear, vm.currentMonth);
951
1100
  this.actions.jump(newYear, vm.currentMonth);
952
- this.dispatchMonthChange();
953
1101
  }
954
1102
  break;
955
1103
  }
@@ -959,21 +1107,60 @@ var init_CalendarElement = __esm({
959
1107
  const newYear = vm.currentYear + 1;
960
1108
  this.yearInput = String(newYear);
961
1109
  this.updatePickerYear(newYear);
1110
+ this.dispatchMonthChange(newYear, vm.currentMonth);
962
1111
  this.actions.jump(newYear, vm.currentMonth);
963
- this.dispatchMonthChange();
964
1112
  }
965
1113
  break;
966
1114
  }
967
1115
  case "select-month": {
968
1116
  const month = parseInt(actionEl.dataset.month || "0", 10);
969
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1117
+ const year = this.engine.getViewModel().currentYear;
1118
+ this.dispatchMonthChange(year, month);
1119
+ this.actions.jump(year, month);
970
1120
  this.pickerOpen = false;
971
- this.dispatchMonthChange();
972
1121
  break;
973
1122
  }
974
1123
  case "close-popup":
1124
+ this._timeRangeDate = null;
1125
+ this._timeRangeStart = null;
1126
+ this._timeRangeEnd = null;
1127
+ this._timeRangeComplete = false;
975
1128
  this.engine.clearSelection();
976
1129
  break;
1130
+ case "select-slot": {
1131
+ const slotStart = actionEl.dataset.startTime;
1132
+ const slotEnd = actionEl.dataset.endTime;
1133
+ const slotDate = new Date(actionEl.dataset.date);
1134
+ if (this._timeRangeComplete) {
1135
+ this._timeRangeDate = slotDate;
1136
+ this._timeRangeStart = slotStart;
1137
+ this._timeRangeEnd = slotEnd;
1138
+ this._timeRangeComplete = false;
1139
+ } else if (this._timeRangeStart === null) {
1140
+ this._timeRangeDate = slotDate;
1141
+ this._timeRangeStart = slotStart;
1142
+ this._timeRangeEnd = slotEnd;
1143
+ } else {
1144
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1145
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1146
+ this._timeRangeStart = newStart;
1147
+ this._timeRangeEnd = newEnd;
1148
+ this._timeRangeComplete = true;
1149
+ }
1150
+ this.render();
1151
+ this.dispatchEvent(
1152
+ new CustomEvent("cal-availability-select", {
1153
+ bubbles: true,
1154
+ composed: true,
1155
+ detail: {
1156
+ date: this._timeRangeDate,
1157
+ startTime: this._timeRangeStart,
1158
+ endTime: this._timeRangeEnd
1159
+ }
1160
+ })
1161
+ );
1162
+ break;
1163
+ }
977
1164
  }
978
1165
  };
979
1166
  this.delegatedInputHandler = (e) => {
@@ -998,8 +1185,8 @@ var init_CalendarElement = __esm({
998
1185
  target.value = this.yearInput;
999
1186
  target.classList.remove("invalid");
1000
1187
  } else if (year !== vm.currentYear) {
1188
+ this.dispatchMonthChange(year, vm.currentMonth);
1001
1189
  this.actions.jump(year, vm.currentMonth);
1002
- this.dispatchMonthChange();
1003
1190
  }
1004
1191
  };
1005
1192
  this.delegatedKeydownHandler = (e) => {
@@ -1038,14 +1225,17 @@ var init_CalendarElement = __esm({
1038
1225
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
1039
1226
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
1040
1227
  }
1041
- dispatchMonthChange() {
1228
+ dispatchMonthChange(year, month) {
1042
1229
  if (!this.engine) return;
1043
1230
  const vm = this.engine.getViewModel();
1044
1231
  this.dispatchEvent(
1045
1232
  new CustomEvent("cal-month-change", {
1046
1233
  bubbles: true,
1047
1234
  composed: true,
1048
- detail: { year: vm.currentYear, month: vm.currentMonth }
1235
+ detail: {
1236
+ year: year ?? vm.currentYear,
1237
+ month: month ?? vm.currentMonth
1238
+ }
1049
1239
  })
1050
1240
  );
1051
1241
  }
@@ -1060,7 +1250,10 @@ var init_CalendarElement = __esm({
1060
1250
  return this.engine?.getViewModel().selectedDate ?? null;
1061
1251
  }
1062
1252
  goToDate(date) {
1063
- this.actions?.jump(date.getFullYear(), date.getMonth());
1253
+ const year = date.getFullYear();
1254
+ const month = date.getMonth();
1255
+ this.dispatchMonthChange(year, month);
1256
+ this.actions?.jump(year, month);
1064
1257
  }
1065
1258
  getEngine() {
1066
1259
  if (!this.engine)
@@ -1074,7 +1267,10 @@ var init_CalendarElement = __esm({
1074
1267
  "max-year",
1075
1268
  "week-starts-on",
1076
1269
  "title",
1077
- "use-short-month-names"
1270
+ "use-short-month-names",
1271
+ "availability-mode",
1272
+ "selectable",
1273
+ "loading"
1078
1274
  ];
1079
1275
  }
1080
1276
  });