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.umd.js CHANGED
@@ -438,6 +438,7 @@ var Kalendly = (() => {
438
438
  };
439
439
 
440
440
  // src/web-components/CalendarElement.ts
441
+ var isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
441
442
  var CalendarElement = class extends HTMLElement {
442
443
  constructor() {
443
444
  super(...arguments);
@@ -459,6 +460,13 @@ var Kalendly = (() => {
459
460
  this._categoryColors = null;
460
461
  this._renderEvent = null;
461
462
  this._renderNoEvents = null;
463
+ // Selection state (availability mode — range)
464
+ this._rangeStart = null;
465
+ this._rangeEnd = null;
466
+ this._timeRangeDate = null;
467
+ this._timeRangeStart = null;
468
+ this._timeRangeEnd = null;
469
+ this._timeRangeComplete = false;
462
470
  }
463
471
  get events() {
464
472
  return this._events;
@@ -487,6 +495,13 @@ var Kalendly = (() => {
487
495
  this._renderNoEvents = val;
488
496
  if (this.engine) this.render();
489
497
  }
498
+ get loading() {
499
+ return this.hasAttribute("loading");
500
+ }
501
+ set loading(val) {
502
+ if (val) this.setAttribute("loading", "");
503
+ else this.removeAttribute("loading");
504
+ }
490
505
  connectedCallback() {
491
506
  this.classList.add("kalendly-calendar");
492
507
  this.initEngine();
@@ -497,8 +512,20 @@ var Kalendly = (() => {
497
512
  this.classList.remove("kalendly-calendar");
498
513
  this.innerHTML = "";
499
514
  }
500
- attributeChangedCallback(_name, oldVal, newVal) {
515
+ attributeChangedCallback(name, oldVal, newVal) {
501
516
  if (oldVal === newVal) return;
517
+ if (name === "loading") {
518
+ this.render();
519
+ return;
520
+ }
521
+ if (name === "selectable" && newVal === null) {
522
+ this._rangeStart = null;
523
+ this._rangeEnd = null;
524
+ this._timeRangeDate = null;
525
+ this._timeRangeStart = null;
526
+ this._timeRangeEnd = null;
527
+ this._timeRangeComplete = false;
528
+ }
502
529
  this.reinit();
503
530
  }
504
531
  get minYear() {
@@ -600,6 +627,9 @@ var Kalendly = (() => {
600
627
  const title = this.getAttribute("title");
601
628
  const minYear = this.minYear;
602
629
  const maxYear = this.maxYear;
630
+ const availabilityMode = this.getAttribute("availability-mode");
631
+ const selectable = this.hasAttribute("selectable");
632
+ const isLoading = this.loading;
603
633
  const today = /* @__PURE__ */ new Date();
604
634
  const todayMonth = today.getMonth();
605
635
  const todayYear = today.getFullYear();
@@ -709,6 +739,37 @@ var Kalendly = (() => {
709
739
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
710
740
  const renderEvent = this._renderEvent || defaultRenderEvent;
711
741
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
742
+ const renderTimeGrid = (events, date) => {
743
+ const isHourBooked = (hour) => events.some((event) => {
744
+ if (!event.startTime || !event.endTime) return true;
745
+ const [startH] = event.startTime.split(":").map(Number);
746
+ const [endH] = event.endTime.split(":").map(Number);
747
+ return hour >= startH && hour < endH;
748
+ });
749
+ const slots = Array.from({ length: 24 }, (_, hour) => {
750
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
751
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
752
+ const booked = isHourBooked(hour);
753
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
754
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
755
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
756
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
757
+ const slotClasses = [
758
+ "time-grid__slot",
759
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
760
+ isRangeStart ? "time-grid__slot--range-start" : "",
761
+ isRangeEnd ? "time-grid__slot--range-end" : "",
762
+ isInRange ? "time-grid__slot--in-range" : ""
763
+ ].filter(Boolean).join(" ");
764
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
765
+ return `
766
+ <div class="${slotClasses}" ${slotAttrs}>
767
+ <span class="time-grid__label">${startTime}</span>
768
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
769
+ </div>`;
770
+ });
771
+ return `<div class="time-grid">${slots.join("")}</div>`;
772
+ };
712
773
  const html = `
713
774
  ${title ? `
714
775
  <div class="page--title">
@@ -798,30 +859,52 @@ var Kalendly = (() => {
798
859
  </tr>
799
860
  </thead>
800
861
  <tbody data-calendar-body>
801
- ${viewModel.calendarDates.map(
862
+ ${isLoading ? Array.from(
863
+ { length: 6 },
864
+ () => `<tr>${Array.from(
865
+ { length: 7 },
866
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
867
+ ).join("")}</tr>`
868
+ ).join("") : viewModel.calendarDates.map(
802
869
  (week) => `
803
- <tr>
804
- ${week.map((calendarDate, dayIndex) => {
870
+ <tr>
871
+ ${week.map((calendarDate, dayIndex) => {
805
872
  const classes = getCellClasses(calendarDate);
873
+ if (availabilityMode && calendarDate.isCurrentMonth) {
874
+ classes.push(
875
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
876
+ );
877
+ }
878
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
879
+ const d = calendarDate.date;
880
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
881
+ classes.push("availability--range-start");
882
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
883
+ classes.push("availability--range-end");
884
+ if (this._rangeStart && this._rangeEnd) {
885
+ if (d > this._rangeStart && d < this._rangeEnd)
886
+ classes.push("availability--in-range");
887
+ }
888
+ }
806
889
  const dateString = calendarDate.date.toISOString();
807
890
  return `
808
- <td
809
- class="${classes.join(" ")}"
810
- data-date="${dateString}"
811
- data-day-index="${dayIndex}"
812
- data-clickable="true"
813
- >
814
- ${calendarDate.date.getDate()}
815
- </td>
816
- `;
891
+ <td
892
+ class="${classes.join(" ")}"
893
+ data-date="${dateString}"
894
+ data-day-index="${dayIndex}"
895
+ data-clickable="true"
896
+ >
897
+ ${calendarDate.date.getDate()}
898
+ </td>
899
+ `;
817
900
  }).join("")}
818
- </tr>
819
- `
901
+ </tr>
902
+ `
820
903
  ).join("")}
821
904
  </tbody>
822
905
  </table>
823
906
 
824
- ${viewModel.selectedDate ? `
907
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
825
908
  <div class="date-popup ${viewModel.popupPositionClass}">
826
909
  <div class="popup-header">
827
910
  <h2>${viewModel.scheduleDay}</h2>
@@ -835,7 +918,7 @@ var Kalendly = (() => {
835
918
  ` : ""}
836
919
 
837
920
  <div class="events-container">
838
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
921
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
839
922
  </div>
840
923
  </div>
841
924
  ` : ""}
@@ -857,6 +940,59 @@ var Kalendly = (() => {
857
940
  e.stopPropagation();
858
941
  const date = new Date(cell.dataset.date);
859
942
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
943
+ const availMode = this.getAttribute("availability-mode");
944
+ const isSelectable = this.hasAttribute("selectable");
945
+ if (availMode === "day" && isSelectable) {
946
+ const isBooked = cell.classList.contains("availability--booked");
947
+ if (!isBooked) {
948
+ let startDate;
949
+ let endDate;
950
+ if (this._rangeEnd !== null) {
951
+ this._rangeStart = date;
952
+ this._rangeEnd = null;
953
+ startDate = date;
954
+ endDate = date;
955
+ } else if (this._rangeStart === null) {
956
+ this._rangeStart = date;
957
+ startDate = date;
958
+ endDate = date;
959
+ } else {
960
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
961
+ const cursor = new Date(s);
962
+ cursor.setDate(cursor.getDate() + 1);
963
+ let blocked = false;
964
+ while (cursor < e2) {
965
+ if (this.engine.getEventsForDate(cursor).length > 0) {
966
+ blocked = true;
967
+ break;
968
+ }
969
+ cursor.setDate(cursor.getDate() + 1);
970
+ }
971
+ if (blocked) {
972
+ this._rangeStart = date;
973
+ this._rangeEnd = null;
974
+ } else {
975
+ this._rangeStart = s;
976
+ this._rangeEnd = e2;
977
+ }
978
+ startDate = this._rangeStart;
979
+ endDate = this._rangeEnd ?? this._rangeStart;
980
+ }
981
+ this.dispatchEvent(
982
+ new CustomEvent("cal-availability-select", {
983
+ bubbles: true,
984
+ composed: true,
985
+ detail: { startDate, endDate }
986
+ })
987
+ );
988
+ }
989
+ }
990
+ if (availMode === "time") {
991
+ this._timeRangeDate = null;
992
+ this._timeRangeStart = null;
993
+ this._timeRangeEnd = null;
994
+ this._timeRangeComplete = false;
995
+ }
860
996
  this.engine.handleDateClick(date, dayIndex);
861
997
  this.dispatchEvent(
862
998
  new CustomEvent("cal-date-select", {
@@ -872,19 +1008,29 @@ var Kalendly = (() => {
872
1008
  const action = actionEl.dataset.action;
873
1009
  e.stopPropagation();
874
1010
  switch (action) {
875
- case "previous":
1011
+ case "previous": {
1012
+ const vm = this.engine.getViewModel();
1013
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1014
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1015
+ this.dispatchMonthChange(tYear, tMonth);
876
1016
  this.actions.previous();
877
- this.dispatchMonthChange();
878
1017
  break;
879
- case "next":
1018
+ }
1019
+ case "next": {
1020
+ const vm = this.engine.getViewModel();
1021
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1022
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1023
+ this.dispatchMonthChange(tYear, tMonth);
880
1024
  this.actions.next();
881
- this.dispatchMonthChange();
882
1025
  break;
883
- case "today":
1026
+ }
1027
+ case "today": {
1028
+ const t = /* @__PURE__ */ new Date();
1029
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
884
1030
  this.actions.goToToday();
885
1031
  this.pickerOpen = false;
886
- this.dispatchMonthChange();
887
1032
  break;
1033
+ }
888
1034
  case "toggle-picker":
889
1035
  this.pickerOpen = !this.pickerOpen;
890
1036
  if (this.pickerOpen) {
@@ -899,8 +1045,8 @@ var Kalendly = (() => {
899
1045
  const newYear = vm.currentYear - 1;
900
1046
  this.yearInput = String(newYear);
901
1047
  this.updatePickerYear(newYear);
1048
+ this.dispatchMonthChange(newYear, vm.currentMonth);
902
1049
  this.actions.jump(newYear, vm.currentMonth);
903
- this.dispatchMonthChange();
904
1050
  }
905
1051
  break;
906
1052
  }
@@ -910,21 +1056,60 @@ var Kalendly = (() => {
910
1056
  const newYear = vm.currentYear + 1;
911
1057
  this.yearInput = String(newYear);
912
1058
  this.updatePickerYear(newYear);
1059
+ this.dispatchMonthChange(newYear, vm.currentMonth);
913
1060
  this.actions.jump(newYear, vm.currentMonth);
914
- this.dispatchMonthChange();
915
1061
  }
916
1062
  break;
917
1063
  }
918
1064
  case "select-month": {
919
1065
  const month = parseInt(actionEl.dataset.month || "0", 10);
920
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1066
+ const year = this.engine.getViewModel().currentYear;
1067
+ this.dispatchMonthChange(year, month);
1068
+ this.actions.jump(year, month);
921
1069
  this.pickerOpen = false;
922
- this.dispatchMonthChange();
923
1070
  break;
924
1071
  }
925
1072
  case "close-popup":
1073
+ this._timeRangeDate = null;
1074
+ this._timeRangeStart = null;
1075
+ this._timeRangeEnd = null;
1076
+ this._timeRangeComplete = false;
926
1077
  this.engine.clearSelection();
927
1078
  break;
1079
+ case "select-slot": {
1080
+ const slotStart = actionEl.dataset.startTime;
1081
+ const slotEnd = actionEl.dataset.endTime;
1082
+ const slotDate = new Date(actionEl.dataset.date);
1083
+ if (this._timeRangeComplete) {
1084
+ this._timeRangeDate = slotDate;
1085
+ this._timeRangeStart = slotStart;
1086
+ this._timeRangeEnd = slotEnd;
1087
+ this._timeRangeComplete = false;
1088
+ } else if (this._timeRangeStart === null) {
1089
+ this._timeRangeDate = slotDate;
1090
+ this._timeRangeStart = slotStart;
1091
+ this._timeRangeEnd = slotEnd;
1092
+ } else {
1093
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1094
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1095
+ this._timeRangeStart = newStart;
1096
+ this._timeRangeEnd = newEnd;
1097
+ this._timeRangeComplete = true;
1098
+ }
1099
+ this.render();
1100
+ this.dispatchEvent(
1101
+ new CustomEvent("cal-availability-select", {
1102
+ bubbles: true,
1103
+ composed: true,
1104
+ detail: {
1105
+ date: this._timeRangeDate,
1106
+ startTime: this._timeRangeStart,
1107
+ endTime: this._timeRangeEnd
1108
+ }
1109
+ })
1110
+ );
1111
+ break;
1112
+ }
928
1113
  }
929
1114
  };
930
1115
  this.delegatedInputHandler = (e) => {
@@ -949,8 +1134,8 @@ var Kalendly = (() => {
949
1134
  target.value = this.yearInput;
950
1135
  target.classList.remove("invalid");
951
1136
  } else if (year !== vm.currentYear) {
1137
+ this.dispatchMonthChange(year, vm.currentMonth);
952
1138
  this.actions.jump(year, vm.currentMonth);
953
- this.dispatchMonthChange();
954
1139
  }
955
1140
  };
956
1141
  this.delegatedKeydownHandler = (e) => {
@@ -989,14 +1174,17 @@ var Kalendly = (() => {
989
1174
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
990
1175
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
991
1176
  }
992
- dispatchMonthChange() {
1177
+ dispatchMonthChange(year, month) {
993
1178
  if (!this.engine) return;
994
1179
  const vm = this.engine.getViewModel();
995
1180
  this.dispatchEvent(
996
1181
  new CustomEvent("cal-month-change", {
997
1182
  bubbles: true,
998
1183
  composed: true,
999
- detail: { year: vm.currentYear, month: vm.currentMonth }
1184
+ detail: {
1185
+ year: year ?? vm.currentYear,
1186
+ month: month ?? vm.currentMonth
1187
+ }
1000
1188
  })
1001
1189
  );
1002
1190
  }
@@ -1011,7 +1199,10 @@ var Kalendly = (() => {
1011
1199
  return this.engine?.getViewModel().selectedDate ?? null;
1012
1200
  }
1013
1201
  goToDate(date) {
1014
- this.actions?.jump(date.getFullYear(), date.getMonth());
1202
+ const year = date.getFullYear();
1203
+ const month = date.getMonth();
1204
+ this.dispatchMonthChange(year, month);
1205
+ this.actions?.jump(year, month);
1015
1206
  }
1016
1207
  getEngine() {
1017
1208
  if (!this.engine)
@@ -1025,7 +1216,10 @@ var Kalendly = (() => {
1025
1216
  "max-year",
1026
1217
  "week-starts-on",
1027
1218
  "title",
1028
- "use-short-month-names"
1219
+ "use-short-month-names",
1220
+ "availability-mode",
1221
+ "selectable",
1222
+ "loading"
1029
1223
  ];
1030
1224
  function defineCalendarElement(tagName = "kal-calendar") {
1031
1225
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {