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.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() {
@@ -590,6 +617,8 @@ var Kalendly = (() => {
590
617
  root.style.setProperty("--calendar-today-outline", t.todayOutline);
591
618
  if (t.selectedBg)
592
619
  root.style.setProperty("--calendar-selected-bg", t.selectedBg);
620
+ if (t.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
621
+ if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
593
622
  if (t.eventIndicator)
594
623
  root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
595
624
  }
@@ -600,6 +629,9 @@ var Kalendly = (() => {
600
629
  const title = this.getAttribute("title");
601
630
  const minYear = this.minYear;
602
631
  const maxYear = this.maxYear;
632
+ const availabilityMode = this.getAttribute("availability-mode");
633
+ const selectable = this.hasAttribute("selectable");
634
+ const isLoading = this.loading;
603
635
  const today = /* @__PURE__ */ new Date();
604
636
  const todayMonth = today.getMonth();
605
637
  const todayYear = today.getFullYear();
@@ -709,6 +741,37 @@ var Kalendly = (() => {
709
741
  const defaultRenderNoEvents = () => '<div class="no-events-message">No events scheduled for this day.</div>';
710
742
  const renderEvent = this._renderEvent || defaultRenderEvent;
711
743
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
744
+ const renderTimeGrid = (events, date) => {
745
+ const isHourBooked = (hour) => events.some((event) => {
746
+ if (!event.startTime || !event.endTime) return true;
747
+ const [startH] = event.startTime.split(":").map(Number);
748
+ const [endH] = event.endTime.split(":").map(Number);
749
+ return hour >= startH && hour < endH;
750
+ });
751
+ const slots = Array.from({ length: 24 }, (_, hour) => {
752
+ const startTime = `${String(hour).padStart(2, "0")}:00`;
753
+ const endTime = `${String(hour + 1).padStart(2, "0")}:00`;
754
+ const booked = isHourBooked(hour);
755
+ const inTimeRange = !booked && selectable && this._timeRangeStart !== null && this._timeRangeEnd !== null && isSameDay2(this._timeRangeDate, date) && startTime >= this._timeRangeStart && endTime <= this._timeRangeEnd;
756
+ const isRangeStart = inTimeRange && startTime === this._timeRangeStart;
757
+ const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
758
+ const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
759
+ const slotClasses = [
760
+ "time-grid__slot",
761
+ booked ? "time-grid__slot--booked" : "time-grid__slot--free",
762
+ isRangeStart ? "time-grid__slot--range-start" : "",
763
+ isRangeEnd ? "time-grid__slot--range-end" : "",
764
+ isInRange ? "time-grid__slot--in-range" : ""
765
+ ].filter(Boolean).join(" ");
766
+ const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
767
+ return `
768
+ <div class="${slotClasses}" ${slotAttrs}>
769
+ <span class="time-grid__label">${startTime}</span>
770
+ <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
771
+ </div>`;
772
+ });
773
+ return `<div class="time-grid">${slots.join("")}</div>`;
774
+ };
712
775
  const html = `
713
776
  ${title ? `
714
777
  <div class="page--title">
@@ -798,30 +861,52 @@ var Kalendly = (() => {
798
861
  </tr>
799
862
  </thead>
800
863
  <tbody data-calendar-body>
801
- ${viewModel.calendarDates.map(
864
+ ${isLoading ? Array.from(
865
+ { length: 6 },
866
+ () => `<tr>${Array.from(
867
+ { length: 7 },
868
+ () => `<td class="calendar--skeleton" aria-hidden="true"></td>`
869
+ ).join("")}</tr>`
870
+ ).join("") : viewModel.calendarDates.map(
802
871
  (week) => `
803
- <tr>
804
- ${week.map((calendarDate, dayIndex) => {
872
+ <tr>
873
+ ${week.map((calendarDate, dayIndex) => {
805
874
  const classes = getCellClasses(calendarDate);
875
+ if (availabilityMode && calendarDate.isCurrentMonth) {
876
+ classes.push(
877
+ calendarDate.hasEvents ? "availability--booked" : "availability--free"
878
+ );
879
+ }
880
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
881
+ const d = calendarDate.date;
882
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
883
+ classes.push("availability--range-start");
884
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
885
+ classes.push("availability--range-end");
886
+ if (this._rangeStart && this._rangeEnd) {
887
+ if (d > this._rangeStart && d < this._rangeEnd)
888
+ classes.push("availability--in-range");
889
+ }
890
+ }
806
891
  const dateString = calendarDate.date.toISOString();
807
892
  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
- `;
893
+ <td
894
+ class="${classes.join(" ")}"
895
+ data-date="${dateString}"
896
+ data-day-index="${dayIndex}"
897
+ data-clickable="true"
898
+ >
899
+ ${calendarDate.date.getDate()}
900
+ </td>
901
+ `;
817
902
  }).join("")}
818
- </tr>
819
- `
903
+ </tr>
904
+ `
820
905
  ).join("")}
821
906
  </tbody>
822
907
  </table>
823
908
 
824
- ${viewModel.selectedDate ? `
909
+ ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
825
910
  <div class="date-popup ${viewModel.popupPositionClass}">
826
911
  <div class="popup-header">
827
912
  <h2>${viewModel.scheduleDay}</h2>
@@ -835,7 +920,7 @@ var Kalendly = (() => {
835
920
  ` : ""}
836
921
 
837
922
  <div class="events-container">
838
- ${viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
923
+ ${availabilityMode === "time" ? renderTimeGrid(viewModel.tasks, viewModel.selectedDate) : viewModel.tasks.length > 0 ? viewModel.tasks.map((event) => renderEvent(event)).join("") : renderNoEvents()}
839
924
  </div>
840
925
  </div>
841
926
  ` : ""}
@@ -857,6 +942,59 @@ var Kalendly = (() => {
857
942
  e.stopPropagation();
858
943
  const date = new Date(cell.dataset.date);
859
944
  const dayIndex = parseInt(cell.dataset.dayIndex || "0");
945
+ const availMode = this.getAttribute("availability-mode");
946
+ const isSelectable = this.hasAttribute("selectable");
947
+ if (availMode === "day" && isSelectable) {
948
+ const isBooked = cell.classList.contains("availability--booked");
949
+ if (!isBooked) {
950
+ let startDate;
951
+ let endDate;
952
+ if (this._rangeEnd !== null) {
953
+ this._rangeStart = date;
954
+ this._rangeEnd = null;
955
+ startDate = date;
956
+ endDate = date;
957
+ } else if (this._rangeStart === null) {
958
+ this._rangeStart = date;
959
+ startDate = date;
960
+ endDate = date;
961
+ } else {
962
+ const [s, e2] = this._rangeStart <= date ? [this._rangeStart, date] : [date, this._rangeStart];
963
+ const cursor = new Date(s);
964
+ cursor.setDate(cursor.getDate() + 1);
965
+ let blocked = false;
966
+ while (cursor < e2) {
967
+ if (this.engine.getEventsForDate(cursor).length > 0) {
968
+ blocked = true;
969
+ break;
970
+ }
971
+ cursor.setDate(cursor.getDate() + 1);
972
+ }
973
+ if (blocked) {
974
+ this._rangeStart = date;
975
+ this._rangeEnd = null;
976
+ } else {
977
+ this._rangeStart = s;
978
+ this._rangeEnd = e2;
979
+ }
980
+ startDate = this._rangeStart;
981
+ endDate = this._rangeEnd ?? this._rangeStart;
982
+ }
983
+ this.dispatchEvent(
984
+ new CustomEvent("cal-availability-select", {
985
+ bubbles: true,
986
+ composed: true,
987
+ detail: { startDate, endDate }
988
+ })
989
+ );
990
+ }
991
+ }
992
+ if (availMode === "time") {
993
+ this._timeRangeDate = null;
994
+ this._timeRangeStart = null;
995
+ this._timeRangeEnd = null;
996
+ this._timeRangeComplete = false;
997
+ }
860
998
  this.engine.handleDateClick(date, dayIndex);
861
999
  this.dispatchEvent(
862
1000
  new CustomEvent("cal-date-select", {
@@ -872,19 +1010,29 @@ var Kalendly = (() => {
872
1010
  const action = actionEl.dataset.action;
873
1011
  e.stopPropagation();
874
1012
  switch (action) {
875
- case "previous":
1013
+ case "previous": {
1014
+ const vm = this.engine.getViewModel();
1015
+ const tMonth = vm.currentMonth === 0 ? 11 : vm.currentMonth - 1;
1016
+ const tYear = vm.currentMonth === 0 ? vm.currentYear - 1 : vm.currentYear;
1017
+ this.dispatchMonthChange(tYear, tMonth);
876
1018
  this.actions.previous();
877
- this.dispatchMonthChange();
878
1019
  break;
879
- case "next":
1020
+ }
1021
+ case "next": {
1022
+ const vm = this.engine.getViewModel();
1023
+ const tMonth = vm.currentMonth === 11 ? 0 : vm.currentMonth + 1;
1024
+ const tYear = vm.currentMonth === 11 ? vm.currentYear + 1 : vm.currentYear;
1025
+ this.dispatchMonthChange(tYear, tMonth);
880
1026
  this.actions.next();
881
- this.dispatchMonthChange();
882
1027
  break;
883
- case "today":
1028
+ }
1029
+ case "today": {
1030
+ const t = /* @__PURE__ */ new Date();
1031
+ this.dispatchMonthChange(t.getFullYear(), t.getMonth());
884
1032
  this.actions.goToToday();
885
1033
  this.pickerOpen = false;
886
- this.dispatchMonthChange();
887
1034
  break;
1035
+ }
888
1036
  case "toggle-picker":
889
1037
  this.pickerOpen = !this.pickerOpen;
890
1038
  if (this.pickerOpen) {
@@ -899,8 +1047,8 @@ var Kalendly = (() => {
899
1047
  const newYear = vm.currentYear - 1;
900
1048
  this.yearInput = String(newYear);
901
1049
  this.updatePickerYear(newYear);
1050
+ this.dispatchMonthChange(newYear, vm.currentMonth);
902
1051
  this.actions.jump(newYear, vm.currentMonth);
903
- this.dispatchMonthChange();
904
1052
  }
905
1053
  break;
906
1054
  }
@@ -910,21 +1058,60 @@ var Kalendly = (() => {
910
1058
  const newYear = vm.currentYear + 1;
911
1059
  this.yearInput = String(newYear);
912
1060
  this.updatePickerYear(newYear);
1061
+ this.dispatchMonthChange(newYear, vm.currentMonth);
913
1062
  this.actions.jump(newYear, vm.currentMonth);
914
- this.dispatchMonthChange();
915
1063
  }
916
1064
  break;
917
1065
  }
918
1066
  case "select-month": {
919
1067
  const month = parseInt(actionEl.dataset.month || "0", 10);
920
- this.actions.jump(this.engine.getViewModel().currentYear, month);
1068
+ const year = this.engine.getViewModel().currentYear;
1069
+ this.dispatchMonthChange(year, month);
1070
+ this.actions.jump(year, month);
921
1071
  this.pickerOpen = false;
922
- this.dispatchMonthChange();
923
1072
  break;
924
1073
  }
925
1074
  case "close-popup":
1075
+ this._timeRangeDate = null;
1076
+ this._timeRangeStart = null;
1077
+ this._timeRangeEnd = null;
1078
+ this._timeRangeComplete = false;
926
1079
  this.engine.clearSelection();
927
1080
  break;
1081
+ case "select-slot": {
1082
+ const slotStart = actionEl.dataset.startTime;
1083
+ const slotEnd = actionEl.dataset.endTime;
1084
+ const slotDate = new Date(actionEl.dataset.date);
1085
+ if (this._timeRangeComplete) {
1086
+ this._timeRangeDate = slotDate;
1087
+ this._timeRangeStart = slotStart;
1088
+ this._timeRangeEnd = slotEnd;
1089
+ this._timeRangeComplete = false;
1090
+ } else if (this._timeRangeStart === null) {
1091
+ this._timeRangeDate = slotDate;
1092
+ this._timeRangeStart = slotStart;
1093
+ this._timeRangeEnd = slotEnd;
1094
+ } else {
1095
+ const newStart = slotStart < this._timeRangeStart ? slotStart : this._timeRangeStart;
1096
+ const newEnd = slotEnd > this._timeRangeEnd ? slotEnd : this._timeRangeEnd;
1097
+ this._timeRangeStart = newStart;
1098
+ this._timeRangeEnd = newEnd;
1099
+ this._timeRangeComplete = true;
1100
+ }
1101
+ this.render();
1102
+ this.dispatchEvent(
1103
+ new CustomEvent("cal-availability-select", {
1104
+ bubbles: true,
1105
+ composed: true,
1106
+ detail: {
1107
+ date: this._timeRangeDate,
1108
+ startTime: this._timeRangeStart,
1109
+ endTime: this._timeRangeEnd
1110
+ }
1111
+ })
1112
+ );
1113
+ break;
1114
+ }
928
1115
  }
929
1116
  };
930
1117
  this.delegatedInputHandler = (e) => {
@@ -949,8 +1136,8 @@ var Kalendly = (() => {
949
1136
  target.value = this.yearInput;
950
1137
  target.classList.remove("invalid");
951
1138
  } else if (year !== vm.currentYear) {
1139
+ this.dispatchMonthChange(year, vm.currentMonth);
952
1140
  this.actions.jump(year, vm.currentMonth);
953
- this.dispatchMonthChange();
954
1141
  }
955
1142
  };
956
1143
  this.delegatedKeydownHandler = (e) => {
@@ -989,14 +1176,17 @@ var Kalendly = (() => {
989
1176
  if (prevBtn) prevBtn.disabled = year <= this.minYear;
990
1177
  if (nextBtn) nextBtn.disabled = year >= this.maxYear;
991
1178
  }
992
- dispatchMonthChange() {
1179
+ dispatchMonthChange(year, month) {
993
1180
  if (!this.engine) return;
994
1181
  const vm = this.engine.getViewModel();
995
1182
  this.dispatchEvent(
996
1183
  new CustomEvent("cal-month-change", {
997
1184
  bubbles: true,
998
1185
  composed: true,
999
- detail: { year: vm.currentYear, month: vm.currentMonth }
1186
+ detail: {
1187
+ year: year ?? vm.currentYear,
1188
+ month: month ?? vm.currentMonth
1189
+ }
1000
1190
  })
1001
1191
  );
1002
1192
  }
@@ -1011,7 +1201,10 @@ var Kalendly = (() => {
1011
1201
  return this.engine?.getViewModel().selectedDate ?? null;
1012
1202
  }
1013
1203
  goToDate(date) {
1014
- this.actions?.jump(date.getFullYear(), date.getMonth());
1204
+ const year = date.getFullYear();
1205
+ const month = date.getMonth();
1206
+ this.dispatchMonthChange(year, month);
1207
+ this.actions?.jump(year, month);
1015
1208
  }
1016
1209
  getEngine() {
1017
1210
  if (!this.engine)
@@ -1025,7 +1218,10 @@ var Kalendly = (() => {
1025
1218
  "max-year",
1026
1219
  "week-starts-on",
1027
1220
  "title",
1028
- "use-short-month-names"
1221
+ "use-short-month-names",
1222
+ "availability-mode",
1223
+ "selectable",
1224
+ "loading"
1029
1225
  ];
1030
1226
  function defineCalendarElement(tagName = "kal-calendar") {
1031
1227
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {