kalendly 0.2.2 → 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,42 +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.headerBg) root.style.setProperty("--calendar-header-bg", t.headerBg);
621
- if (t.popupBg) root.style.setProperty("--calendar-popup-bg", t.popupBg);
622
- if (t.eventIndicator)
623
- 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;
624
740
  }
625
741
  render() {
626
742
  if (!this.engine) return;
627
743
  const viewModel = this.engine.getViewModel();
628
744
  const useShortMonths = this.hasAttribute("use-short-month-names");
629
- const title = this.getAttribute("title");
745
+ const title = this.headingText;
630
746
  const minYear = this.minYear;
631
747
  const maxYear = this.maxYear;
632
748
  const availabilityMode = this.getAttribute("availability-mode");
633
749
  const selectable = this.hasAttribute("selectable");
634
750
  const isLoading = this.loading;
751
+ if (availabilityMode) {
752
+ this.assertStatusesDeclared();
753
+ this.assertStatusesKnown();
754
+ }
635
755
  const today = /* @__PURE__ */ new Date();
636
756
  const todayMonth = today.getMonth();
637
757
  const todayYear = today.getFullYear();
@@ -640,7 +760,7 @@ var Kalendly = (() => {
640
760
  const defaultRenderEvent = (event) => {
641
761
  const timeRange = formatTimeRange(event);
642
762
  const attendeesList = formatAttendees(event.attendees);
643
- let borderColor = event.color || "#3b82f6";
763
+ let borderColor = safeColor(event.color || "#3b82f6");
644
764
  if (event.category) {
645
765
  borderColor = this.engine.getCategoryColor(event.category);
646
766
  }
@@ -675,56 +795,56 @@ var Kalendly = (() => {
675
795
  return `
676
796
  <div class="event-card" style="border-left-color: ${borderColor}">
677
797
  <div class="event-header">
678
- <div class="event-title">${event.name}</div>
798
+ <div class="event-title">${escapeHtml(event.name)}</div>
679
799
  <div class="event-badges">
680
- ${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
681
- ${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
682
- ${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>` : ""}
683
803
  </div>
684
804
  </div>
685
805
 
686
806
  ${timeRange ? `
687
807
  <div class="event-time">
688
808
  <span class="event-time-label">Time:</span>
689
- <span class="event-time-value">${timeRange}</span>
809
+ <span class="event-time-value">${escapeHtml(timeRange)}</span>
690
810
  </div>
691
811
  ` : ""}
692
812
 
693
813
  ${event.description ? `
694
- <div class="event-description">${event.description}</div>
814
+ <div class="event-description">${escapeHtml(event.description)}</div>
695
815
  ` : ""}
696
816
 
697
817
  ${event.location ? `
698
818
  <div class="event-time">
699
819
  <span class="event-time-label">Location:</span>
700
- <span class="event-time-value">${event.location}</span>
820
+ <span class="event-time-value">${escapeHtml(event.location)}</span>
701
821
  </div>
702
822
  ` : ""}
703
823
 
704
824
  ${attendeesList ? `
705
825
  <div class="event-time">
706
826
  <span class="event-time-label">Attendees:</span>
707
- <span class="event-time-value">${attendeesList}</span>
827
+ <span class="event-time-value">${escapeHtml(attendeesList)}</span>
708
828
  </div>
709
829
  ` : ""}
710
830
 
711
831
  ${event.organizer ? `
712
832
  <div class="event-time">
713
833
  <span class="event-time-label">Organizer:</span>
714
- <span class="event-time-value">${event.organizer}</span>
834
+ <span class="event-time-value">${escapeHtml(event.organizer)}</span>
715
835
  </div>
716
836
  ` : ""}
717
837
 
718
838
  ${event.notes ? `
719
839
  <div class="event-time">
720
840
  <span class="event-time-label">Notes:</span>
721
- <span class="event-time-value">${event.notes}</span>
841
+ <span class="event-time-value">${escapeHtml(event.notes)}</span>
722
842
  </div>
723
843
  ` : ""}
724
844
 
725
845
  ${event.url ? `
726
846
  <div class="event-time">
727
- <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">
728
848
  View Details \u2192
729
849
  </a>
730
850
  </div>
@@ -732,7 +852,7 @@ var Kalendly = (() => {
732
852
 
733
853
  ${event.tags && event.tags.length > 0 ? `
734
854
  <div class="event-tags">
735
- ${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
855
+ ${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
736
856
  </div>
737
857
  ` : ""}
738
858
  </div>
@@ -742,11 +862,17 @@ var Kalendly = (() => {
742
862
  const renderEvent = this._renderEvent || defaultRenderEvent;
743
863
  const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
744
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
+ };
745
870
  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;
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;
750
876
  });
751
877
  const slots = Array.from({ length: 24 }, (_, hour) => {
752
878
  const startTime = `${String(hour).padStart(2, "0")}:00`;
@@ -757,81 +883,156 @@ var Kalendly = (() => {
757
883
  const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
758
884
  const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
759
885
  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" : ""
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" : ""
765
891
  ].filter(Boolean).join(" ");
766
892
  const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
767
893
  return `
768
894
  <div class="${slotClasses}" ${slotAttrs}>
769
- <span class="time-grid__label">${startTime}</span>
770
- <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>
771
897
  </div>`;
772
898
  });
773
899
  return `<div class="time-grid">${slots.join("")}</div>`;
774
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
+ `;
775
976
  const html = `
776
977
  ${title ? `
777
- <div class="page--title">
778
- <h1>${title}</h1>
978
+ <div class="calendar-title">
979
+ <h1>${escapeHtml(title)}</h1>
779
980
  </div>
780
981
  ` : ""}
781
982
 
782
- <div class="calendar--content">
783
- <div class="calendar--card">
784
- <div class="calendar--nav-header">
785
- <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">
786
987
  &#8249;
787
988
  </button>
788
989
 
789
- <div class="calendar--picker-container" data-picker-container>
990
+ <div class="calendar-picker-container" data-picker-container>
790
991
  <button
791
992
  type="button"
792
- class="calendar--picker-btn"
993
+ class="calendar-picker-btn"
793
994
  data-action="toggle-picker"
794
995
  aria-expanded="${this.pickerOpen ? "true" : "false"}"
795
996
  aria-haspopup="true"
796
997
  >
797
998
  ${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
798
- <span class="calendar--picker-chevron">&#9662;</span>
999
+ <span class="calendar-picker-chevron">&#9662;</span>
799
1000
  </button>
800
1001
 
801
1002
  ${this.pickerOpen ? `
802
- <div class="calendar--picker-dropdown">
803
- <div class="calendar--picker-year-row">
1003
+ <div class="calendar-picker-dropdown">
1004
+ <div class="calendar-picker-year-row">
804
1005
  <button
805
1006
  type="button"
806
- class="calendar--picker-year-arrow"
1007
+ class="calendar-picker-year-arrow"
807
1008
  data-action="year-prev"
808
1009
  ${viewModel.currentYear <= minYear ? "disabled" : ""}
809
1010
  aria-label="Previous year"
810
1011
  >&#8249;</button>
811
1012
  <input
812
1013
  type="text"
813
- class="calendar--picker-year-input${!this.yearInputValid ? " invalid" : ""}"
814
- value="${this.yearInput || viewModel.currentYear}"
1014
+ class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
1015
+ value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
815
1016
  data-year-input
816
1017
  aria-label="Year"
817
1018
  />
818
1019
  <button
819
1020
  type="button"
820
- class="calendar--picker-year-arrow"
1021
+ class="calendar-picker-year-arrow"
821
1022
  data-action="year-next"
822
1023
  ${viewModel.currentYear >= maxYear ? "disabled" : ""}
823
1024
  aria-label="Next year"
824
1025
  >&#8250;</button>
825
1026
  </div>
826
1027
 
827
- <div class="calendar--picker-months">
1028
+ <div class="calendar-picker-months">
828
1029
  ${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
829
1030
  const isSelected = index === viewModel.currentMonth;
830
1031
  const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
831
1032
  return `
832
1033
  <button
833
1034
  type="button"
834
- class="calendar--picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
1035
+ class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
835
1036
  data-action="select-month"
836
1037
  data-month="${index}"
837
1038
  >${month}</button>
@@ -844,70 +1045,22 @@ var Kalendly = (() => {
844
1045
 
845
1046
  <button
846
1047
  type="button"
847
- class="calendar--today-btn"
1048
+ class="calendar-today-btn"
848
1049
  data-action="today"
849
1050
  ${isCurrentMonth ? "disabled" : ""}
850
1051
  >Today</button>
851
1052
 
852
- <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">
853
1054
  &#8250;
854
1055
  </button>
855
1056
  </div>
856
1057
 
857
- <table class="calendar--table calendar--table--bordered">
858
- <thead>
859
- <tr>
860
- ${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
861
- </tr>
862
- </thead>
863
- <tbody data-calendar-body>
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(
871
- (week) => `
872
- <tr>
873
- ${week.map((calendarDate, dayIndex) => {
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
- }
891
- const dateString = calendarDate.date.toISOString();
892
- return `
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
- `;
902
- }).join("")}
903
- </tr>
904
- `
905
- ).join("")}
906
- </tbody>
907
- </table>
1058
+ <div class="calendar-panes">
1059
+ ${viewModel.panes.map(renderPane).join("")}
1060
+ </div>
908
1061
 
909
1062
  ${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
910
- <div class="date-popup ${viewModel.popupPositionClass}">
1063
+ <div class="date-popup">
911
1064
  <div class="popup-header">
912
1065
  <h2>${viewModel.scheduleDay}</h2>
913
1066
  <button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
@@ -945,8 +1098,7 @@ var Kalendly = (() => {
945
1098
  const availMode = this.getAttribute("availability-mode");
946
1099
  const isSelectable = this.hasAttribute("selectable");
947
1100
  if (availMode === "day" && isSelectable) {
948
- const isBooked = cell.classList.contains("availability--booked");
949
- if (!isBooked) {
1101
+ if (this.isDateSelectable(date)) {
950
1102
  let startDate;
951
1103
  let endDate;
952
1104
  if (this._rangeEnd !== null) {
@@ -964,7 +1116,7 @@ var Kalendly = (() => {
964
1116
  cursor.setDate(cursor.getDate() + 1);
965
1117
  let blocked = false;
966
1118
  while (cursor < e2) {
967
- if (this.engine.getEventsForDate(cursor).length > 0) {
1119
+ if (!this.isDateSelectable(cursor)) {
968
1120
  blocked = true;
969
1121
  break;
970
1122
  }
@@ -1198,31 +1350,84 @@ var Kalendly = (() => {
1198
1350
  this.theme = theme;
1199
1351
  }
1200
1352
  getCurrentDate() {
1353
+ this.rethrowInitError();
1201
1354
  return this.engine?.getViewModel().selectedDate ?? null;
1202
1355
  }
1203
1356
  goToDate(date) {
1357
+ this.rethrowInitError();
1204
1358
  const year = date.getFullYear();
1205
1359
  const month = date.getMonth();
1206
1360
  this.dispatchMonthChange(year, month);
1207
1361
  this.actions?.jump(year, month);
1208
1362
  }
1209
1363
  getEngine() {
1364
+ this.rethrowInitError();
1210
1365
  if (!this.engine)
1211
1366
  throw new Error("CalendarElement is not connected to the DOM");
1212
1367
  return this.engine;
1213
1368
  }
1214
1369
  };
1215
- CalendarElement.observedAttributes = [
1370
+ _CalendarElement.observedAttributes = [
1216
1371
  "initial-date",
1217
1372
  "min-year",
1218
1373
  "max-year",
1219
1374
  "week-starts-on",
1375
+ "heading",
1376
+ "months",
1220
1377
  "title",
1221
1378
  "use-short-month-names",
1222
1379
  "availability-mode",
1223
1380
  "selectable",
1224
1381
  "loading"
1225
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;
1226
1431
  function defineCalendarElement(tagName = "kal-calendar") {
1227
1432
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1228
1433
  customElements.define(tagName, CalendarElement);