@meetelise/chat 1.11.0 → 1.12.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.
Files changed (56) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/.github/workflows/release.yml +1 -0
  3. package/.vscode/settings.json +6 -1
  4. package/CONTRIBUTING.md +8 -0
  5. package/package.json +9 -10
  6. package/public/demo/index.html +78 -11
  7. package/public/dist/index.js +1714 -1
  8. package/public/dist/index.js.LICENSE.txt +26 -14
  9. package/public/index.html +2 -1
  10. package/src/MEChat.test.ts +5 -5
  11. package/src/MEChat.ts +53 -0
  12. package/src/WebComponent/InHouseLauncher.ts +446 -0
  13. package/src/WebComponent/MEChat.css +5 -0
  14. package/src/WebComponent/MEChat.ts +282 -0
  15. package/src/WebComponent/OfficeHours.ts +73 -0
  16. package/src/WebComponent/Scheduler/date-picker.ts +320 -0
  17. package/src/WebComponent/Scheduler/me-select.ts +244 -0
  18. package/src/WebComponent/Scheduler/time-picker.ts +101 -0
  19. package/src/WebComponent/Scheduler/tour-scheduler.ts +383 -0
  20. package/src/WebComponent/Scheduler/tour-type-option.ts +92 -0
  21. package/src/WebComponent/actions/ActionConfirmButton.ts +94 -0
  22. package/src/WebComponent/actions/CallUsWindow.ts +110 -0
  23. package/src/WebComponent/actions/DetailsWindow.ts +109 -0
  24. package/src/WebComponent/actions/EmailUsWindow.ts +432 -0
  25. package/src/WebComponent/actions/InputStyles.ts +31 -0
  26. package/src/WebComponent/actions/TextUsWindow.ts +226 -0
  27. package/src/WebComponent/actions/formatPhoneNumber.ts +42 -0
  28. package/src/WebComponent/inHouseLauncherStyles.ts +300 -0
  29. package/src/WebComponent/index.ts +2 -0
  30. package/src/WebComponent/utils.ts +82 -0
  31. package/src/fetchBuildingInfo.ts +1 -0
  32. package/src/getAvailabilities.ts +71 -0
  33. package/src/themes.ts +5 -3
  34. package/tsconfig.json +9 -3
  35. package/web-test-runner.config.js +0 -6
  36. package/webpack.config.cjs +8 -25
  37. package/public/dist/index.d.ts +0 -1
  38. package/public/dist/src/ChatButton.d.ts +0 -9
  39. package/public/dist/src/ChatIcon.d.ts +0 -6
  40. package/public/dist/src/InHouseLauncher.d.ts +0 -11
  41. package/public/dist/src/MEChat.d.ts +0 -73
  42. package/public/dist/src/analytics.d.ts +0 -34
  43. package/public/dist/src/assetUrls.d.ts +0 -2
  44. package/public/dist/src/chatID.d.ts +0 -11
  45. package/public/dist/src/createConversation.d.ts +0 -4
  46. package/public/dist/src/fetchBuildingInfo.d.ts +0 -25
  47. package/public/dist/src/themes.d.ts +0 -52
  48. package/public/dist/src/utils.d.ts +0 -2
  49. package/src/ChatButton.module.scss +0 -51
  50. package/src/ChatButton.tsx +0 -38
  51. package/src/ChatIcon.tsx +0 -26
  52. package/src/DemoApp.tsx +0 -113
  53. package/src/InHouseLauncher.module.scss +0 -139
  54. package/src/InHouseLauncher.tsx +0 -69
  55. package/src/MEChat.module.scss +0 -22
  56. package/src/MEChat.tsx +0 -293
