kalendly 0.2.1 → 0.3.0

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
@@ -139,26 +139,16 @@ var Kalendly = (() => {
139
139
  }
140
140
  return dates;
141
141
  }
142
- function getPopupPositionClass(selectedDayIndex) {
143
- if (selectedDayIndex === null) return "popup-center-bottom";
144
- if (selectedDayIndex < 3) {
145
- return "popup-right";
146
- } else if (selectedDayIndex > 4) {
147
- return "popup-left";
148
- } else {
149
- return "popup-center-bottom";
150
- }
151
- }
152
142
  function getCellClasses(calendarDate) {
153
143
  const classes = [];
154
144
  if (!calendarDate.isCurrentMonth) {
155
- classes.push("other-month");
145
+ classes.push("calendar-cell-other-month");
156
146
  }
157
147
  if (calendarDate.isToday) {
158
- classes.push("schedule--current--exam");
148
+ classes.push("calendar-cell-today");
159
149
  }
160
150
  if (calendarDate.hasEvents) {
161
- classes.push("has--event");
151
+ classes.push("calendar-cell-has-event");
162
152
  }
163
153
  return classes;
164
154
  }
@@ -208,6 +198,30 @@ var Kalendly = (() => {
208
198
  const color = colorMap[category] || colorMap.other || "#fc8917";
209
199
  return isValidHexColor(color) ? color : "#fc8917";
210
200
  }
201
+ var HTML_ESCAPES = {
202
+ "&": "&amp;",
203
+ "<": "&lt;",
204
+ ">": "&gt;",
205
+ '"': "&quot;",
206
+ "'": "&#39;"
207
+ };
208
+ function escapeHtml(value) {
209
+ if (value === null || value === void 0) return "";
210
+ return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
211
+ }
212
+ function slugifyToken(value) {
213
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
214
+ }
215
+ function safeUrl(value) {
216
+ const normalized = String(value).split("").filter((char) => char.charCodeAt(0) > 32).join("");
217
+ if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;
218
+ const schemeless = !/^[^/?#]*:/.test(normalized);
219
+ return schemeless ? normalized : "#";
220
+ }
221
+ function safeColor(value) {
222
+ const trimmed = String(value).trim();
223
+ return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
224
+ }
211
225
 
212
226
  // src/core/calendar-engine.ts
213
227
  var CalendarEngine = class {
@@ -248,24 +262,38 @@ var Kalendly = (() => {
248
262
  * Get view model with computed properties
249
263
  */
250
264
  getViewModel() {
251
- const calendarDates = generateCalendarDates(
252
- this.state.currentYear,
253
- this.state.currentMonth,
254
- this.config.events,
255
- this.config.weekStartsOn
265
+ const panes = Array.from(
266
+ { length: Math.max(1, this.config.monthCount ?? 1) },
267
+ (_, offset) => {
268
+ const anchor = new Date(
269
+ this.state.currentYear,
270
+ this.state.currentMonth + offset,
271
+ 1
272
+ );
273
+ const year = anchor.getFullYear();
274
+ const month = anchor.getMonth();
275
+ return {
276
+ year,
277
+ month,
278
+ monthAndYearText: getMonthYearText(year, month),
279
+ calendarDates: generateCalendarDates(
280
+ year,
281
+ month,
282
+ this.config.events,
283
+ this.config.weekStartsOn
284
+ )
285
+ };
286
+ }
256
287
  );
257
288
  return {
258
289
  ...this.state,
259
290
  months: MONTHS,
260
291
  days: DAYS,
261
292
  years: generateYears(this.config.minYear, this.config.maxYear),
262
- monthAndYearText: getMonthYearText(
263
- this.state.currentYear,
264
- this.state.currentMonth
265
- ),
293
+ monthAndYearText: panes[0].monthAndYearText,
266
294
  scheduleDay: this.state.selectedDate ? formatDateForDisplay(this.state.selectedDate) : "",
267
- calendarDates,
268
- popupPositionClass: getPopupPositionClass(this.state.selectedDayIndex)
295
+ panes,
296
+ calendarDates: panes[0].calendarDates
269
297
  };
270
298
  }
271
299
  /**
@@ -439,7 +467,7 @@ var Kalendly = (() => {
439
467
 
440
468
  // src/web-components/CalendarElement.ts
441
469
  var isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
442
- var CalendarElement = class extends HTMLElement {
470
+ var _CalendarElement = class _CalendarElement extends HTMLElement {
443
471
  constructor() {
444
472
  super(...arguments);
445
473
  this.engine = null;
@@ -460,6 +488,10 @@ var Kalendly = (() => {
460
488
  this._categoryColors = null;
461
489
  this._renderEvent = null;
462
490
  this._renderNoEvents = null;
491
+ this._availabilityColors = null;
492
+ this._selectableStatuses = null;
493
+ this._initError = null;
494
+ this.pendingUpdate = null;
463
495
  // Selection state (availability mode — range)
464
496
  this._rangeStart = null;
465
497
  this._rangeEnd = null;
@@ -472,6 +504,7 @@ var Kalendly = (() => {
472
504
  return this._events;
473
505
  }
474
506
  set events(val) {
507
+ this._initError = null;
475
508
  this._events = val;
476
509
  if (this.engine) {
477
510
  this.engine.updateEvents(val);
@@ -487,13 +520,28 @@ var Kalendly = (() => {
487
520
  this.engine.updateCategoryColors(val);
488
521
  }
489
522
  }
523
+ get availabilityColors() {
524
+ return this._availabilityColors ?? {};
525
+ }
526
+ set availabilityColors(val) {
527
+ this._initError = null;
528
+ this._availabilityColors = val;
529
+ if (this.engine) this.scheduleRender();
530
+ }
531
+ get selectableStatuses() {
532
+ return this._selectableStatuses ?? [];
533
+ }
534
+ set selectableStatuses(val) {
535
+ this._selectableStatuses = val;
536
+ if (this.engine) this.scheduleRender();
537
+ }
490
538
  set renderEvent(val) {
491
539
  this._renderEvent = val;
492
- if (this.engine) this.render();
540
+ if (this.engine) this.scheduleRender();
493
541
  }
494
542
  set renderNoEvents(val) {
495
543
  this._renderNoEvents = val;
496
- if (this.engine) this.render();
544
+ if (this.engine) this.scheduleRender();
497
545
  }
498
546
  get loading() {
499
547
  return this.hasAttribute("loading");
@@ -502,10 +550,22 @@ var Kalendly = (() => {
502
550
  if (val) this.setAttribute("loading", "");
503
551
  else this.removeAttribute("loading");
504
552
  }
553
+ // Custom element reactions report rather than propagate, so a failure here
554
+ // is kept and resurfaced at the next call the integrator makes
555
+ reaction(run) {
556
+ try {
557
+ run();
558
+ } catch (error) {
559
+ this._initError = error;
560
+ throw error;
561
+ }
562
+ }
505
563
  connectedCallback() {
506
564
  this.classList.add("kalendly-calendar");
507
- this.initEngine();
508
- this.render();
565
+ this.reaction(() => {
566
+ this.initEngine();
567
+ this.render();
568
+ });
509
569
  }
510
570
  disconnectedCallback() {
511
571
  this.cleanup();
@@ -515,7 +575,7 @@ var Kalendly = (() => {
515
575
  attributeChangedCallback(name, oldVal, newVal) {
516
576
  if (oldVal === newVal) return;
517
577
  if (name === "loading") {
518
- this.render();
578
+ this.scheduleRender();
519
579
  return;
520
580
  }
521
581
  if (name === "selectable" && newVal === null) {
@@ -528,6 +588,23 @@ var Kalendly = (() => {
528
588
  }
529
589
  this.reinit();
530
590
  }
591
+ get headingText() {
592
+ const heading = this.getAttribute("heading");
593
+ if (heading !== null) return heading;
594
+ const title = this.getAttribute("title");
595
+ if (title !== null && !_CalendarElement.titleDeprecationWarned) {
596
+ _CalendarElement.titleDeprecationWarned = true;
597
+ console.warn(
598
+ `<kal-calendar> the title attribute is deprecated and will be removed in a future release \u2014 use heading instead. title is a global HTML attribute, so the browser also renders it as a tooltip over the whole calendar.`
599
+ );
600
+ }
601
+ return title;
602
+ }
603
+ get monthCount() {
604
+ const requested = Number(this.getAttribute("months") ?? 1);
605
+ if (!Number.isInteger(requested) || requested < 1) return 1;
606
+ return Math.min(requested, 2);
607
+ }
531
608
  get minYear() {
532
609
  const val = this.getAttribute("min-year");
533
610
  return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() - 30;
@@ -547,19 +624,20 @@ var Kalendly = (() => {
547
624
  minYear: this.minYear,
548
625
  maxYear: this.maxYear,
549
626
  weekStartsOn,
550
- categoryColors: this._categoryColors ?? void 0
627
+ categoryColors: this._categoryColors ?? void 0,
628
+ monthCount: this.monthCount
551
629
  });
552
630
  this.actions = this.engine.getActions();
553
631
  this.applyTheme();
554
632
  this.unsubscribe = this.engine.subscribe(() => {
555
- this.render();
633
+ this.scheduleRender();
556
634
  });
557
635
  }
558
636
  reinit() {
559
637
  if (!this.engine) return;
560
638
  this.cleanup();
561
639
  this.initEngine();
562
- this.render();
640
+ this.scheduleRender();
563
641
  }
564
642
  cleanup() {
565
643
  if (this.unsubscribe) {
@@ -596,40 +674,84 @@ var Kalendly = (() => {
596
674
  applyTheme() {
597
675
  if (!this._theme) return;
598
676
  const root = document.documentElement;
599
- const t = this._theme;
600
- if (t.primary)
601
- root.style.setProperty("--calendar-primary-color", t.primary);
602
- if (t.secondary)
603
- root.style.setProperty("--calendar-secondary-color", t.secondary);
604
- if (t.tertiary)
605
- root.style.setProperty("--calendar-tertiary-color", t.tertiary);
606
- if (t.textColor)
607
- root.style.setProperty("--calendar-text-color", t.textColor);
608
- if (t.textLight)
609
- root.style.setProperty("--calendar-text-light", t.textLight);
610
- if (t.background)
611
- root.style.setProperty("--calendar-background", t.background);
612
- if (t.cellHover)
613
- root.style.setProperty("--calendar-cell-hover", t.cellHover);
614
- if (t.borderColor)
615
- root.style.setProperty("--calendar-border-color", t.borderColor);
616
- if (t.todayOutline)
617
- root.style.setProperty("--calendar-today-outline", t.todayOutline);
618
- if (t.selectedBg)
619
- root.style.setProperty("--calendar-selected-bg", t.selectedBg);
620
- if (t.eventIndicator)
621
- root.style.setProperty("--calendar-event-indicator", t.eventIndicator);
677
+ for (const key of Object.keys(_CalendarElement.themeMap)) {
678
+ const value = this._theme[key];
679
+ if (value) root.style.setProperty(_CalendarElement.themeMap[key], value);
680
+ }
681
+ }
682
+ get knownBuckets() {
683
+ return [
684
+ ..._CalendarElement.BUILT_IN_BUCKETS,
685
+ ...Object.keys(this._availabilityColors ?? {})
686
+ ];
687
+ }
688
+ get updateComplete() {
689
+ return this.pendingUpdate ?? Promise.resolve();
690
+ }
691
+ scheduleRender() {
692
+ if (this.pendingUpdate) return;
693
+ this.pendingUpdate = Promise.resolve().then(() => {
694
+ this.pendingUpdate = null;
695
+ try {
696
+ this.render();
697
+ } catch (error) {
698
+ this._initError = error;
699
+ throw error;
700
+ }
701
+ });
702
+ }
703
+ rethrowInitError() {
704
+ if (this._initError) throw this._initError;
705
+ }
706
+ assertStatusesDeclared() {
707
+ if (!this.getAttribute("availability-mode")) return;
708
+ const missing = this._events.filter(
709
+ (event) => typeof event.availabilityStatus !== "string" || event.availabilityStatus === ""
710
+ ).map((event) => String(event.id));
711
+ if (missing.length) {
712
+ throw new Error(
713
+ `<kal-calendar> availability-mode requires availabilityStatus on every event. Missing on: ${missing.join(", ")}.`
714
+ );
715
+ }
716
+ }
717
+ assertStatusesKnown() {
718
+ const known = new Set(this.knownBuckets);
719
+ const unknown = this._events.filter((event) => !known.has(event.availabilityStatus)).map((event) => `${event.id} (${event.availabilityStatus})`);
720
+ if (unknown.length) {
721
+ throw new Error(
722
+ `<kal-calendar> availabilityStatus must name a built-in bucket (${_CalendarElement.BUILT_IN_BUCKETS.join(", ")}) or a key of availabilityColors. Unrecognised on: ${unknown.join(", ")}.`
723
+ );
724
+ }
725
+ }
726
+ resolveBucket(date) {
727
+ if (!this.engine) return "open";
728
+ const declared = new Set(
729
+ this.engine.getEventsForDate(date).map((event) => event.availabilityStatus)
730
+ );
731
+ if (declared.size === 0) return "open";
732
+ return this.knownBuckets.find((bucket) => declared.has(bucket));
733
+ }
734
+ isDateSelectable(date) {
735
+ if (!this.engine) return false;
736
+ if (this._selectableStatuses) {
737
+ return this._selectableStatuses.includes(this.resolveBucket(date));
738
+ }
739
+ return this.engine.getEventsForDate(date).length === 0;
622
740
  }
623
741
  render() {
624
742
  if (!this.engine) return;
625
743
  const viewModel = this.engine.getViewModel();
626
744
  const useShortMonths = this.hasAttribute("use-short-month-names");
627
- const title = this.getAttribute("title");
745
+ const title = this.headingText;
628
746
  const minYear = this.minYear;
629
747
  const maxYear = this.maxYear;
630
748
  const availabilityMode = this.getAttribute("availability-mode");
631
749
  const selectable = this.hasAttribute("selectable");
632
750
  const isLoading = this.loading;
751
+ if (availabilityMode) {
752
+ this.assertStatusesDeclared();
753
+ this.assertStatusesKnown();
754
+ }
633
755
  const today = /* @__PURE__ */ new Date();
634
756
  const todayMonth = today.getMonth();
635
757
  const todayYear = today.getFullYear();
@@ -638,7 +760,7 @@ var Kalendly = (() => {
638
760
  const defaultRenderEvent = (event) => {
639
761
  const timeRange = formatTimeRange(event);
640
762
  const attendeesList = formatAttendees(event.attendees);
641
- let borderColor = event.color || "#3b82f6";
763
+ let borderColor = safeColor(event.color || "#3b82f6");
642
764
  if (event.category) {
643
765
  borderColor = this.engine.getCategoryColor(event.category);
644
766
  }
@@ -673,56 +795,56 @@ var Kalendly = (() => {
673
795
  return `
674
796
  <div class="event-card" style="border-left-color: ${borderColor}">
675
797
  <div class="event-header">
676
- <div class="event-title">${event.name}</div>
798
+ <div class="event-title">${escapeHtml(event.name)}</div>
677
799
  <div class="event-badges">
678
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
679
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
680
- ${event.status && event.status !== "scheduled" ? `<span class="badge status-${event.status}">${getStatusLabel(event.status)}</span>` : ""}
800
+ ${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
801
+ ${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
802
+ ${event.status && event.status !== "scheduled" ? `<span class="badge status status-${slugifyToken(event.status)}">${escapeHtml(getStatusLabel(event.status))}</span>` : ""}
681
803
  </div>
682
804
  </div>
683
805
 
684
806
  ${timeRange ? `
685
807
  <div class="event-time">
686
808
  <span class="event-time-label">Time:</span>
687
- <span class="event-time-value">${timeRange}</span>
809
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
688
810
  </div>
689
811
  ` : ""}
690
812
 
691
813
  ${event.description ? `
692
- <div class="event-description">${event.description}</div>
814
+ <div class="event-description">${escapeHtml(event.description)}</div>
693
815
  ` : ""}
694
816
 
695
817
  ${event.location ? `
696
818
  <div class="event-time">
697
819
  <span class="event-time-label">Location:</span>
698
- <span class="event-time-value">${event.location}</span>
820
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
699
821
  </div>
700
822
  ` : ""}
701
823
 
702
824
  ${attendeesList ? `
703
825
  <div class="event-time">
704
826
  <span class="event-time-label">Attendees:</span>
705
- <span class="event-time-value">${attendeesList}</span>
827
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
706
828
  </div>
707
829
  ` : ""}
708
830
 
709
831
  ${event.organizer ? `
710
832
  <div class="event-time">
711
833
  <span class="event-time-label">Organizer:</span>
712
- <span class="event-time-value">${event.organizer}</span>
834
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
713
835
  </div>
714
836
  ` : ""}
715
837
 
716
838
  ${event.notes ? `
717
839
  <div class="event-time">
718
840
  <span class="event-time-label">Notes:</span>
719
- <span class="event-time-value">${event.notes}</span>
841
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
720
842
  </div>
721
843
  ` : ""}
722
844
 
723
845
  ${event.url ? `
724
846
  <div class="event-time">
725
- <a href="${event.url}" target="_blank" rel="noopener noreferrer" class="event-link">
847
+ <a href="${escapeHtml(safeUrl(event.url))}" target="_blank" rel="noopener noreferrer" class="event-link">
726
848
  View Details \u2192
727
849
  </a>
728
850
  </div>
@@ -730,7 +852,7 @@ var Kalendly = (() => {
730
852
 
731
853
  ${event.tags && event.tags.length > 0 ? `
732
854
  <div class="event-tags">
733
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
855
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
734
856
  </div>
735
857
  ` : ""}
736
858
  </div>
@@ -740,11 +862,17 @@ var Kalendly = (() => {
740
862
  const renderEvent = this._renderEvent || defaultRenderEvent;
741
863
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
742
864
  const renderTimeGrid = (events, date) => {
865
+ const startHour = (time) => {
866
+ if (typeof time !== "string") return null;
867
+ const hour = Number(time.split(":")[0]);
868
+ return Number.isInteger(hour) && hour >= 0 && hour <= 24 ? hour : null;
869
+ };
743
870
  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;
871
+ if (!event.startTime) return true;
872
+ const from = startHour(event.startTime);
873
+ const to = event.endTime ? startHour(event.endTime) : 24;
874
+ if (from === null || to === null) return true;
875
+ return hour >= from && hour < to;
748
876
  });
749
877
  const slots = Array.from({ length: 24 }, (_, hour) => {
750
878
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -755,81 +883,156 @@ var Kalendly = (() => {
755
883
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
756
884
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
757
885
  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" : ""
886
+ "time-grid-slot",
887
+ booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
888
+ isRangeStart ? "time-grid-slot-range-start" : "",
889
+ isRangeEnd ? "time-grid-slot-range-end" : "",
890
+ isInRange ? "time-grid-slot-in-range" : ""
763
891
  ].filter(Boolean).join(" ");
764
892
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
765
893
  return `
766
894
  <div class="${slotClasses}" ${slotAttrs}>
767
- <span class="time-grid__label">${startTime}</span>
768
- <span class="time-grid__status">${booked ? "Booked" : "Available"}</span>
895
+ <span class="time-grid-label">${startTime}</span>
896
+ <span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
769
897
  </div>`;
770
898
  });
771
899
  return `<div class="time-grid">${slots.join("")}</div>`;
772
900
  };
901
+ const multiMonth = viewModel.panes.length > 1;
902
+ const renderPane = (pane) => `
903
+ <div class="calendar-pane">
904
+ ${multiMonth ? `<div class="calendar-pane-caption">${escapeHtml(pane.monthAndYearText)}</div>` : ""}
905
+ <table class="calendar-table calendar-table-bordered">
906
+ <thead>
907
+ <tr>
908
+ ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
909
+ </tr>
910
+ </thead>
911
+ <tbody data-calendar-body>
912
+ ${isLoading ? Array.from(
913
+ { length: 6 },
914
+ () => `<tr>${Array.from(
915
+ { length: 7 },
916
+ () => `<td class="calendar-skeleton" aria-hidden="true"></td>`
917
+ ).join("")}</tr>`
918
+ ).join("") : pane.calendarDates.map(
919
+ (week) => `
920
+ <tr>
921
+ ${week.map((calendarDate, dayIndex) => {
922
+ const classes = getCellClasses(calendarDate);
923
+ const cellAttrs = [];
924
+ if (availabilityMode && calendarDate.isCurrentMonth) {
925
+ const bucket = this.resolveBucket(
926
+ calendarDate.date
927
+ );
928
+ const custom = (this._availabilityColors ?? {})[bucket];
929
+ classes.push(
930
+ `availability-${slugifyToken(bucket)}`
931
+ );
932
+ if (custom) {
933
+ classes.push("availability-status");
934
+ cellAttrs.push(
935
+ `style="--availability-color: ${escapeHtml(safeColor(custom))}"`
936
+ );
937
+ }
938
+ if (!this.isDateSelectable(calendarDate.date)) {
939
+ classes.push("availability-unselectable");
940
+ }
941
+ cellAttrs.push(
942
+ `aria-label="${escapeHtml(bucket)}"`
943
+ );
944
+ }
945
+ if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
946
+ const d = calendarDate.date;
947
+ if (this._rangeStart && isSameDay2(d, this._rangeStart))
948
+ classes.push("availability-range-start");
949
+ if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
950
+ classes.push("availability-range-end");
951
+ if (this._rangeStart && this._rangeEnd) {
952
+ if (d > this._rangeStart && d < this._rangeEnd)
953
+ classes.push("availability-in-range");
954
+ }
955
+ }
956
+ const dateString = calendarDate.date.toISOString();
957
+ return `
958
+ <td
959
+ class="${classes.join(" ")}"
960
+ data-date="${dateString}"
961
+ data-day-index="${dayIndex}"
962
+ data-clickable="true"
963
+ ${cellAttrs.join(" ")}
964
+ >
965
+ ${calendarDate.date.getDate()}
966
+ </td>
967
+ `;
968
+ }).join("")}
969
+ </tr>
970
+ `
971
+ ).join("")}
972
+ </tbody>
973
+ </table>
974
+ </div>
975
+ `;
773
976
  const html = `
774
977
  ${title ? `
775
- <div class="page--title">
776
- <h1>${title}</h1>
978
+ <div class="calendar-title">
979
+ <h1>${escapeHtml(title)}</h1>
777
980
  </div>
778
981
  ` : ""}
779
982
 
780
- <div class="calendar--content">
781
- <div class="calendar--card">
782
- <div class="calendar--nav-header">
783
- <button type="button" class="calendar--nav-arrow" data-action="previous" aria-label="Previous month">
983
+ <div class="calendar-content">
984
+ <div class="calendar-card">
985
+ <div class="calendar-nav-header">
986
+ <button type="button" class="calendar-nav-arrow" data-action="previous" aria-label="Previous month">
784
987
  &#8249;
785
988
  </button>
786
989
 
787
- <div class="calendar--picker-container" data-picker-container>
990
+ <div class="calendar-picker-container" data-picker-container>
788
991
  <button
789
992
  type="button"
790
- class="calendar--picker-btn"
993
+ class="calendar-picker-btn"
791
994
  data-action="toggle-picker"
792
995
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
793
996
  aria-haspopup="true"
794
997
  >
795
998
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
796
- <span class="calendar--picker-chevron">&#9662;</span>
999
+ <span class="calendar-picker-chevron">&#9662;</span>
797
1000
  </button>
798
1001
 
799
1002
  ${this.pickerOpen ? `
800
- <div class="calendar--picker-dropdown">
801
- <div class="calendar--picker-year-row">
1003
+ <div class="calendar-picker-dropdown">
1004
+ <div class="calendar-picker-year-row">
802
1005
  <button
803
1006
  type="button"
804
- class="calendar--picker-year-arrow"
1007
+ class="calendar-picker-year-arrow"
805
1008
  data-action="year-prev"
806
1009
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
807
1010
  aria-label="Previous year"
808
1011
  >&#8249;</button>
809
1012
  <input
810
1013
  type="text"
811
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
812
- value="${this.yearInput || viewModel.currentYear}"
1014
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1015
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
813
1016
  data-year-input
814
1017
  aria-label="Year"
815
1018
  />
816
1019
  <button
817
1020
  type="button"
818
- class="calendar--picker-year-arrow"
1021
+ class="calendar-picker-year-arrow"
819
1022
  data-action="year-next"
820
1023
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
821
1024
  aria-label="Next year"
822
1025
  >&#8250;</button>
823
1026
  </div>
824
1027
 
825
- <div class="calendar--picker-months">
1028
+ <div class="calendar-picker-months">
826
1029
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
827
1030
  const isSelected = index === viewModel.currentMonth;
828
1031
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
829
1032
  return `
830
1033
  <button
831
1034
  type="button"
832
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1035
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
833
1036
  data-action="select-month"
834
1037
  data-month="${index}"
835
1038
  >${month}</button>
@@ -842,70 +1045,22 @@ var Kalendly = (() => {
842
1045
 
843
1046
  <button
844
1047
  type="button"
845
- class="calendar--today-btn"
1048
+ class="calendar-today-btn"
846
1049
  data-action="today"
847
1050
  ${isCurrentMonth ? "disabled" : ""}
848
1051
  >Today</button>
849
1052
 
850
- <button type="button" class="calendar--nav-arrow" data-action="next" aria-label="Next month">
1053
+ <button type="button" class="calendar-nav-arrow" data-action="next" aria-label="Next month">
851
1054
  &#8250;
852
1055
  </button>
853
1056
  </div>
854
1057
 
855
- <table class="calendar--table calendar--table--bordered">
856
- <thead>
857
- <tr>
858
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
859
- </tr>
860
- </thead>
861
- <tbody data-calendar-body>
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(
869
- (week) => `
870
- <tr>
871
- ${week.map((calendarDate, dayIndex) => {
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
- }
889
- const dateString = calendarDate.date.toISOString();
890
- return `
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
- `;
900
- }).join("")}
901
- </tr>
902
- `
903
- ).join("")}
904
- </tbody>
905
- </table>
1058
+ <div class="calendar-panes">
1059
+ ${viewModel.panes.map(renderPane).join("")}
1060
+ </div>
906
1061
 
907
1062
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
908
- <div class="date-popup ${viewModel.popupPositionClass}">
1063
+ <div class="date-popup">
909
1064
  <div class="popup-header">
910
1065
  <h2>${viewModel.scheduleDay}</h2>
911
1066
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -943,8 +1098,7 @@ var Kalendly = (() => {
943
1098
  const availMode = this.getAttribute("availability-mode");
944
1099
  const isSelectable = this.hasAttribute("selectable");
945
1100
  if (availMode === "day" && isSelectable) {
946
- const isBooked = cell.classList.contains("availability--booked");
947
- if (!isBooked) {
1101
+ if (this.isDateSelectable(date)) {
948
1102
  let startDate;
949
1103
  let endDate;
950
1104
  if (this._rangeEnd !== null) {
@@ -962,7 +1116,7 @@ var Kalendly = (() => {
962
1116
  cursor.setDate(cursor.getDate() + 1);
963
1117
  let blocked = false;
964
1118
  while (cursor < e2) {
965
- if (this.engine.getEventsForDate(cursor).length > 0) {
1119
+ if (!this.isDateSelectable(cursor)) {
966
1120
  blocked = true;
967
1121
  break;
968
1122
  }
@@ -1196,31 +1350,84 @@ var Kalendly = (() => {
1196
1350
  this.theme = theme;
1197
1351
  }
1198
1352
  getCurrentDate() {
1353
+ this.rethrowInitError();
1199
1354
  return this.engine?.getViewModel().selectedDate ?? null;
1200
1355
  }
1201
1356
  goToDate(date) {
1357
+ this.rethrowInitError();
1202
1358
  const year = date.getFullYear();
1203
1359
  const month = date.getMonth();
1204
1360
  this.dispatchMonthChange(year, month);
1205
1361
  this.actions?.jump(year, month);
1206
1362
  }
1207
1363
  getEngine() {
1364
+ this.rethrowInitError();
1208
1365
  if (!this.engine)
1209
1366
  throw new Error("CalendarElement is not connected to the DOM");
1210
1367
  return this.engine;
1211
1368
  }
1212
1369
  };
1213
- CalendarElement.observedAttributes = [
1370
+ _CalendarElement.observedAttributes = [
1214
1371
  "initial-date",
1215
1372
  "min-year",
1216
1373
  "max-year",
1217
1374
  "week-starts-on",
1375
+ "heading",
1376
+ "months",
1218
1377
  "title",
1219
1378
  "use-short-month-names",
1220
1379
  "availability-mode",
1221
1380
  "selectable",
1222
1381
  "loading"
1223
1382
  ];
1383
+ _CalendarElement.themeMap = {
1384
+ primary: "--calendar-primary-color",
1385
+ secondary: "--calendar-secondary-color",
1386
+ tertiary: "--calendar-tertiary-color",
1387
+ textColor: "--calendar-text-color",
1388
+ textLight: "--calendar-text-light",
1389
+ background: "--calendar-background",
1390
+ cellHover: "--calendar-cell-hover",
1391
+ borderColor: "--calendar-border-color",
1392
+ todayOutline: "--calendar-today-outline",
1393
+ selectedBg: "--calendar-selected-bg",
1394
+ headerBg: "--calendar-header-bg",
1395
+ popupBg: "--calendar-popup-bg",
1396
+ pickerBg: "--calendar-picker-bg",
1397
+ pickerShadow: "--calendar-picker-shadow",
1398
+ eventIndicator: "--calendar-event-indicator",
1399
+ onAccent: "--calendar-on-accent",
1400
+ link: "--calendar-link",
1401
+ openBg: "--calendar-open-bg",
1402
+ openFg: "--calendar-open-fg",
1403
+ conditionalBg: "--calendar-conditional-bg",
1404
+ conditionalFg: "--calendar-conditional-fg",
1405
+ blockedBg: "--calendar-blocked-bg",
1406
+ blockedFg: "--calendar-blocked-fg",
1407
+ rangeBg: "--calendar-range-bg",
1408
+ rangeOutline: "--calendar-range-outline",
1409
+ inRangeBg: "--calendar-in-range-bg",
1410
+ inRangeOutline: "--calendar-in-range-outline",
1411
+ badgeBg: "--calendar-badge-bg",
1412
+ badgeText: "--calendar-badge-text",
1413
+ badgeSuccessBg: "--calendar-badge-success-bg",
1414
+ badgeSuccessText: "--calendar-badge-success-text",
1415
+ badgeInfoBg: "--calendar-badge-info-bg",
1416
+ badgeInfoText: "--calendar-badge-info-text",
1417
+ badgeWarningBg: "--calendar-badge-warning-bg",
1418
+ badgeWarningText: "--calendar-badge-warning-text",
1419
+ badgeDangerBg: "--calendar-badge-danger-bg",
1420
+ badgeDangerText: "--calendar-badge-danger-text",
1421
+ badgeNeutralBg: "--calendar-badge-neutral-bg",
1422
+ badgeNeutralText: "--calendar-badge-neutral-text",
1423
+ badgePositiveBg: "--calendar-badge-positive-bg",
1424
+ badgePositiveText: "--calendar-badge-positive-text",
1425
+ badgeTentativeBg: "--calendar-badge-tentative-bg",
1426
+ badgeTentativeText: "--calendar-badge-tentative-text"
1427
+ };
1428
+ _CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
1429
+ _CalendarElement.titleDeprecationWarned = false;
1430
+ var CalendarElement = _CalendarElement;
1224
1431
  function defineCalendarElement(tagName = "kal-calendar") {
1225
1432
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1226
1433
  customElements.define(tagName, CalendarElement);