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/README.md +202 -29
- package/dist/core/index.d.mts +48 -10
- package/dist/core/index.d.ts +48 -10
- package/dist/core/index.js +60 -26
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +56 -25
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.d.mts +74 -10
- package/dist/index.d.ts +74 -10
- package/dist/index.js +373 -162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -161
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +363 -158
- package/dist/index.umd.js.map +1 -1
- package/dist/styles/calendar.css +445 -395
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -95,26 +95,16 @@ function generateCalendarDates(year, month, events = [], weekStartsOn = 0) {
|
|
|
95
95
|
}
|
|
96
96
|
return dates;
|
|
97
97
|
}
|
|
98
|
-
function getPopupPositionClass(selectedDayIndex) {
|
|
99
|
-
if (selectedDayIndex === null) return "popup-center-bottom";
|
|
100
|
-
if (selectedDayIndex < 3) {
|
|
101
|
-
return "popup-right";
|
|
102
|
-
} else if (selectedDayIndex > 4) {
|
|
103
|
-
return "popup-left";
|
|
104
|
-
} else {
|
|
105
|
-
return "popup-center-bottom";
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
98
|
function getCellClasses(calendarDate) {
|
|
109
99
|
const classes = [];
|
|
110
100
|
if (!calendarDate.isCurrentMonth) {
|
|
111
|
-
classes.push("other-month");
|
|
101
|
+
classes.push("calendar-cell-other-month");
|
|
112
102
|
}
|
|
113
103
|
if (calendarDate.isToday) {
|
|
114
|
-
classes.push("
|
|
104
|
+
classes.push("calendar-cell-today");
|
|
115
105
|
}
|
|
116
106
|
if (calendarDate.hasEvents) {
|
|
117
|
-
classes.push("has
|
|
107
|
+
classes.push("calendar-cell-has-event");
|
|
118
108
|
}
|
|
119
109
|
return classes;
|
|
120
110
|
}
|
|
@@ -173,7 +163,24 @@ function getCategoryColor(category, customColors) {
|
|
|
173
163
|
const color = colorMap[category] || colorMap.other || "#fc8917";
|
|
174
164
|
return isValidHexColor(color) ? color : "#fc8917";
|
|
175
165
|
}
|
|
176
|
-
|
|
166
|
+
function escapeHtml(value) {
|
|
167
|
+
if (value === null || value === void 0) return "";
|
|
168
|
+
return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
|
|
169
|
+
}
|
|
170
|
+
function slugifyToken(value) {
|
|
171
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
172
|
+
}
|
|
173
|
+
function safeUrl(value) {
|
|
174
|
+
const normalized = String(value).split("").filter((char) => char.charCodeAt(0) > 32).join("");
|
|
175
|
+
if (/^(?:https?:|mailto:)/i.test(normalized)) return normalized;
|
|
176
|
+
const schemeless = !/^[^/?#]*:/.test(normalized);
|
|
177
|
+
return schemeless ? normalized : "#";
|
|
178
|
+
}
|
|
179
|
+
function safeColor(value) {
|
|
180
|
+
const trimmed = String(value).trim();
|
|
181
|
+
return isValidHexColor(trimmed) || /^[a-z]+$/i.test(trimmed) ? trimmed : "#3b82f6";
|
|
182
|
+
}
|
|
183
|
+
var MONTHS, MONTHS_FULL, DAYS, DEFAULT_CATEGORY_COLORS, HTML_ESCAPES;
|
|
177
184
|
var init_utils = __esm({
|
|
178
185
|
"src/core/utils.ts"() {
|
|
179
186
|
"use strict";
|
|
@@ -222,6 +229,13 @@ var init_utils = __esm({
|
|
|
222
229
|
appointment: "#f59e0b",
|
|
223
230
|
other: "#6b7280"
|
|
224
231
|
};
|
|
232
|
+
HTML_ESCAPES = {
|
|
233
|
+
"&": "&",
|
|
234
|
+
"<": "<",
|
|
235
|
+
">": ">",
|
|
236
|
+
'"': """,
|
|
237
|
+
"'": "'"
|
|
238
|
+
};
|
|
225
239
|
}
|
|
226
240
|
});
|
|
227
241
|
|
|
@@ -269,24 +283,38 @@ var init_calendar_engine = __esm({
|
|
|
269
283
|
* Get view model with computed properties
|
|
270
284
|
*/
|
|
271
285
|
getViewModel() {
|
|
272
|
-
const
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
const panes = Array.from(
|
|
287
|
+
{ length: Math.max(1, this.config.monthCount ?? 1) },
|
|
288
|
+
(_, offset) => {
|
|
289
|
+
const anchor = new Date(
|
|
290
|
+
this.state.currentYear,
|
|
291
|
+
this.state.currentMonth + offset,
|
|
292
|
+
1
|
|
293
|
+
);
|
|
294
|
+
const year = anchor.getFullYear();
|
|
295
|
+
const month = anchor.getMonth();
|
|
296
|
+
return {
|
|
297
|
+
year,
|
|
298
|
+
month,
|
|
299
|
+
monthAndYearText: getMonthYearText(year, month),
|
|
300
|
+
calendarDates: generateCalendarDates(
|
|
301
|
+
year,
|
|
302
|
+
month,
|
|
303
|
+
this.config.events,
|
|
304
|
+
this.config.weekStartsOn
|
|
305
|
+
)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
277
308
|
);
|
|
278
309
|
return {
|
|
279
310
|
...this.state,
|
|
280
311
|
months: MONTHS,
|
|
281
312
|
days: DAYS,
|
|
282
313
|
years: generateYears(this.config.minYear, this.config.maxYear),
|
|
283
|
-
monthAndYearText:
|
|
284
|
-
this.state.currentYear,
|
|
285
|
-
this.state.currentMonth
|
|
286
|
-
),
|
|
314
|
+
monthAndYearText: panes[0].monthAndYearText,
|
|
287
315
|
scheduleDay: this.state.selectedDate ? formatDateForDisplay(this.state.selectedDate) : "",
|
|
288
|
-
|
|
289
|
-
|
|
316
|
+
panes,
|
|
317
|
+
calendarDates: panes[0].calendarDates
|
|
290
318
|
};
|
|
291
319
|
}
|
|
292
320
|
/**
|
|
@@ -482,13 +510,13 @@ function defineCalendarElement(tagName = "kal-calendar") {
|
|
|
482
510
|
customElements.define(tagName, CalendarElement);
|
|
483
511
|
}
|
|
484
512
|
}
|
|
485
|
-
var isSameDay2, CalendarElement;
|
|
513
|
+
var isSameDay2, _CalendarElement, CalendarElement;
|
|
486
514
|
var init_CalendarElement = __esm({
|
|
487
515
|
"src/web-components/CalendarElement.ts"() {
|
|
488
516
|
"use strict";
|
|
489
517
|
init_core();
|
|
490
518
|
isSameDay2 = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
491
|
-
|
|
519
|
+
_CalendarElement = class _CalendarElement extends HTMLElement {
|
|
492
520
|
constructor() {
|
|
493
521
|
super(...arguments);
|
|
494
522
|
this.engine = null;
|
|
@@ -509,6 +537,10 @@ var init_CalendarElement = __esm({
|
|
|
509
537
|
this._categoryColors = null;
|
|
510
538
|
this._renderEvent = null;
|
|
511
539
|
this._renderNoEvents = null;
|
|
540
|
+
this._availabilityColors = null;
|
|
541
|
+
this._selectableStatuses = null;
|
|
542
|
+
this._initError = null;
|
|
543
|
+
this.pendingUpdate = null;
|
|
512
544
|
// Selection state (availability mode — range)
|
|
513
545
|
this._rangeStart = null;
|
|
514
546
|
this._rangeEnd = null;
|
|
@@ -521,6 +553,7 @@ var init_CalendarElement = __esm({
|
|
|
521
553
|
return this._events;
|
|
522
554
|
}
|
|
523
555
|
set events(val) {
|
|
556
|
+
this._initError = null;
|
|
524
557
|
this._events = val;
|
|
525
558
|
if (this.engine) {
|
|
526
559
|
this.engine.updateEvents(val);
|
|
@@ -536,13 +569,28 @@ var init_CalendarElement = __esm({
|
|
|
536
569
|
this.engine.updateCategoryColors(val);
|
|
537
570
|
}
|
|
538
571
|
}
|
|
572
|
+
get availabilityColors() {
|
|
573
|
+
return this._availabilityColors ?? {};
|
|
574
|
+
}
|
|
575
|
+
set availabilityColors(val) {
|
|
576
|
+
this._initError = null;
|
|
577
|
+
this._availabilityColors = val;
|
|
578
|
+
if (this.engine) this.scheduleRender();
|
|
579
|
+
}
|
|
580
|
+
get selectableStatuses() {
|
|
581
|
+
return this._selectableStatuses ?? [];
|
|
582
|
+
}
|
|
583
|
+
set selectableStatuses(val) {
|
|
584
|
+
this._selectableStatuses = val;
|
|
585
|
+
if (this.engine) this.scheduleRender();
|
|
586
|
+
}
|
|
539
587
|
set renderEvent(val) {
|
|
540
588
|
this._renderEvent = val;
|
|
541
|
-
if (this.engine) this.
|
|
589
|
+
if (this.engine) this.scheduleRender();
|
|
542
590
|
}
|
|
543
591
|
set renderNoEvents(val) {
|
|
544
592
|
this._renderNoEvents = val;
|
|
545
|
-
if (this.engine) this.
|
|
593
|
+
if (this.engine) this.scheduleRender();
|
|
546
594
|
}
|
|
547
595
|
get loading() {
|
|
548
596
|
return this.hasAttribute("loading");
|
|
@@ -551,10 +599,22 @@ var init_CalendarElement = __esm({
|
|
|
551
599
|
if (val) this.setAttribute("loading", "");
|
|
552
600
|
else this.removeAttribute("loading");
|
|
553
601
|
}
|
|
602
|
+
// Custom element reactions report rather than propagate, so a failure here
|
|
603
|
+
// is kept and resurfaced at the next call the integrator makes
|
|
604
|
+
reaction(run) {
|
|
605
|
+
try {
|
|
606
|
+
run();
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this._initError = error;
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
554
612
|
connectedCallback() {
|
|
555
613
|
this.classList.add("kalendly-calendar");
|
|
556
|
-
this.
|
|
557
|
-
|
|
614
|
+
this.reaction(() => {
|
|
615
|
+
this.initEngine();
|
|
616
|
+
this.render();
|
|
617
|
+
});
|
|
558
618
|
}
|
|
559
619
|
disconnectedCallback() {
|
|
560
620
|
this.cleanup();
|
|
@@ -564,7 +624,7 @@ var init_CalendarElement = __esm({
|
|
|
564
624
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
565
625
|
if (oldVal === newVal) return;
|
|
566
626
|
if (name === "loading") {
|
|
567
|
-
this.
|
|
627
|
+
this.scheduleRender();
|
|
568
628
|
return;
|
|
569
629
|
}
|
|
570
630
|
if (name === "selectable" && newVal === null) {
|
|
@@ -577,6 +637,23 @@ var init_CalendarElement = __esm({
|
|
|
577
637
|
}
|
|
578
638
|
this.reinit();
|
|
579
639
|
}
|
|
640
|
+
get headingText() {
|
|
641
|
+
const heading = this.getAttribute("heading");
|
|
642
|
+
if (heading !== null) return heading;
|
|
643
|
+
const title = this.getAttribute("title");
|
|
644
|
+
if (title !== null && !_CalendarElement.titleDeprecationWarned) {
|
|
645
|
+
_CalendarElement.titleDeprecationWarned = true;
|
|
646
|
+
console.warn(
|
|
647
|
+
`<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.`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return title;
|
|
651
|
+
}
|
|
652
|
+
get monthCount() {
|
|
653
|
+
const requested = Number(this.getAttribute("months") ?? 1);
|
|
654
|
+
if (!Number.isInteger(requested) || requested < 1) return 1;
|
|
655
|
+
return Math.min(requested, 2);
|
|
656
|
+
}
|
|
580
657
|
get minYear() {
|
|
581
658
|
const val = this.getAttribute("min-year");
|
|
582
659
|
return val ? parseInt(val, 10) : (/* @__PURE__ */ new Date()).getFullYear() - 30;
|
|
@@ -596,19 +673,20 @@ var init_CalendarElement = __esm({
|
|
|
596
673
|
minYear: this.minYear,
|
|
597
674
|
maxYear: this.maxYear,
|
|
598
675
|
weekStartsOn,
|
|
599
|
-
categoryColors: this._categoryColors ?? void 0
|
|
676
|
+
categoryColors: this._categoryColors ?? void 0,
|
|
677
|
+
monthCount: this.monthCount
|
|
600
678
|
});
|
|
601
679
|
this.actions = this.engine.getActions();
|
|
602
680
|
this.applyTheme();
|
|
603
681
|
this.unsubscribe = this.engine.subscribe(() => {
|
|
604
|
-
this.
|
|
682
|
+
this.scheduleRender();
|
|
605
683
|
});
|
|
606
684
|
}
|
|
607
685
|
reinit() {
|
|
608
686
|
if (!this.engine) return;
|
|
609
687
|
this.cleanup();
|
|
610
688
|
this.initEngine();
|
|
611
|
-
this.
|
|
689
|
+
this.scheduleRender();
|
|
612
690
|
}
|
|
613
691
|
cleanup() {
|
|
614
692
|
if (this.unsubscribe) {
|
|
@@ -645,42 +723,84 @@ var init_CalendarElement = __esm({
|
|
|
645
723
|
applyTheme() {
|
|
646
724
|
if (!this._theme) return;
|
|
647
725
|
const root = document.documentElement;
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
root.style.setProperty(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if (
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
726
|
+
for (const key of Object.keys(_CalendarElement.themeMap)) {
|
|
727
|
+
const value = this._theme[key];
|
|
728
|
+
if (value) root.style.setProperty(_CalendarElement.themeMap[key], value);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
get knownBuckets() {
|
|
732
|
+
return [
|
|
733
|
+
..._CalendarElement.BUILT_IN_BUCKETS,
|
|
734
|
+
...Object.keys(this._availabilityColors ?? {})
|
|
735
|
+
];
|
|
736
|
+
}
|
|
737
|
+
get updateComplete() {
|
|
738
|
+
return this.pendingUpdate ?? Promise.resolve();
|
|
739
|
+
}
|
|
740
|
+
scheduleRender() {
|
|
741
|
+
if (this.pendingUpdate) return;
|
|
742
|
+
this.pendingUpdate = Promise.resolve().then(() => {
|
|
743
|
+
this.pendingUpdate = null;
|
|
744
|
+
try {
|
|
745
|
+
this.render();
|
|
746
|
+
} catch (error) {
|
|
747
|
+
this._initError = error;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
rethrowInitError() {
|
|
753
|
+
if (this._initError) throw this._initError;
|
|
754
|
+
}
|
|
755
|
+
assertStatusesDeclared() {
|
|
756
|
+
if (!this.getAttribute("availability-mode")) return;
|
|
757
|
+
const missing = this._events.filter(
|
|
758
|
+
(event) => typeof event.availabilityStatus !== "string" || event.availabilityStatus === ""
|
|
759
|
+
).map((event) => String(event.id));
|
|
760
|
+
if (missing.length) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
`<kal-calendar> availability-mode requires availabilityStatus on every event. Missing on: ${missing.join(", ")}.`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
assertStatusesKnown() {
|
|
767
|
+
const known = new Set(this.knownBuckets);
|
|
768
|
+
const unknown = this._events.filter((event) => !known.has(event.availabilityStatus)).map((event) => `${event.id} (${event.availabilityStatus})`);
|
|
769
|
+
if (unknown.length) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`<kal-calendar> availabilityStatus must name a built-in bucket (${_CalendarElement.BUILT_IN_BUCKETS.join(", ")}) or a key of availabilityColors. Unrecognised on: ${unknown.join(", ")}.`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
resolveBucket(date) {
|
|
776
|
+
if (!this.engine) return "open";
|
|
777
|
+
const declared = new Set(
|
|
778
|
+
this.engine.getEventsForDate(date).map((event) => event.availabilityStatus)
|
|
779
|
+
);
|
|
780
|
+
if (declared.size === 0) return "open";
|
|
781
|
+
return this.knownBuckets.find((bucket) => declared.has(bucket));
|
|
782
|
+
}
|
|
783
|
+
isDateSelectable(date) {
|
|
784
|
+
if (!this.engine) return false;
|
|
785
|
+
if (this._selectableStatuses) {
|
|
786
|
+
return this._selectableStatuses.includes(this.resolveBucket(date));
|
|
787
|
+
}
|
|
788
|
+
return this.engine.getEventsForDate(date).length === 0;
|
|
673
789
|
}
|
|
674
790
|
render() {
|
|
675
791
|
if (!this.engine) return;
|
|
676
792
|
const viewModel = this.engine.getViewModel();
|
|
677
793
|
const useShortMonths = this.hasAttribute("use-short-month-names");
|
|
678
|
-
const title = this.
|
|
794
|
+
const title = this.headingText;
|
|
679
795
|
const minYear = this.minYear;
|
|
680
796
|
const maxYear = this.maxYear;
|
|
681
797
|
const availabilityMode = this.getAttribute("availability-mode");
|
|
682
798
|
const selectable = this.hasAttribute("selectable");
|
|
683
799
|
const isLoading = this.loading;
|
|
800
|
+
if (availabilityMode) {
|
|
801
|
+
this.assertStatusesDeclared();
|
|
802
|
+
this.assertStatusesKnown();
|
|
803
|
+
}
|
|
684
804
|
const today = /* @__PURE__ */ new Date();
|
|
685
805
|
const todayMonth = today.getMonth();
|
|
686
806
|
const todayYear = today.getFullYear();
|
|
@@ -689,7 +809,7 @@ var init_CalendarElement = __esm({
|
|
|
689
809
|
const defaultRenderEvent = (event) => {
|
|
690
810
|
const timeRange = formatTimeRange(event);
|
|
691
811
|
const attendeesList = formatAttendees(event.attendees);
|
|
692
|
-
let borderColor = event.color || "#3b82f6";
|
|
812
|
+
let borderColor = safeColor(event.color || "#3b82f6");
|
|
693
813
|
if (event.category) {
|
|
694
814
|
borderColor = this.engine.getCategoryColor(event.category);
|
|
695
815
|
}
|
|
@@ -724,56 +844,56 @@ var init_CalendarElement = __esm({
|
|
|
724
844
|
return `
|
|
725
845
|
<div class="event-card" style="border-left-color: ${borderColor}">
|
|
726
846
|
<div class="event-header">
|
|
727
|
-
<div class="event-title">${event.name}</div>
|
|
847
|
+
<div class="event-title">${escapeHtml(event.name)}</div>
|
|
728
848
|
<div class="event-badges">
|
|
729
|
-
${event.category ? `<span class="badge category-${event.category}">${getCategoryLabel(event.category)}</span>` : ""}
|
|
730
|
-
${event.priority ? `<span class="badge priority-${event.priority}">${getPriorityLabel(event.priority)}</span>` : ""}
|
|
731
|
-
${event.status && event.status !== "scheduled" ? `<span class="badge status-${event.status}">${getStatusLabel(event.status)}</span>` : ""}
|
|
849
|
+
${event.category ? `<span class="badge category category-${slugifyToken(event.category)}">${escapeHtml(getCategoryLabel(event.category))}</span>` : ""}
|
|
850
|
+
${event.priority ? `<span class="badge priority priority-${slugifyToken(event.priority)}">${escapeHtml(getPriorityLabel(event.priority))}</span>` : ""}
|
|
851
|
+
${event.status && event.status !== "scheduled" ? `<span class="badge status status-${slugifyToken(event.status)}">${escapeHtml(getStatusLabel(event.status))}</span>` : ""}
|
|
732
852
|
</div>
|
|
733
853
|
</div>
|
|
734
854
|
|
|
735
855
|
${timeRange ? `
|
|
736
856
|
<div class="event-time">
|
|
737
857
|
<span class="event-time-label">Time:</span>
|
|
738
|
-
<span class="event-time-value">${timeRange}</span>
|
|
858
|
+
<span class="event-time-value">${escapeHtml(timeRange)}</span>
|
|
739
859
|
</div>
|
|
740
860
|
` : ""}
|
|
741
861
|
|
|
742
862
|
${event.description ? `
|
|
743
|
-
<div class="event-description">${event.description}</div>
|
|
863
|
+
<div class="event-description">${escapeHtml(event.description)}</div>
|
|
744
864
|
` : ""}
|
|
745
865
|
|
|
746
866
|
${event.location ? `
|
|
747
867
|
<div class="event-time">
|
|
748
868
|
<span class="event-time-label">Location:</span>
|
|
749
|
-
<span class="event-time-value">${event.location}</span>
|
|
869
|
+
<span class="event-time-value">${escapeHtml(event.location)}</span>
|
|
750
870
|
</div>
|
|
751
871
|
` : ""}
|
|
752
872
|
|
|
753
873
|
${attendeesList ? `
|
|
754
874
|
<div class="event-time">
|
|
755
875
|
<span class="event-time-label">Attendees:</span>
|
|
756
|
-
<span class="event-time-value">${attendeesList}</span>
|
|
876
|
+
<span class="event-time-value">${escapeHtml(attendeesList)}</span>
|
|
757
877
|
</div>
|
|
758
878
|
` : ""}
|
|
759
879
|
|
|
760
880
|
${event.organizer ? `
|
|
761
881
|
<div class="event-time">
|
|
762
882
|
<span class="event-time-label">Organizer:</span>
|
|
763
|
-
<span class="event-time-value">${event.organizer}</span>
|
|
883
|
+
<span class="event-time-value">${escapeHtml(event.organizer)}</span>
|
|
764
884
|
</div>
|
|
765
885
|
` : ""}
|
|
766
886
|
|
|
767
887
|
${event.notes ? `
|
|
768
888
|
<div class="event-time">
|
|
769
889
|
<span class="event-time-label">Notes:</span>
|
|
770
|
-
<span class="event-time-value">${event.notes}</span>
|
|
890
|
+
<span class="event-time-value">${escapeHtml(event.notes)}</span>
|
|
771
891
|
</div>
|
|
772
892
|
` : ""}
|
|
773
893
|
|
|
774
894
|
${event.url ? `
|
|
775
895
|
<div class="event-time">
|
|
776
|
-
<a href="${event.url}" target="_blank" rel="noopener noreferrer" class="event-link">
|
|
896
|
+
<a href="${escapeHtml(safeUrl(event.url))}" target="_blank" rel="noopener noreferrer" class="event-link">
|
|
777
897
|
View Details \u2192
|
|
778
898
|
</a>
|
|
779
899
|
</div>
|
|
@@ -781,7 +901,7 @@ var init_CalendarElement = __esm({
|
|
|
781
901
|
|
|
782
902
|
${event.tags && event.tags.length > 0 ? `
|
|
783
903
|
<div class="event-tags">
|
|
784
|
-
${event.tags.map((tag) => `<span class="event-tag">${tag}</span>`).join("")}
|
|
904
|
+
${event.tags.map((tag) => `<span class="event-tag">${escapeHtml(tag)}</span>`).join("")}
|
|
785
905
|
</div>
|
|
786
906
|
` : ""}
|
|
787
907
|
</div>
|
|
@@ -791,11 +911,17 @@ var init_CalendarElement = __esm({
|
|
|
791
911
|
const renderEvent = this._renderEvent || defaultRenderEvent;
|
|
792
912
|
const renderNoEvents = this._renderNoEvents || defaultRenderNoEvents;
|
|
793
913
|
const renderTimeGrid = (events, date) => {
|
|
914
|
+
const startHour = (time) => {
|
|
915
|
+
if (typeof time !== "string") return null;
|
|
916
|
+
const hour = Number(time.split(":")[0]);
|
|
917
|
+
return Number.isInteger(hour) && hour >= 0 && hour <= 24 ? hour : null;
|
|
918
|
+
};
|
|
794
919
|
const isHourBooked = (hour) => events.some((event) => {
|
|
795
|
-
if (!event.startTime
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
|
|
920
|
+
if (!event.startTime) return true;
|
|
921
|
+
const from = startHour(event.startTime);
|
|
922
|
+
const to = event.endTime ? startHour(event.endTime) : 24;
|
|
923
|
+
if (from === null || to === null) return true;
|
|
924
|
+
return hour >= from && hour < to;
|
|
799
925
|
});
|
|
800
926
|
const slots = Array.from({ length: 24 }, (_, hour) => {
|
|
801
927
|
const startTime = `${String(hour).padStart(2, "0")}:00`;
|
|
@@ -806,81 +932,156 @@ var init_CalendarElement = __esm({
|
|
|
806
932
|
const isRangeEnd = inTimeRange && endTime === this._timeRangeEnd;
|
|
807
933
|
const isInRange = inTimeRange && !isRangeStart && !isRangeEnd;
|
|
808
934
|
const slotClasses = [
|
|
809
|
-
"time-
|
|
810
|
-
booked ? "time-
|
|
811
|
-
isRangeStart ? "time-
|
|
812
|
-
isRangeEnd ? "time-
|
|
813
|
-
isInRange ? "time-
|
|
935
|
+
"time-grid-slot",
|
|
936
|
+
booked ? "time-grid-slot-blocked" : "time-grid-slot-open",
|
|
937
|
+
isRangeStart ? "time-grid-slot-range-start" : "",
|
|
938
|
+
isRangeEnd ? "time-grid-slot-range-end" : "",
|
|
939
|
+
isInRange ? "time-grid-slot-in-range" : ""
|
|
814
940
|
].filter(Boolean).join(" ");
|
|
815
941
|
const slotAttrs = !booked && selectable ? `data-action="select-slot" data-start-time="${startTime}" data-end-time="${endTime}" data-date="${date.toISOString()}"` : "";
|
|
816
942
|
return `
|
|
817
943
|
<div class="${slotClasses}" ${slotAttrs}>
|
|
818
|
-
<span class="time-
|
|
819
|
-
<span class="time-
|
|
944
|
+
<span class="time-grid-label">${startTime}</span>
|
|
945
|
+
<span class="time-grid-status">${booked ? "Booked" : "Available"}</span>
|
|
820
946
|
</div>`;
|
|
821
947
|
});
|
|
822
948
|
return `<div class="time-grid">${slots.join("")}</div>`;
|
|
823
949
|
};
|
|
950
|
+
const multiMonth = viewModel.panes.length > 1;
|
|
951
|
+
const renderPane = (pane) => `
|
|
952
|
+
<div class="calendar-pane">
|
|
953
|
+
${multiMonth ? `<div class="calendar-pane-caption">${escapeHtml(pane.monthAndYearText)}</div>` : ""}
|
|
954
|
+
<table class="calendar-table calendar-table-bordered">
|
|
955
|
+
<thead>
|
|
956
|
+
<tr>
|
|
957
|
+
${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
|
|
958
|
+
</tr>
|
|
959
|
+
</thead>
|
|
960
|
+
<tbody data-calendar-body>
|
|
961
|
+
${isLoading ? Array.from(
|
|
962
|
+
{ length: 6 },
|
|
963
|
+
() => `<tr>${Array.from(
|
|
964
|
+
{ length: 7 },
|
|
965
|
+
() => `<td class="calendar-skeleton" aria-hidden="true"></td>`
|
|
966
|
+
).join("")}</tr>`
|
|
967
|
+
).join("") : pane.calendarDates.map(
|
|
968
|
+
(week) => `
|
|
969
|
+
<tr>
|
|
970
|
+
${week.map((calendarDate, dayIndex) => {
|
|
971
|
+
const classes = getCellClasses(calendarDate);
|
|
972
|
+
const cellAttrs = [];
|
|
973
|
+
if (availabilityMode && calendarDate.isCurrentMonth) {
|
|
974
|
+
const bucket = this.resolveBucket(
|
|
975
|
+
calendarDate.date
|
|
976
|
+
);
|
|
977
|
+
const custom = (this._availabilityColors ?? {})[bucket];
|
|
978
|
+
classes.push(
|
|
979
|
+
`availability-${slugifyToken(bucket)}`
|
|
980
|
+
);
|
|
981
|
+
if (custom) {
|
|
982
|
+
classes.push("availability-status");
|
|
983
|
+
cellAttrs.push(
|
|
984
|
+
`style="--availability-color: ${escapeHtml(safeColor(custom))}"`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (!this.isDateSelectable(calendarDate.date)) {
|
|
988
|
+
classes.push("availability-unselectable");
|
|
989
|
+
}
|
|
990
|
+
cellAttrs.push(
|
|
991
|
+
`aria-label="${escapeHtml(bucket)}"`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
|
|
995
|
+
const d = calendarDate.date;
|
|
996
|
+
if (this._rangeStart && isSameDay2(d, this._rangeStart))
|
|
997
|
+
classes.push("availability-range-start");
|
|
998
|
+
if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
|
|
999
|
+
classes.push("availability-range-end");
|
|
1000
|
+
if (this._rangeStart && this._rangeEnd) {
|
|
1001
|
+
if (d > this._rangeStart && d < this._rangeEnd)
|
|
1002
|
+
classes.push("availability-in-range");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const dateString = calendarDate.date.toISOString();
|
|
1006
|
+
return `
|
|
1007
|
+
<td
|
|
1008
|
+
class="${classes.join(" ")}"
|
|
1009
|
+
data-date="${dateString}"
|
|
1010
|
+
data-day-index="${dayIndex}"
|
|
1011
|
+
data-clickable="true"
|
|
1012
|
+
${cellAttrs.join(" ")}
|
|
1013
|
+
>
|
|
1014
|
+
${calendarDate.date.getDate()}
|
|
1015
|
+
</td>
|
|
1016
|
+
`;
|
|
1017
|
+
}).join("")}
|
|
1018
|
+
</tr>
|
|
1019
|
+
`
|
|
1020
|
+
).join("")}
|
|
1021
|
+
</tbody>
|
|
1022
|
+
</table>
|
|
1023
|
+
</div>
|
|
1024
|
+
`;
|
|
824
1025
|
const html = `
|
|
825
1026
|
${title ? `
|
|
826
|
-
<div class="
|
|
827
|
-
<h1>${title}</h1>
|
|
1027
|
+
<div class="calendar-title">
|
|
1028
|
+
<h1>${escapeHtml(title)}</h1>
|
|
828
1029
|
</div>
|
|
829
1030
|
` : ""}
|
|
830
1031
|
|
|
831
|
-
<div class="calendar
|
|
832
|
-
<div class="calendar
|
|
833
|
-
<div class="calendar
|
|
834
|
-
<button type="button" class="calendar
|
|
1032
|
+
<div class="calendar-content">
|
|
1033
|
+
<div class="calendar-card">
|
|
1034
|
+
<div class="calendar-nav-header">
|
|
1035
|
+
<button type="button" class="calendar-nav-arrow" data-action="previous" aria-label="Previous month">
|
|
835
1036
|
‹
|
|
836
1037
|
</button>
|
|
837
1038
|
|
|
838
|
-
<div class="calendar
|
|
1039
|
+
<div class="calendar-picker-container" data-picker-container>
|
|
839
1040
|
<button
|
|
840
1041
|
type="button"
|
|
841
|
-
class="calendar
|
|
1042
|
+
class="calendar-picker-btn"
|
|
842
1043
|
data-action="toggle-picker"
|
|
843
1044
|
aria-expanded="${this.pickerOpen ? "true" : "false"}"
|
|
844
1045
|
aria-haspopup="true"
|
|
845
1046
|
>
|
|
846
1047
|
${useShortMonths ? `${MONTHS[viewModel.currentMonth]} ${viewModel.currentYear}` : viewModel.monthAndYearText}
|
|
847
|
-
<span class="calendar
|
|
1048
|
+
<span class="calendar-picker-chevron">▾</span>
|
|
848
1049
|
</button>
|
|
849
1050
|
|
|
850
1051
|
${this.pickerOpen ? `
|
|
851
|
-
<div class="calendar
|
|
852
|
-
<div class="calendar
|
|
1052
|
+
<div class="calendar-picker-dropdown">
|
|
1053
|
+
<div class="calendar-picker-year-row">
|
|
853
1054
|
<button
|
|
854
1055
|
type="button"
|
|
855
|
-
class="calendar
|
|
1056
|
+
class="calendar-picker-year-arrow"
|
|
856
1057
|
data-action="year-prev"
|
|
857
1058
|
${viewModel.currentYear <= minYear ? "disabled" : ""}
|
|
858
1059
|
aria-label="Previous year"
|
|
859
1060
|
>‹</button>
|
|
860
1061
|
<input
|
|
861
1062
|
type="text"
|
|
862
|
-
class="calendar
|
|
863
|
-
value="${this.yearInput || viewModel.currentYear}"
|
|
1063
|
+
class="calendar-picker-year-input${!this.yearInputValid ? " invalid" : ""}"
|
|
1064
|
+
value="${escapeHtml(this.yearInput || viewModel.currentYear)}"
|
|
864
1065
|
data-year-input
|
|
865
1066
|
aria-label="Year"
|
|
866
1067
|
/>
|
|
867
1068
|
<button
|
|
868
1069
|
type="button"
|
|
869
|
-
class="calendar
|
|
1070
|
+
class="calendar-picker-year-arrow"
|
|
870
1071
|
data-action="year-next"
|
|
871
1072
|
${viewModel.currentYear >= maxYear ? "disabled" : ""}
|
|
872
1073
|
aria-label="Next year"
|
|
873
1074
|
>›</button>
|
|
874
1075
|
</div>
|
|
875
1076
|
|
|
876
|
-
<div class="calendar
|
|
1077
|
+
<div class="calendar-picker-months">
|
|
877
1078
|
${(useShortMonths ? MONTHS : MONTHS_FULL).map((month, index) => {
|
|
878
1079
|
const isSelected = index === viewModel.currentMonth;
|
|
879
1080
|
const isCurrent = index === todayMonth && viewModel.currentYear === todayYear;
|
|
880
1081
|
return `
|
|
881
1082
|
<button
|
|
882
1083
|
type="button"
|
|
883
|
-
class="calendar
|
|
1084
|
+
class="calendar-picker-month${isSelected ? " selected" : ""}${isCurrent ? " current-month" : ""}"
|
|
884
1085
|
data-action="select-month"
|
|
885
1086
|
data-month="${index}"
|
|
886
1087
|
>${month}</button>
|
|
@@ -893,70 +1094,22 @@ var init_CalendarElement = __esm({
|
|
|
893
1094
|
|
|
894
1095
|
<button
|
|
895
1096
|
type="button"
|
|
896
|
-
class="calendar
|
|
1097
|
+
class="calendar-today-btn"
|
|
897
1098
|
data-action="today"
|
|
898
1099
|
${isCurrentMonth ? "disabled" : ""}
|
|
899
1100
|
>Today</button>
|
|
900
1101
|
|
|
901
|
-
<button type="button" class="calendar
|
|
1102
|
+
<button type="button" class="calendar-nav-arrow" data-action="next" aria-label="Next month">
|
|
902
1103
|
›
|
|
903
1104
|
</button>
|
|
904
1105
|
</div>
|
|
905
1106
|
|
|
906
|
-
<
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
${viewModel.days.map((day) => `<th>${day.slice(0, 3)}</th>`).join("")}
|
|
910
|
-
</tr>
|
|
911
|
-
</thead>
|
|
912
|
-
<tbody data-calendar-body>
|
|
913
|
-
${isLoading ? Array.from(
|
|
914
|
-
{ length: 6 },
|
|
915
|
-
() => `<tr>${Array.from(
|
|
916
|
-
{ length: 7 },
|
|
917
|
-
() => `<td class="calendar--skeleton" aria-hidden="true"></td>`
|
|
918
|
-
).join("")}</tr>`
|
|
919
|
-
).join("") : viewModel.calendarDates.map(
|
|
920
|
-
(week) => `
|
|
921
|
-
<tr>
|
|
922
|
-
${week.map((calendarDate, dayIndex) => {
|
|
923
|
-
const classes = getCellClasses(calendarDate);
|
|
924
|
-
if (availabilityMode && calendarDate.isCurrentMonth) {
|
|
925
|
-
classes.push(
|
|
926
|
-
calendarDate.hasEvents ? "availability--booked" : "availability--free"
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
if (availabilityMode === "day" && selectable && calendarDate.isCurrentMonth) {
|
|
930
|
-
const d = calendarDate.date;
|
|
931
|
-
if (this._rangeStart && isSameDay2(d, this._rangeStart))
|
|
932
|
-
classes.push("availability--range-start");
|
|
933
|
-
if (this._rangeEnd && isSameDay2(d, this._rangeEnd))
|
|
934
|
-
classes.push("availability--range-end");
|
|
935
|
-
if (this._rangeStart && this._rangeEnd) {
|
|
936
|
-
if (d > this._rangeStart && d < this._rangeEnd)
|
|
937
|
-
classes.push("availability--in-range");
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const dateString = calendarDate.date.toISOString();
|
|
941
|
-
return `
|
|
942
|
-
<td
|
|
943
|
-
class="${classes.join(" ")}"
|
|
944
|
-
data-date="${dateString}"
|
|
945
|
-
data-day-index="${dayIndex}"
|
|
946
|
-
data-clickable="true"
|
|
947
|
-
>
|
|
948
|
-
${calendarDate.date.getDate()}
|
|
949
|
-
</td>
|
|
950
|
-
`;
|
|
951
|
-
}).join("")}
|
|
952
|
-
</tr>
|
|
953
|
-
`
|
|
954
|
-
).join("")}
|
|
955
|
-
</tbody>
|
|
956
|
-
</table>
|
|
1107
|
+
<div class="calendar-panes">
|
|
1108
|
+
${viewModel.panes.map(renderPane).join("")}
|
|
1109
|
+
</div>
|
|
957
1110
|
|
|
958
1111
|
${!isLoading && availabilityMode !== "day" && viewModel.selectedDate ? `
|
|
959
|
-
<div class="date-popup
|
|
1112
|
+
<div class="date-popup">
|
|
960
1113
|
<div class="popup-header">
|
|
961
1114
|
<h2>${viewModel.scheduleDay}</h2>
|
|
962
1115
|
<button type="button" class="popup-close" data-action="close-popup" aria-label="Close">\u2715</button>
|
|
@@ -994,8 +1147,7 @@ var init_CalendarElement = __esm({
|
|
|
994
1147
|
const availMode = this.getAttribute("availability-mode");
|
|
995
1148
|
const isSelectable = this.hasAttribute("selectable");
|
|
996
1149
|
if (availMode === "day" && isSelectable) {
|
|
997
|
-
|
|
998
|
-
if (!isBooked) {
|
|
1150
|
+
if (this.isDateSelectable(date)) {
|
|
999
1151
|
let startDate;
|
|
1000
1152
|
let endDate;
|
|
1001
1153
|
if (this._rangeEnd !== null) {
|
|
@@ -1013,7 +1165,7 @@ var init_CalendarElement = __esm({
|
|
|
1013
1165
|
cursor.setDate(cursor.getDate() + 1);
|
|
1014
1166
|
let blocked = false;
|
|
1015
1167
|
while (cursor < e2) {
|
|
1016
|
-
if (this.
|
|
1168
|
+
if (!this.isDateSelectable(cursor)) {
|
|
1017
1169
|
blocked = true;
|
|
1018
1170
|
break;
|
|
1019
1171
|
}
|
|
@@ -1247,31 +1399,84 @@ var init_CalendarElement = __esm({
|
|
|
1247
1399
|
this.theme = theme;
|
|
1248
1400
|
}
|
|
1249
1401
|
getCurrentDate() {
|
|
1402
|
+
this.rethrowInitError();
|
|
1250
1403
|
return this.engine?.getViewModel().selectedDate ?? null;
|
|
1251
1404
|
}
|
|
1252
1405
|
goToDate(date) {
|
|
1406
|
+
this.rethrowInitError();
|
|
1253
1407
|
const year = date.getFullYear();
|
|
1254
1408
|
const month = date.getMonth();
|
|
1255
1409
|
this.dispatchMonthChange(year, month);
|
|
1256
1410
|
this.actions?.jump(year, month);
|
|
1257
1411
|
}
|
|
1258
1412
|
getEngine() {
|
|
1413
|
+
this.rethrowInitError();
|
|
1259
1414
|
if (!this.engine)
|
|
1260
1415
|
throw new Error("CalendarElement is not connected to the DOM");
|
|
1261
1416
|
return this.engine;
|
|
1262
1417
|
}
|
|
1263
1418
|
};
|
|
1264
|
-
|
|
1419
|
+
_CalendarElement.observedAttributes = [
|
|
1265
1420
|
"initial-date",
|
|
1266
1421
|
"min-year",
|
|
1267
1422
|
"max-year",
|
|
1268
1423
|
"week-starts-on",
|
|
1424
|
+
"heading",
|
|
1425
|
+
"months",
|
|
1269
1426
|
"title",
|
|
1270
1427
|
"use-short-month-names",
|
|
1271
1428
|
"availability-mode",
|
|
1272
1429
|
"selectable",
|
|
1273
1430
|
"loading"
|
|
1274
1431
|
];
|
|
1432
|
+
_CalendarElement.themeMap = {
|
|
1433
|
+
primary: "--calendar-primary-color",
|
|
1434
|
+
secondary: "--calendar-secondary-color",
|
|
1435
|
+
tertiary: "--calendar-tertiary-color",
|
|
1436
|
+
textColor: "--calendar-text-color",
|
|
1437
|
+
textLight: "--calendar-text-light",
|
|
1438
|
+
background: "--calendar-background",
|
|
1439
|
+
cellHover: "--calendar-cell-hover",
|
|
1440
|
+
borderColor: "--calendar-border-color",
|
|
1441
|
+
todayOutline: "--calendar-today-outline",
|
|
1442
|
+
selectedBg: "--calendar-selected-bg",
|
|
1443
|
+
headerBg: "--calendar-header-bg",
|
|
1444
|
+
popupBg: "--calendar-popup-bg",
|
|
1445
|
+
pickerBg: "--calendar-picker-bg",
|
|
1446
|
+
pickerShadow: "--calendar-picker-shadow",
|
|
1447
|
+
eventIndicator: "--calendar-event-indicator",
|
|
1448
|
+
onAccent: "--calendar-on-accent",
|
|
1449
|
+
link: "--calendar-link",
|
|
1450
|
+
openBg: "--calendar-open-bg",
|
|
1451
|
+
openFg: "--calendar-open-fg",
|
|
1452
|
+
conditionalBg: "--calendar-conditional-bg",
|
|
1453
|
+
conditionalFg: "--calendar-conditional-fg",
|
|
1454
|
+
blockedBg: "--calendar-blocked-bg",
|
|
1455
|
+
blockedFg: "--calendar-blocked-fg",
|
|
1456
|
+
rangeBg: "--calendar-range-bg",
|
|
1457
|
+
rangeOutline: "--calendar-range-outline",
|
|
1458
|
+
inRangeBg: "--calendar-in-range-bg",
|
|
1459
|
+
inRangeOutline: "--calendar-in-range-outline",
|
|
1460
|
+
badgeBg: "--calendar-badge-bg",
|
|
1461
|
+
badgeText: "--calendar-badge-text",
|
|
1462
|
+
badgeSuccessBg: "--calendar-badge-success-bg",
|
|
1463
|
+
badgeSuccessText: "--calendar-badge-success-text",
|
|
1464
|
+
badgeInfoBg: "--calendar-badge-info-bg",
|
|
1465
|
+
badgeInfoText: "--calendar-badge-info-text",
|
|
1466
|
+
badgeWarningBg: "--calendar-badge-warning-bg",
|
|
1467
|
+
badgeWarningText: "--calendar-badge-warning-text",
|
|
1468
|
+
badgeDangerBg: "--calendar-badge-danger-bg",
|
|
1469
|
+
badgeDangerText: "--calendar-badge-danger-text",
|
|
1470
|
+
badgeNeutralBg: "--calendar-badge-neutral-bg",
|
|
1471
|
+
badgeNeutralText: "--calendar-badge-neutral-text",
|
|
1472
|
+
badgePositiveBg: "--calendar-badge-positive-bg",
|
|
1473
|
+
badgePositiveText: "--calendar-badge-positive-text",
|
|
1474
|
+
badgeTentativeBg: "--calendar-badge-tentative-bg",
|
|
1475
|
+
badgeTentativeText: "--calendar-badge-tentative-text"
|
|
1476
|
+
};
|
|
1477
|
+
_CalendarElement.BUILT_IN_BUCKETS = ["blocked", "conditional", "open"];
|
|
1478
|
+
_CalendarElement.titleDeprecationWarned = false;
|
|
1479
|
+
CalendarElement = _CalendarElement;
|
|
1275
1480
|
}
|
|
1276
1481
|
});
|
|
1277
1482
|
|
|
@@ -1293,6 +1498,7 @@ export {
|
|
|
1293
1498
|
MONTHS,
|
|
1294
1499
|
MONTHS_FULL,
|
|
1295
1500
|
defineCalendarElement,
|
|
1501
|
+
escapeHtml,
|
|
1296
1502
|
formatAttendees,
|
|
1297
1503
|
formatDateForDisplay,
|
|
1298
1504
|
formatTimeRange,
|
|
@@ -1303,13 +1509,15 @@ export {
|
|
|
1303
1509
|
getDefaultEventColor,
|
|
1304
1510
|
getEventsForDate,
|
|
1305
1511
|
getMonthYearText,
|
|
1306
|
-
getPopupPositionClass,
|
|
1307
1512
|
hasEvents,
|
|
1308
1513
|
isSameDay,
|
|
1309
1514
|
isToday,
|
|
1310
1515
|
isValidHexColor,
|
|
1311
1516
|
mergeCategoryColors,
|
|
1312
1517
|
normalizeDate,
|
|
1518
|
+
safeColor,
|
|
1519
|
+
safeUrl,
|
|
1520
|
+
slugifyToken,
|
|
1313
1521
|
sortEventsByTime
|
|
1314
1522
|
};
|
|
1315
1523
|
//# sourceMappingURL=index.mjs.map
|