@@ -0,0 +1,282 @@
1
+ import { css, html, LitElement, TemplateResult } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { createRef, ref, Ref } from "lit/directives/ref.js";
5
+ import Talk from "talkjs";
6
+ import { InHouseLauncher } from ".";
7
+ import "./Scheduler/tour-scheduler";
8
+ import Analytics from "../analytics";
9
+ import { getChatID } from "../chatID";
10
+ import createConversation from "../createConversation";
11
+ import fetchBuildingInfo, { Building } from "../fetchBuildingInfo";
12
+ import { getTheme, Theme, ThemeIdString } from "../themes";
13
+ import { isMobile } from "../utils";
14
+ import { installInHouseLauncher } from "./InHouseLauncher";
15
+
16
+ import "./MEChat.css";
17
+
18
+ export interface Options {
19
+ building: string;
20
+ organization: string;
21
+ themeId?: ThemeIdString;
22
+ avatarSrc?: string;
23
+ mini?: boolean;
24
+ }
25
+
26
+ @customElement("me-chat")
27
+ export class MEChat extends LitElement {
28
+ static styles = css`
29
+ #__talkjs_launcher:not(.shouldBeVisible) {
30
+ display: none;
31
+ }
32
+
33
+ .inHouseLauncherContainer.in-house-launcher__mobile {
34
+ width: 100%;
35
+ height: 100px;
36
+ }
37
+ `;
38
+ static session: Promise<Talk.Session> = Talk.ready.then(() => {
39
+ const me = new Talk.User({
40
+ id: "anonymous",
41
+ name: "Me",
42
+ email: null,
43
+ role: "Default",
44
+ });
45
+ return new Talk.Session({
46
+ appId: "ogKIvCor",
47
+ me,
48
+ });
49
+ });
50
+ @property({ type: String })
51
+ private buildingSlug = "";
52
+ @property({ type: String })
53
+ private orgSlug = "";
54
+ @property({ type: String })
55
+ private themeId: string | undefined;
56
+ @property({ attribute: false })
57
+ private avatarSrc?: string | null;
58
+ @property({ type: Boolean })
59
+ useMiniWidget = true;
60
+
61
+ @state()
62
+ private popup: Talk.Popup | null = null;
63
+ @state()
64
+ private theme: Theme = getTheme();
65
+ @state()
66
+ private chatId = "";
67
+ @state()
68
+ private analytics: Analytics | null = null;
69
+ @state()
70
+ private isMobile = isMobile();
71
+ @state()
72
+ private launcher: HTMLElement | null = null;
73
+ @state()
74
+ private building: Building | null = null;
75
+ @state()
76
+ private hasMounted = false;
77
+ @state()
78
+ private hideLauncher = false;
79
+
80
+ private yardiDNIScriptInterval: NodeJS.Timer | null = null;
81
+ inHouseLauncherRef: Ref<InHouseLauncher> = createRef();
82
+
83
+ initializeInstanceVariables = async (): Promise<void> => {
84
+ await this.setBuildingDerivedInfo();
85
+ await this.initializeLaunchJS();
86
+ this.attachOnClickToInHouseLauncher();
87
+ };
88
+
89
+ setBuildingDerivedInfo = async (): Promise<void> => {
90
+ if (!this.buildingSlug || !this.orgSlug) {
91
+ return;
92
+ }
93
+ this.building = await fetchBuildingInfo(this.orgSlug, this.buildingSlug);
94
+ this.chatId = getChatID(this.orgSlug, this.buildingSlug);
95
+ this.avatarSrc = this.avatarSrc || this.building.avatarSrc;
96
+ this.theme = getTheme(this.themeId ?? this.building.themeId);
97
+ };
98
+
99
+ private initializeLaunchJS = async () => {
100
+ if (!this.building || !this.theme) {
101
+ return;
102
+ }
103
+ if (this.popup && this.launcher) {
104
+ return;
105
+ }
106
+
107
+ const [building, theme, avatarSrc, session] = await Promise.all([
108
+ this.building,
109
+ this.theme,
110
+ this.avatarSrc,
111
+ MEChat.session,
112
+ ]);
113
+ if (building.conversationMaintenanceMode) {
114
+ return new Promise(() => {
115
+ // If in maintenance mode, we return an always-pending Promise
116
+ // eslint-disable-next-line no-console
117
+ console.warn(
118
+ "MeetElise Chat is in maintenance mode. Chat icon will not appear."
119
+ );
120
+ });
121
+ }
122
+ await this.configureTalkJSPopup(building, theme, session, avatarSrc);
123
+ this.configureLauncherElement();
124
+
125
+ this.yardiDNIScriptInterval = setInterval(
126
+ () => this.pollForYardiCampaignSource(),
127
+ 1000
128
+ );
129
+ setTimeout(clearInterval, 15000, this.yardiDNIScriptInterval);
130
+ };
131
+
132
+ private pollForYardiCampaignSource() {
133
+ const yardiSource = window.RCTPCampaign?.CampaignDetails?.Source;
134
+ if (yardiSource) {
135
+ this.analytics?.ping("yardi-DNI-init");
136
+ if (this.yardiDNIScriptInterval) {
137
+ clearInterval(this.yardiDNIScriptInterval);
138
+ }
139
+ }
140
+ }
141
+
142
+ private configureLauncherElement = () => {
143
+ if (!this.popup || !this.inHouseLauncherRef.value) {
144
+ return;
145
+ }
146
+ this.launcher = this.inHouseLauncherRef.value;
147
+ };
148
+
149
+ private configureTalkJSPopup = async (
150
+ building: Building,
151
+ theme: Theme,
152
+ session: Talk.Session,
153
+ avatarSrc?: string | null
154
+ ) => {
155
+ const popup = session.createPopup(
156
+ createConversation(
157
+ session,
158
+ building,
159
+ theme,
160
+ avatarSrc || building.avatarSrc,
161
+ this.chatId,
162
+ this.isMobile
163
+ ),
164
+ {
165
+ launcher: "never",
166
+ showCloseInHeader: true,
167
+ messageField: { placeholder: "Ask a question..." },
168
+ }
169
+ );
170
+
171
+ await popup.mount({ show: false });
172
+ popup.on("close", () => {
173
+ this.hideLauncher = false;
174
+ });
175
+ const talkjsPopupElement = document.querySelector(".__talkjs_popup");
176
+ if (!talkjsPopupElement) throw new Error("Failed to find chat window");
177
+ talkjsPopupElement.classList.add("meetelise-chat", "pane");
178
+ if (!this.isMobile) {
179
+ talkjsPopupElement.classList.add("in-house-launcher__desktop");
180
+ }
181
+ (talkjsPopupElement as HTMLElement).style.zIndex = "99999999999";
182
+ this.popup = popup;
183
+ };
184
+
185
+ /**
186
+ * Remove the instance from the screen.
187
+ *
188
+ * Chat will be unusable after this. If you just need to hide the
189
+ * chat button, use {@link MEChat#hide} instead.
190
+ */
191
+ remove(): void {
192
+ this.popup?.destroy();
193
+ }
194
+
195
+ /** Open the messages window */
196
+ open(): void {
197
+ this.popup?.show();
198
+ }
199
+
200
+ /** Close the messages window */
201
+ close(): void {
202
+ this.popup?.hide();
203
+ }
204
+
205
+ /** Show the chat button on the screen if it was previously hidden. */
206
+ // TODO: will this work with the new launcher? it needs to be display flex? will this just change the inline style and leave the stylesheet/style tag alone?
207
+ show(): void {
208
+ if (!this.launcher) {
209
+ return;
210
+ }
211
+ this.launcher.style.display = "";
212
+ }
213
+
214
+ /** Hide the chat button from the screen (but don't remove from the DOM). */
215
+ hide(): void {
216
+ if (!this.launcher) {
217
+ return;
218
+ }
219
+ this.launcher.style.display = "none";
220
+ }
221
+
222
+ firstUpdated = (): void => {
223
+ this.initializeInstanceVariables();
224
+ };
225
+
226
+ render(): TemplateResult {
227
+ installInHouseLauncher();
228
+
229
+ return html`
230
+ <div
231
+ class=${classMap({
232
+ inHouseLauncherContainer: true,
233
+ ["in-house-launcher__mobile"]: this.isMobile,
234
+ ["in-house-launcher__desktop"]: !this.isMobile,
235
+ ["meetelise-chat"]: true,
236
+ launcher: true,
237
+ shouldBeVisible: true,
238
+ })}
239
+ >
240
+ <in-house-launcher
241
+ ${ref(this.inHouseLauncherRef)}
242
+ .isMobile=${this.isMobile}
243
+ .isFirstMount=${!this.hasMounted}
244
+ .isMini=${this.useMiniWidget}
245
+ .buildingId=${this.building?.id ?? 0}
246
+ phoneNumber="${this.building?.phoneNumber ?? ""}"
247
+ textColor="${this.theme.chatHeader.textColor}"
248
+ backgroundColor="${this.theme.chatPaneBackgroundColor}"
249
+ ?hidden=${this.hideLauncher}
250
+ >
251
+ </in-house-launcher>
252
+ </div>
253
+ `;
254
+ }
255
+
256
+ private attachOnClickToInHouseLauncher = () => {
257
+ const inHouseLauncher = this.inHouseLauncherRef.value;
258
+ if (!inHouseLauncher) {
259
+ return;
260
+ }
261
+
262
+ inHouseLauncher.onChatTapped = async () => {
263
+ if (!this.popup) {
264
+ return;
265
+ }
266
+
267
+ this.popup.show();
268
+ this.analytics?.ping("open");
269
+ this.hideLauncher = true;
270
+ this.hasMounted = true;
271
+ };
272
+ };
273
+ }
274
+
275
+ declare global {
276
+ interface HTMLElementTagNameMap {
277
+ "me-chat": MEChat;
278
+ }
279
+ interface Window {
280
+ RCTPCampaign?: { CampaignDetails: { Source: string } };
281
+ }
282
+ }
@@ -0,0 +1,73 @@
1
+ import eachDayOfInterval from "date-fns/eachDayOfInterval";
2
+ import startOfWeek from "date-fns/startOfWeek";
3
+ import endOfWeek from "date-fns/endOfWeek";
4
+ import setHours from "date-fns/setHours";
5
+ import getDay from "date-fns/getDay";
6
+ import axios from "axios";
7
+
8
+ interface OfficeHourAvailabilityReponse {
9
+ [dayOfWeek: string]: { start: string; end: string };
10
+ }
11
+
12
+ const officeHoursCache: {
13
+ [buildingId: number]: OfficeHourAvailabilityReponse;
14
+ } = {};
15
+
16
+ export const getRawOfficeHours = async (
17
+ buildingId: number
18
+ ): Promise<OfficeHourAvailabilityReponse> => {
19
+ if (officeHoursCache[buildingId]) {
20
+ return officeHoursCache[buildingId];
21
+ }
22
+ const url = `https://app.meetelise.com/platformApi/connectors/talk_js/leasing_office_hours/${buildingId}`;
23
+ const result = await axios.get<OfficeHourAvailabilityReponse>(url);
24
+ officeHoursCache[buildingId] = result.data;
25
+ return result.data;
26
+ };
27
+
28
+ export const getDefaultOfficeHours = (): {
29
+ startTime: Date;
30
+ endTime: Date;
31
+ }[] => {
32
+ return eachDayOfInterval({
33
+ start: startOfWeek(new Date()),
34
+ end: endOfWeek(new Date()),
35
+ })
36
+ .filter((date) => getDay(date) !== 0 && getDay(date) !== 6)
37
+ .map((date) => ({
38
+ startTime: setHours(date, 9),
39
+ endTime: setHours(date, 17),
40
+ }));
41
+ };
42
+
43
+ export const dayNamesInOrder = [
44
+ "monday",
45
+ "tuesday",
46
+ "wednesday",
47
+ "thursday",
48
+ "friday",
49
+ "saturday",
50
+ "sunday",
51
+ ];
52
+
53
+ /**
54
+ * Based on the design, we're going with monday as the first day of the week
55
+ */
56
+ export const getOfficeHourText = async (
57
+ buildingId: number
58
+ ): Promise<
59
+ (string | { dayOfWeek: string; startTime: string; endTime: string })[]
60
+ > => {
61
+ const rawOfficeHours = await getRawOfficeHours(buildingId);
62
+ return dayNamesInOrder.map((dow) => {
63
+ const officeHourInfo = rawOfficeHours[dow];
64
+ if (!officeHourInfo) {
65
+ return dow;
66
+ }
67
+ return {
68
+ dayOfWeek: dow,
69
+ startTime: officeHourInfo.start,
70
+ endTime: officeHourInfo.end,
71
+ };
72
+ });
73
+ };
@@ -0,0 +1,320 @@
1
+ import { LitElement, html, TemplateResult, css } from "lit";
2
+ import { property, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import {
5
+ dayNames,
6
+ getDaysInMonth,
7
+ getDaysInPreviousMonth,
8
+ getMonthEndDay,
9
+ getMonthStartDay,
10
+ Month,
11
+ monthNames,
12
+ } from "../utils";
13
+
14
+ export class DatePicker extends LitElement {
15
+ /**
16
+ * Optional attribute to set the date picker's default month.
17
+ * The attribute will not be updated if the user changes the month shown.
18
+ */
19
+ @property({ attribute: "month", type: String })
20
+ defaultMonth?: Month;
21
+
22
+ /**
23
+ * Optional attribute to set the date picker's default year.
24
+ * The attribute will not be updated if the user changes the year shown.
25
+ */
26
+ @property({ attribute: "year", type: Number })
27
+ defaultYear?: number;
28
+
29
+ private _selectedDate?: number = undefined;
30
+ set selectedDate(date: number | undefined) {
31
+ const old = this._selectedDate;
32
+ this._selectedDate = date;
33
+ this.requestUpdate("selectedDate", old);
34
+ this.dispatchEvent(new Event("change"));
35
+ }
36
+
37
+ @property({ type: Number })
38
+ get selectedDate(): undefined | number {
39
+ return this._selectedDate;
40
+ }
41
+
42
+ now = new Date();
43
+
44
+ private _monthShown =
45
+ this.defaultMonth && monthNames.indexOf(this.defaultMonth) !== -1
46
+ ? monthNames.indexOf(this.defaultMonth)
47
+ : this.now.getMonth();
48
+
49
+ // Make it easy to increment/decrement `this.monthShown` at year boundaries
50
+ set monthShown(month: number) {
51
+ const oldMonthShown = this._monthShown;
52
+ if (month === 12) {
53
+ this._monthShown = 0;
54
+ this.yearShown++;
55
+ } else if (month === -1) {
56
+ this._monthShown = 11;
57
+ this.yearShown--;
58
+ } else {
59
+ this._monthShown = month;
60
+ }
61
+ this.requestUpdate("monthShown", oldMonthShown);
62
+ }
63
+
64
+ /** 0-indexed month, e.g. `0` for January */
65
+ @state()
66
+ get monthShown(): number {
67
+ return this._monthShown;
68
+ }
69
+
70
+ @state()
71
+ yearShown = this.defaultYear ?? this.now.getFullYear();
72
+
73
+ static styles = css`
74
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;900&display=swap");
75
+
76
+ :host {
77
+ box-sizing: border-box;
78
+ font-family: "Poppins";
79
+ color: #202020;
80
+ font-style: normal;
81
+ }
82
+
83
+ #calendar {
84
+ display: flex;
85
+ flex-direction: column;
86
+ user-select: none;
87
+ min-width: 206px;
88
+ width: 206px;
89
+ background: #e7e7e7;
90
+ border: 1px solid #ffffff;
91
+ border-radius: 10px;
92
+ padding: 15px 12px 10px;
93
+ }
94
+
95
+ #header {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ margin-bottom: 9px;
100
+ }
101
+
102
+ h1 {
103
+ font-weight: 600;
104
+ font-size: 12px;
105
+ }
106
+
107
+ #arrows {
108
+ width: 40.46px;
109
+ display: flex;
110
+ justify-content: space-between;
111
+ align-items: center;
112
+ }
113
+
114
+ #rows {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 10px;
118
+ }
119
+
120
+ .row {
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ width: 100%;
125
+ }
126
+
127
+ .dayNumber,
128
+ .dayInitial {
129
+ position: relative;
130
+ font-size: 12px;
131
+ text-align: center;
132
+ flex: 1 1 0px;
133
+ }
134
+
135
+ .dayInitial {
136
+ font-weight: 400;
137
+ }
138
+
139
+ .dayNumber {
140
+ font-weight: 600;
141
+ }
142
+
143
+ .dayNumber.selected::after,
144
+ .dayNumber:not(.selected):hover::after {
145
+ content: "";
146
+ height: 23px;
147
+ aspect-ratio: 1;
148
+ border-radius: 50%;
149
+ background-color: #202020;
150
+ position: absolute;
151
+ top: 50%;
152
+ left: 50%;
153
+ transform: translate(-50%, -50%);
154
+ z-index: -1;
155
+ }
156
+ .dayNumber:not(.selected):hover::after {
157
+ background-color: lightgray;
158
+ }
159
+ .dayNumber.selected::after {
160
+ background-color: #202020;
161
+ }
162
+ .dayNumber.selected,
163
+ .dayNumber:not(.selected):not(.differentMonth):hover {
164
+ z-index: 1;
165
+ }
166
+ .dayNumber.selected {
167
+ color: white;
168
+ }
169
+
170
+ .dayNumber.differentMonth {
171
+ color: #84838f;
172
+ }
173
+ `;
174
+
175
+ render(): TemplateResult {
176
+ // TODO: check 0 vs. 1-indexing, make sure this works.
177
+ // TODO: handle year boundaries, e.g. if it's January and last month was last year. also same for next month when switching months.
178
+ const daysInMonth = getDaysInMonth(this.yearShown, this.monthShown);
179
+ const monthStartDay =
180
+ dayNames[getMonthStartDay(this.yearShown, this.monthShown)];
181
+ const daysInPreviousMonth = getDaysInPreviousMonth(
182
+ this.yearShown,
183
+ this.monthShown
184
+ );
185
+ const monthEndDay =
186
+ dayNames[getMonthEndDay(this.yearShown, this.monthShown)];
187
+ const extraDaysAtBeginningOfMonth = [];
188
+ if (monthStartDay !== "Sunday") {
189
+ const numberOfExtraDaysAtBeginningOfMonth =
190
+ dayNames.indexOf(monthStartDay);
191
+ for (let i = numberOfExtraDaysAtBeginningOfMonth; i > 0; i--) {
192
+ extraDaysAtBeginningOfMonth.push(daysInPreviousMonth + 1 - i);
193
+ }
194
+ }
195
+
196
+ const daysOfMonth = new Array(daysInMonth).fill(null).map((_, i) => i + 1);
197
+
198
+ const extraDaysAtEndOfMonth = [];
199
+ if (monthEndDay !== "Saturday") {
200
+ const numberOfExtraDaysAtEndOfMonth = [...dayNames]
201
+ .reverse()
202
+ .indexOf(monthEndDay);
203
+ for (let i = 1; i <= numberOfExtraDaysAtEndOfMonth; i++) {
204
+ extraDaysAtEndOfMonth.push(i);
205
+ }
206
+ }
207
+
208
+ const dayNums = [
209
+ ...extraDaysAtBeginningOfMonth,
210
+ ...daysOfMonth,
211
+ ...extraDaysAtEndOfMonth,
212
+ ];
213
+
214
+ const dayElements = dayNums.map((dayNumber, index) => {
215
+ const isDifferentMonth =
216
+ index < extraDaysAtBeginningOfMonth.length ||
217
+ index >= extraDaysAtBeginningOfMonth.length + daysOfMonth.length;
218
+ return html`<span
219
+ class="dayNumber ${classMap({
220
+ differentMonth: isDifferentMonth,
221
+ selected: dayNumber === this.selectedDate && !isDifferentMonth,
222
+ })}"
223
+ >${dayNumber}</span
224
+ >`;
225
+ });
226
+ const rows = chunk(7, dayElements);
227
+
228
+ return html`
229
+ <div id="calendar">
230
+ <div id="header">
231
+ <h1>${monthNames[this.monthShown]} ${this.yearShown}</h1>
232
+ <div
233
+ id="arrows"
234
+ @click="${(e: MouseEvent) => {
235
+ // TODO: disable incrementing/decrementing to months with no appointments available
236
+ if ((e.target as HTMLElement)?.closest("#back")) {
237
+ this.monthShown--;
238
+ }
239
+ if ((e.target as HTMLElement)?.closest("#forward")) {
240
+ this.monthShown++;
241
+ }
242
+ }}"
243
+ @keydown="${(e: KeyboardEvent) => {
244
+ if (![" ", "Enter"].includes(e.key)) {
245
+ return;
246
+ }
247
+ if ((e.target as HTMLElement)?.closest("#back")) {
248
+ e.preventDefault();
249
+ e.stopPropagation();
250
+ this.monthShown--;
251
+ } else if ((e.target as HTMLElement)?.closest("#forward")) {
252
+ e.preventDefault();
253
+ e.stopPropagation();
254
+ this.monthShown++;
255
+ }
256
+ }}"
257
+ >
258
+ <svg
259
+ id="back"
260
+ tabindex="0"
261
+ width="9"
262
+ height="16"
263
+ viewBox="0 0 9 16"
264
+ fill="none"
265
+ xmlns="http://www.w3.org/2000/svg"
266
+ >
267
+ <path
268
+ d="M8.67727 2.34317L7.26305 0.928955L0.192017 8.00001L7.26308 15.0711L8.6773 13.6569L3.02044 8L8.67727 2.34317Z"
269
+ fill="#83818E"
270
+ />
271
+ </svg>
272
+ <svg
273
+ id="forward"
274
+ tabindex="0"
275
+ width="9"
276
+ height="16"
277
+ viewBox="0 0 9 16"
278
+ fill="none"
279
+ xmlns="http://www.w3.org/2000/svg"
280
+ >
281
+ <path
282
+ d="M0.157227 2.34315L1.57144 0.928932L8.64251 8L1.57144 15.0711L0.157227 13.6569L5.81408 8L0.157227 2.34315Z"
283
+ fill="#83818E"
284
+ />
285
+ </svg>
286
+ </div>
287
+ </div>
288
+
289
+ <div
290
+ id="rows"
291
+ @click="${(e: MouseEvent) => {
292
+ const target = e.target as HTMLElement | undefined;
293
+ if (target?.closest("span.dayNumber:not(.differentMonth)"))
294
+ this.selectedDate = parseInt(target.innerText);
295
+ }}"
296
+ >
297
+ <div class="row">
298
+ <span class="dayInitial">S</span>
299
+ <span class="dayInitial">M</span>
300
+ <span class="dayInitial">T</span>
301
+ <span class="dayInitial">W</span>
302
+ <span class="dayInitial">T</span>
303
+ <span class="dayInitial">F</span>
304
+ <span class="dayInitial">S</span>
305
+ </div>
306
+ ${rows.map((row) => html`<div class="row">${row}</div>`)}
307
+ </div>
308
+ </div>
309
+ `;
310
+ }
311
+ }
312
+ customElements.define("date-picker", DatePicker);
313
+
314
+ const chunk = <T>(chunkLength: number, array: T[]) => {
315
+ const chunks = [];
316
+ for (let i = 0; i < array.length; i += chunkLength) {
317
+ chunks.push(array.slice(i, i + chunkLength));
318
+ }
319
+ return chunks;
320
+ };