@meetelise/chat 1.51.0 → 1.51.1
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/src/WebComponent/Scheduler/tour-scheduler.d.ts +7 -3
- package/dist/src/WebComponent/actions/email-us-window.d.ts +5 -4
- package/dist/src/WebComponent/actions/formatPhoneNumber.d.ts +22 -3
- package/dist/src/WebComponent/actions/international-phone-input.d.ts +30 -0
- package/package.json +2 -1
- package/public/dist/index.js +695 -575
- package/src/WebComponent/Scheduler/tour-scheduler.ts +37 -189
- package/src/WebComponent/actions/call-us-window.ts +2 -2
- package/src/WebComponent/actions/email-us-window.ts +27 -54
- package/src/WebComponent/actions/formatPhoneNumber.ts +146 -12
- package/src/WebComponent/actions/international-phone-input.ts +308 -0
- package/src/formatPhoneNumber.test.ts +91 -0
- package/src/internationalPhoneInput.test.ts +253 -0
- package/src/internationalPhoneSubmission.test.ts +145 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import { html, LitElement, PropertyValueMap, TemplateResult } from "lit";
|
|
3
3
|
import { customElement, property, query, state } from "lit/decorators.js";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
formatToPhoneInput,
|
|
7
|
-
isPrintableCharacter,
|
|
8
|
-
} from "../actions/formatPhoneNumber";
|
|
4
|
+
import "../actions/international-phone-input";
|
|
5
|
+
import type { InternationalPhoneChangeDetail } from "../actions/international-phone-input";
|
|
9
6
|
import "./tour-type-option.ts";
|
|
10
7
|
import "./date-picker.ts";
|
|
11
8
|
import "./time-picker.ts";
|
|
@@ -30,12 +27,7 @@ import { MESelect } from "../me-select";
|
|
|
30
27
|
import { TimePicker } from "./time-picker";
|
|
31
28
|
import { LabeledOption } from "../../fetchBuildingInfo";
|
|
32
29
|
import { LayoutOption } from "../../fetchBuildingWebchatView";
|
|
33
|
-
import {
|
|
34
|
-
isMobile,
|
|
35
|
-
isValidEmail,
|
|
36
|
-
isValidPhoneNumber,
|
|
37
|
-
snakify,
|
|
38
|
-
} from "../../utils";
|
|
30
|
+
import { isMobile, isValidEmail, snakify } from "../../utils";
|
|
39
31
|
import axios, { AxiosError } from "axios";
|
|
40
32
|
import mapValues from "lodash/mapValues";
|
|
41
33
|
import classnames from "classnames";
|
|
@@ -155,6 +147,14 @@ export class TourScheduler extends LitElement {
|
|
|
155
147
|
@state()
|
|
156
148
|
private phoneNumber = "";
|
|
157
149
|
@state()
|
|
150
|
+
private phoneNumberE164: string | null = null;
|
|
151
|
+
@state()
|
|
152
|
+
private phoneCountry: string | null = null;
|
|
153
|
+
@state()
|
|
154
|
+
private phoneNumberIsValid = false;
|
|
155
|
+
@state()
|
|
156
|
+
private phoneNumberHasBlurred = false;
|
|
157
|
+
@state()
|
|
158
158
|
private availabilitiesGroupedByDay: {
|
|
159
159
|
[day: string]: DateWithTimeZoneOffset[];
|
|
160
160
|
} = {};
|
|
@@ -217,8 +217,6 @@ export class TourScheduler extends LitElement {
|
|
|
217
217
|
lastNameInput!: HTMLInputElement;
|
|
218
218
|
@query(".inputContainer#email input")
|
|
219
219
|
emailInput!: HTMLInputElement;
|
|
220
|
-
@query(".inputContainer#phone input")
|
|
221
|
-
phoneInput!: HTMLInputElement;
|
|
222
220
|
@query("me-select#leadSource")
|
|
223
221
|
selectedLeadSource!: MESelect;
|
|
224
222
|
@query("me-select#layout")
|
|
@@ -630,147 +628,13 @@ export class TourScheduler extends LitElement {
|
|
|
630
628
|
}
|
|
631
629
|
};
|
|
632
630
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
)
|
|
641
|
-
return;
|
|
642
|
-
|
|
643
|
-
const cursorPosition = e.target.selectionStart;
|
|
644
|
-
|
|
645
|
-
if (isPrintableCharacter(e) && !shortcutKeyIsPressed(e)) {
|
|
646
|
-
// If e.key is a character, and no modifier key is pressed, insert it at the cursor, filter out non-numbers, and auto-format
|
|
647
|
-
e.preventDefault();
|
|
648
|
-
e.stopPropagation();
|
|
649
|
-
const updated =
|
|
650
|
-
this.phoneNumber.slice(0, cursorPosition) +
|
|
651
|
-
e.key +
|
|
652
|
-
this.phoneNumber.slice(cursorPosition);
|
|
653
|
-
this.phoneNumber = formatToPhoneInput(updated.replace(/\D/g, ""));
|
|
654
|
-
this.phoneInput.value = this.phoneNumber;
|
|
655
|
-
} else if (e.key === "Backspace") {
|
|
656
|
-
/*
|
|
657
|
-
Handling backspace:
|
|
658
|
-
- A single backspace should delete the last digit before the cursor, not just a punctuation character; the user shouldn't interact directly with the punctuation
|
|
659
|
-
- Let the OS handle backspace combos like `Alt + Backspace`, then re-autoformat if necessary (in keyup)
|
|
660
|
-
- If the user wants to select and backspace a range of text, let them, then auto-format the remainder
|
|
661
|
-
*/
|
|
662
|
-
|
|
663
|
-
// backspace combos
|
|
664
|
-
if (shortcutKeyIsPressed(e)) {
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// backspace selection
|
|
669
|
-
if (
|
|
670
|
-
this.phoneInput.selectionEnd &&
|
|
671
|
-
this.phoneInput.selectionStart &&
|
|
672
|
-
this.phoneInput.selectionEnd - this.phoneInput.selectionStart > 0
|
|
673
|
-
) {
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// regular backspace
|
|
678
|
-
const originalCharacterCount = this.phoneNumber.length;
|
|
679
|
-
const digitsBeforeCursor = this.phoneNumber
|
|
680
|
-
.slice(0, cursorPosition)
|
|
681
|
-
.replace(/\D/g, "");
|
|
682
|
-
const digitsAfterCursor = this.phoneNumber
|
|
683
|
-
.slice(cursorPosition)
|
|
684
|
-
.replace(/\D/g, "");
|
|
685
|
-
const updatedDigits = `${digitsBeforeCursor.slice(
|
|
686
|
-
0,
|
|
687
|
-
-1
|
|
688
|
-
)}${digitsAfterCursor}`;
|
|
689
|
-
this.phoneNumber = formatToPhoneInput(updatedDigits);
|
|
690
|
-
this.phoneInput.value = this.phoneNumber;
|
|
691
|
-
const numOfCharactersDeleted =
|
|
692
|
-
originalCharacterCount - this.phoneNumber.length;
|
|
693
|
-
const newCursorPosition = cursorPosition - numOfCharactersDeleted;
|
|
694
|
-
this.phoneInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
695
|
-
e.preventDefault();
|
|
696
|
-
e.stopPropagation();
|
|
697
|
-
return;
|
|
698
|
-
} else if (
|
|
699
|
-
["ArrowLeft", "ArrowRight"].includes(e.key) &&
|
|
700
|
-
!shortcutKeyIsPressed(e) &&
|
|
701
|
-
!e.shiftKey
|
|
702
|
-
) {
|
|
703
|
-
// when navigating with arrow keys, skip punctuation
|
|
704
|
-
if (e.key === "ArrowLeft") {
|
|
705
|
-
const charactersBeforeCursor = this.phoneNumber.slice(
|
|
706
|
-
0,
|
|
707
|
-
cursorPosition
|
|
708
|
-
);
|
|
709
|
-
const numberOfNonDigitsBeforeCursor =
|
|
710
|
-
charactersBeforeCursor.split(/\d+/).at(-1)?.length || 0;
|
|
711
|
-
const moveLeftBy = numberOfNonDigitsBeforeCursor + 1;
|
|
712
|
-
const newCursorPosition =
|
|
713
|
-
cursorPosition - moveLeftBy > -1 ? cursorPosition - moveLeftBy : 0;
|
|
714
|
-
this.phoneInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
715
|
-
}
|
|
716
|
-
if (e.key === "ArrowRight") {
|
|
717
|
-
const charactersAfterCursor = this.phoneNumber.slice(cursorPosition);
|
|
718
|
-
const numberOfNonDigitsAfterCursor =
|
|
719
|
-
charactersAfterCursor.split(/\d+/)[0].length || 0;
|
|
720
|
-
const moveRightBy = numberOfNonDigitsAfterCursor + 1;
|
|
721
|
-
const newCursorPosition =
|
|
722
|
-
cursorPosition + moveRightBy < this.phoneNumber.length
|
|
723
|
-
? cursorPosition + moveRightBy
|
|
724
|
-
: this.phoneNumber.length;
|
|
725
|
-
this.phoneInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
726
|
-
}
|
|
727
|
-
e.preventDefault();
|
|
728
|
-
e.stopPropagation();
|
|
729
|
-
} else {
|
|
730
|
-
// Let browser/OS handle anything else. We'll handle any changes to the phone input in the `keyup` handler.
|
|
731
|
-
// Could be a keyboard shortcut that modifies the input (like `Cmd/Ctrl + V`, which we'll handle in `keyup`),
|
|
732
|
-
// or a keyboard shortcut that doesn't (like `Cmd + L` to jump to URL bar or `Cmd + R` to reload the page),
|
|
733
|
-
// or Tab, an arrow key, etc.
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
handlePhoneKeyup = (e: KeyboardEvent): void => {
|
|
739
|
-
if (!e.key) {
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
// After formatting, place the cursor where it was before, defined as "before the digit that followed it before formatting, if any, otherwise at the end".
|
|
743
|
-
// (We never want the cursor to be before a punctuation mark because the next digit typed will appear after the punctuation mark, not before.)
|
|
744
|
-
// If we don't do this, the cursor automatically goes to the end when we set `this.phoneNumber`.
|
|
745
|
-
// This is sometimes undesired: for example, if we've ended up here because a Mac user typed `Alt + Backspace` in the middle.
|
|
746
|
-
|
|
747
|
-
// Arrow keys are intended to change the cursor position, so don't get in their way
|
|
748
|
-
if (
|
|
749
|
-
e.key.includes("Arrow") ||
|
|
750
|
-
["Meta", "Shift", "Control", "Alt"].includes(e.key)
|
|
751
|
-
) {
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const cursorPosition = this.phoneInput.selectionStart;
|
|
756
|
-
// find the numbers it's before and count backward from end after formatting
|
|
757
|
-
const numbersAfterCursor = cursorPosition
|
|
758
|
-
? this.phoneInput.value.slice(cursorPosition).replace(/\D/g, "")
|
|
759
|
-
: "";
|
|
760
|
-
this.phoneNumber = formatToPhoneInput(this.phoneInput.value);
|
|
761
|
-
|
|
762
|
-
// EXAMPLES: (123)| 4 numbersAfterCursor will be '4'.
|
|
763
|
-
let cursorNegativeIndex = 0;
|
|
764
|
-
let numbersLeft = numbersAfterCursor.length;
|
|
765
|
-
while (numbersLeft) {
|
|
766
|
-
if (this.phoneNumber.at(cursorNegativeIndex)?.match(/\d/)) {
|
|
767
|
-
numbersLeft--;
|
|
768
|
-
}
|
|
769
|
-
cursorNegativeIndex++;
|
|
770
|
-
}
|
|
771
|
-
const cursorPositiveIndex =
|
|
772
|
-
this.phoneInput.value.length - cursorNegativeIndex + 1;
|
|
773
|
-
this.phoneInput.setSelectionRange(cursorPositiveIndex, cursorPositiveIndex);
|
|
631
|
+
onChangePhoneNumber = (
|
|
632
|
+
event: CustomEvent<InternationalPhoneChangeDetail>
|
|
633
|
+
): void => {
|
|
634
|
+
this.phoneNumber = event.detail.formattedValue;
|
|
635
|
+
this.phoneNumberE164 = event.detail.e164;
|
|
636
|
+
this.phoneCountry = event.detail.country;
|
|
637
|
+
this.phoneNumberIsValid = event.detail.isValid;
|
|
774
638
|
};
|
|
775
639
|
|
|
776
640
|
onChangeEmail = (e: Event): void => {
|
|
@@ -788,10 +652,8 @@ export class TourScheduler extends LitElement {
|
|
|
788
652
|
return (
|
|
789
653
|
(!!this.firstNameInput?.value || !!this.lastNameInput?.value) &&
|
|
790
654
|
isValidEmail(this.emailInput?.value ?? "") &&
|
|
791
|
-
// TODO: deleting phone number doesn't cause validation to fail, at least on mobile
|
|
792
655
|
!!this.phoneNumber &&
|
|
793
|
-
this.
|
|
794
|
-
isValidPhoneNumber(this.phoneNumber)
|
|
656
|
+
!!this.phoneNumberE164
|
|
795
657
|
);
|
|
796
658
|
},
|
|
797
659
|
};
|
|
@@ -822,6 +684,11 @@ export class TourScheduler extends LitElement {
|
|
|
822
684
|
if (!this.selectedDate || !this.selectedTime || this.tourType === null) {
|
|
823
685
|
return;
|
|
824
686
|
}
|
|
687
|
+
const phoneNumber = this.phoneNumberE164;
|
|
688
|
+
if (!phoneNumber) {
|
|
689
|
+
this.phoneNumberIsValid = false;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
825
692
|
const queryParams = new URLSearchParams(window.location.search);
|
|
826
693
|
|
|
827
694
|
let parsedLeadSource = null;
|
|
@@ -860,7 +727,7 @@ export class TourScheduler extends LitElement {
|
|
|
860
727
|
this.selectedUnitEl?.value || this.selectedUnitValue || null;
|
|
861
728
|
pushGtmEvent("scheduleTourSubmitted", {
|
|
862
729
|
email: this.email,
|
|
863
|
-
phone:
|
|
730
|
+
phone: phoneNumber,
|
|
864
731
|
firstName: this.firstNameInput?.value ?? this.firstNameInputValue,
|
|
865
732
|
lastName: this.lastNameInput?.value ?? this.lastNameInputValue,
|
|
866
733
|
tourType: tourTypeForSubmission[this.tourType],
|
|
@@ -874,7 +741,7 @@ export class TourScheduler extends LitElement {
|
|
|
874
741
|
const data = {
|
|
875
742
|
referrer: document.referrer,
|
|
876
743
|
email_address: this.email,
|
|
877
|
-
phone_number:
|
|
744
|
+
phone_number: phoneNumber,
|
|
878
745
|
building_id: this.buildingId,
|
|
879
746
|
first_name: this.firstNameInput?.value ?? this.firstNameInputValue,
|
|
880
747
|
last_name: this.lastNameInput?.value ?? this.lastNameInputValue,
|
|
@@ -1414,35 +1281,16 @@ export class TourScheduler extends LitElement {
|
|
|
1414
1281
|
: ""}
|
|
1415
1282
|
</div>
|
|
1416
1283
|
<div class="inputContainer" id="phone">
|
|
1417
|
-
<input
|
|
1418
|
-
|
|
1419
|
-
["webchat-input"]: true,
|
|
1420
|
-
["webchat-font__desktop"]: !isMobile(),
|
|
1421
|
-
["webchat-font__mobile"]: isMobile(),
|
|
1422
|
-
["webchat-input__error"]:
|
|
1423
|
-
this.phoneNumber.length === 14 &&
|
|
1424
|
-
!isValidPhoneNumber(this.phoneNumber),
|
|
1425
|
-
})}
|
|
1426
|
-
type="tel"
|
|
1427
|
-
inputmode="tel"
|
|
1428
|
-
placeholder="Phone"
|
|
1429
|
-
name="phone"
|
|
1430
|
-
autocomplete="tel-national"
|
|
1431
|
-
maxlength="14"
|
|
1284
|
+
<international-phone-input
|
|
1285
|
+
.country=${this.phoneCountry ?? this.country}
|
|
1432
1286
|
.value=${this.phoneNumber}
|
|
1433
|
-
|
|
1434
|
-
@
|
|
1435
|
-
@
|
|
1436
|
-
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
this.phoneNumber = formatToPhoneInput(
|
|
1440
|
-
(e.target as HTMLInputElement).value
|
|
1441
|
-
);
|
|
1287
|
+
.invalid=${this.phoneNumberHasBlurred && !this.phoneNumberIsValid}
|
|
1288
|
+
@phone-change=${this.onChangePhoneNumber}
|
|
1289
|
+
@phone-blur=${() => {
|
|
1290
|
+
this.phoneNumberHasBlurred = true;
|
|
1442
1291
|
}}
|
|
1443
|
-
|
|
1444
|
-
${this.
|
|
1445
|
-
!isValidPhoneNumber(this.phoneNumber)
|
|
1292
|
+
></international-phone-input>
|
|
1293
|
+
${this.phoneNumberHasBlurred && !this.phoneNumberIsValid
|
|
1446
1294
|
? html`<p class="error-message">Invalid phone number</p>`
|
|
1447
1295
|
: ""}
|
|
1448
1296
|
</div>
|
|
@@ -1777,7 +1625,7 @@ export class TourScheduler extends LitElement {
|
|
|
1777
1625
|
changes.
|
|
1778
1626
|
${formDisclaimer({
|
|
1779
1627
|
buildingName: this.buildingName,
|
|
1780
|
-
phoneNumberInput: this.
|
|
1628
|
+
phoneNumberInput: this.phoneNumber,
|
|
1781
1629
|
emailInput: this.emailInput?.value,
|
|
1782
1630
|
orgLegalName: this.orgLegalName,
|
|
1783
1631
|
orgSlug: this.orgSlug,
|
|
@@ -1832,7 +1680,7 @@ export class TourScheduler extends LitElement {
|
|
|
1832
1680
|
changes.
|
|
1833
1681
|
${formDisclaimer({
|
|
1834
1682
|
buildingName: this.buildingName,
|
|
1835
|
-
phoneNumberInput: this.
|
|
1683
|
+
phoneNumberInput: this.phoneNumber,
|
|
1836
1684
|
emailInput: this.emailInput?.value,
|
|
1837
1685
|
orgLegalName: this.orgLegalName,
|
|
1838
1686
|
orgSlug: this.orgSlug,
|
|
@@ -1942,7 +1790,7 @@ export class TourScheduler extends LitElement {
|
|
|
1942
1790
|
? html`
|
|
1943
1791
|
${formDisclaimer({
|
|
1944
1792
|
buildingName: this.buildingName,
|
|
1945
|
-
phoneNumberInput: this.
|
|
1793
|
+
phoneNumberInput: this.phoneNumber,
|
|
1946
1794
|
emailInput: this.emailInput?.value,
|
|
1947
1795
|
orgLegalName: this.orgLegalName,
|
|
1948
1796
|
orgSlug: this.orgSlug,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import { getOfficeHourText } from "../OfficeHours";
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
formatToUsPhoneInput,
|
|
21
21
|
isModifierKey,
|
|
22
22
|
isNumericInput,
|
|
23
23
|
} from "./formatPhoneNumber";
|
|
@@ -279,7 +279,7 @@ export class CallUsWindow extends LitElement {
|
|
|
279
279
|
}
|
|
280
280
|
const inputElement = e.target as HTMLInputElement;
|
|
281
281
|
|
|
282
|
-
this.phoneNumberToText =
|
|
282
|
+
this.phoneNumberToText = formatToUsPhoneInput(inputElement.value);
|
|
283
283
|
|
|
284
284
|
this.phoneNumberInputRef.value.value = this.phoneNumberToText;
|
|
285
285
|
};
|
|
@@ -2,16 +2,12 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
|
|
2
2
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3
3
|
import { customElement, property, query, state } from "lit/decorators.js";
|
|
4
4
|
import { classMap } from "lit/directives/class-map.js";
|
|
5
|
-
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
|
6
5
|
import { installActionConfirmButton } from "./action-confirm-button";
|
|
7
6
|
import { installDetailsWindow } from "./details-window";
|
|
8
7
|
import "../me-select.ts";
|
|
9
8
|
import { MESelect } from "../me-select";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
isModifierKey,
|
|
13
|
-
isNumericInput,
|
|
14
|
-
} from "./formatPhoneNumber";
|
|
9
|
+
import "./international-phone-input";
|
|
10
|
+
import type { InternationalPhoneChangeDetail } from "./international-phone-input";
|
|
15
11
|
import { InputStyles } from "./InputStyles";
|
|
16
12
|
import axios from "axios";
|
|
17
13
|
import { FeatureFlagsShowDropdown } from "../../fetchFeatureFlag";
|
|
@@ -135,8 +131,6 @@ export class EmailUsWindow extends LitElement {
|
|
|
135
131
|
@property({ attribute: true })
|
|
136
132
|
featureFlagShowDropdown = "";
|
|
137
133
|
|
|
138
|
-
phoneNumberInputRef: Ref<HTMLInputElement> = createRef();
|
|
139
|
-
|
|
140
134
|
@property()
|
|
141
135
|
privatePolicyUrl = "https://www.meetelise.com/privacy";
|
|
142
136
|
@property()
|
|
@@ -151,6 +145,10 @@ export class EmailUsWindow extends LitElement {
|
|
|
151
145
|
@state()
|
|
152
146
|
phoneNumber = "";
|
|
153
147
|
@state()
|
|
148
|
+
phoneNumberE164: string | null = null;
|
|
149
|
+
@state()
|
|
150
|
+
phoneCountry: string | null = null;
|
|
151
|
+
@state()
|
|
154
152
|
message = "";
|
|
155
153
|
|
|
156
154
|
@query("me-select#leadSource")
|
|
@@ -200,18 +198,12 @@ export class EmailUsWindow extends LitElement {
|
|
|
200
198
|
this.email = (e.target as HTMLInputElement).value;
|
|
201
199
|
};
|
|
202
200
|
|
|
203
|
-
onChangePhoneNumber = (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
const inputElement = e.target as HTMLInputElement;
|
|
211
|
-
|
|
212
|
-
this.phoneNumber = formatToPhoneInput(inputElement.value);
|
|
213
|
-
|
|
214
|
-
this.phoneNumberInputRef.value.value = this.phoneNumber;
|
|
201
|
+
onChangePhoneNumber = (
|
|
202
|
+
e: CustomEvent<InternationalPhoneChangeDetail>
|
|
203
|
+
): void => {
|
|
204
|
+
this.phoneNumber = e.detail.formattedValue;
|
|
205
|
+
this.phoneNumberE164 = e.detail.e164;
|
|
206
|
+
this.phoneCountry = e.detail.country;
|
|
215
207
|
};
|
|
216
208
|
|
|
217
209
|
onChangeMessage = (e: Event): void => {
|
|
@@ -222,12 +214,6 @@ export class EmailUsWindow extends LitElement {
|
|
|
222
214
|
this.message = (e.target as HTMLTextAreaElement).value;
|
|
223
215
|
};
|
|
224
216
|
|
|
225
|
-
enforceFormat = (e: KeyboardEvent): void => {
|
|
226
|
-
if (!isNumericInput(e) && !isModifierKey(e)) {
|
|
227
|
-
e.preventDefault();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
217
|
validateFormFields = (): void => {
|
|
232
218
|
this.hasNameError = false;
|
|
233
219
|
this.hasEmailError = false;
|
|
@@ -240,7 +226,7 @@ export class EmailUsWindow extends LitElement {
|
|
|
240
226
|
if (!this.email || !isValidEmail(this.email)) {
|
|
241
227
|
this.hasEmailError = true;
|
|
242
228
|
}
|
|
243
|
-
if (!this.
|
|
229
|
+
if (!this.phoneNumberE164) {
|
|
244
230
|
this.hasPhoneNumberError = true;
|
|
245
231
|
}
|
|
246
232
|
this.windowHeight = 525 + 30 * this.getNumErrors();
|
|
@@ -259,6 +245,11 @@ export class EmailUsWindow extends LitElement {
|
|
|
259
245
|
) {
|
|
260
246
|
return;
|
|
261
247
|
}
|
|
248
|
+
const phoneNumber = this.phoneNumberE164;
|
|
249
|
+
if (!phoneNumber) {
|
|
250
|
+
this.hasPhoneNumberError = true;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
262
253
|
try {
|
|
263
254
|
this.isSubmitting = true;
|
|
264
255
|
// Height of the button when it's in the loading state
|
|
@@ -271,7 +262,7 @@ export class EmailUsWindow extends LitElement {
|
|
|
271
262
|
this.firstName,
|
|
272
263
|
this.lastName,
|
|
273
264
|
this.email,
|
|
274
|
-
|
|
265
|
+
phoneNumber,
|
|
275
266
|
this.message,
|
|
276
267
|
this.buildingId,
|
|
277
268
|
this.orgSlug,
|
|
@@ -427,23 +418,12 @@ export class EmailUsWindow extends LitElement {
|
|
|
427
418
|
`
|
|
428
419
|
: ""}
|
|
429
420
|
<div class="email-us__vertical-spacer"></div>
|
|
430
|
-
<input
|
|
431
|
-
|
|
432
|
-
type="text"
|
|
433
|
-
placeholder="Phone"
|
|
434
|
-
autocomplete="tel-national"
|
|
435
|
-
inputmode="tel"
|
|
436
|
-
class=${classMap({
|
|
437
|
-
["webchat-input"]: true,
|
|
438
|
-
["email-us__contact-input"]: true,
|
|
439
|
-
["webchat-font__desktop"]: !isMobile(),
|
|
440
|
-
["webchat-font__mobile"]: isMobile(),
|
|
441
|
-
})}
|
|
421
|
+
<international-phone-input
|
|
422
|
+
.country=${this.phoneCountry ?? this.country}
|
|
442
423
|
.value=${this.phoneNumber}
|
|
443
|
-
|
|
444
|
-
@
|
|
445
|
-
|
|
446
|
-
/>
|
|
424
|
+
.invalid=${this.hasPhoneNumberError}
|
|
425
|
+
@phone-change=${this.onChangePhoneNumber}
|
|
426
|
+
></international-phone-input>
|
|
447
427
|
${this.hasPhoneNumberError
|
|
448
428
|
? html`
|
|
449
429
|
<div class="email-us__error-text">
|
|
@@ -527,7 +507,7 @@ const createEmail = async (
|
|
|
527
507
|
firstName: string,
|
|
528
508
|
lastName: string,
|
|
529
509
|
email: string,
|
|
530
|
-
|
|
510
|
+
phoneNumber: string,
|
|
531
511
|
message: string,
|
|
532
512
|
buildingId: number,
|
|
533
513
|
orgSlug: string,
|
|
@@ -536,13 +516,6 @@ const createEmail = async (
|
|
|
536
516
|
chatId?: string | null,
|
|
537
517
|
leadSourcesWithTimestamps?: LeadSourceSubmittedListItem[]
|
|
538
518
|
) => {
|
|
539
|
-
const formattedPhoneNumber =
|
|
540
|
-
"+1" +
|
|
541
|
-
rawPhoneNumber
|
|
542
|
-
.replace("(", "")
|
|
543
|
-
.replace(")", "")
|
|
544
|
-
.replace(" ", "")
|
|
545
|
-
.replace("-", "");
|
|
546
519
|
const queryParams = new URLSearchParams(window.location.search);
|
|
547
520
|
const requestBody = {
|
|
548
521
|
email_address: email,
|
|
@@ -551,7 +524,7 @@ const createEmail = async (
|
|
|
551
524
|
first_message: message,
|
|
552
525
|
first_name: firstName,
|
|
553
526
|
last_name: lastName,
|
|
554
|
-
phone_number:
|
|
527
|
+
phone_number: phoneNumber,
|
|
555
528
|
referrer: document.referrer,
|
|
556
529
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
557
530
|
// @ts-ignore
|
|
@@ -565,7 +538,7 @@ const createEmail = async (
|
|
|
565
538
|
firstName,
|
|
566
539
|
lastName,
|
|
567
540
|
email,
|
|
568
|
-
phone:
|
|
541
|
+
phone: phoneNumber,
|
|
569
542
|
message,
|
|
570
543
|
originatingSource:
|
|
571
544
|
leadSources.find((i) => i !== "property-website") || null,
|
|
@@ -1,22 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCountries,
|
|
3
|
+
getCountryCallingCode,
|
|
4
|
+
parsePhoneNumberFromString,
|
|
5
|
+
} from "libphonenumber-js/min";
|
|
6
|
+
import type { CountryCode } from "libphonenumber-js/min";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PHONE_COUNTRY: CountryCode = "US";
|
|
9
|
+
const supportedCountries = new Set<string>(getCountries());
|
|
10
|
+
|
|
11
|
+
export type PhoneCountryOption = {
|
|
12
|
+
value: CountryCode;
|
|
13
|
+
label: string;
|
|
14
|
+
compactLabel: string;
|
|
15
|
+
countryName: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getPhoneCountry = (
|
|
19
|
+
country: string | null | undefined
|
|
20
|
+
): CountryCode => {
|
|
21
|
+
const normalizedCountry = country?.toUpperCase();
|
|
22
|
+
return normalizedCountry && supportedCountries.has(normalizedCountry)
|
|
23
|
+
? (normalizedCountry as CountryCode)
|
|
24
|
+
: DEFAULT_PHONE_COUNTRY;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getFlagEmoji = (country: CountryCode): string =>
|
|
28
|
+
String.fromCodePoint(
|
|
29
|
+
...[...country].map((character) => character.charCodeAt(0) + 127397)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const getRegionDisplayNames = (): Intl.DisplayNames | null => {
|
|
33
|
+
try {
|
|
34
|
+
return typeof Intl.DisplayNames === "function"
|
|
35
|
+
? new Intl.DisplayNames(["en"], { type: "region" })
|
|
36
|
+
: null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const PINNED_PHONE_COUNTRIES: CountryCode[] = ["US", "CA", "MX", "GB"];
|
|
43
|
+
|
|
44
|
+
export const phoneCountryOptions: PhoneCountryOption[] = (() => {
|
|
45
|
+
const displayNames = getRegionDisplayNames();
|
|
46
|
+
return getCountries()
|
|
47
|
+
.map((country) => {
|
|
48
|
+
const countryName = displayNames?.of(country) ?? country;
|
|
49
|
+
const flag = getFlagEmoji(country);
|
|
50
|
+
const callingCode = getCountryCallingCode(country);
|
|
51
|
+
return {
|
|
52
|
+
value: country,
|
|
53
|
+
label: `${flag} +${callingCode} – ${countryName}`,
|
|
54
|
+
compactLabel: `${flag} +${callingCode}`,
|
|
55
|
+
countryName,
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
.sort((left, right) => {
|
|
59
|
+
const leftPinnedIndex = PINNED_PHONE_COUNTRIES.indexOf(left.value);
|
|
60
|
+
const rightPinnedIndex = PINNED_PHONE_COUNTRIES.indexOf(right.value);
|
|
61
|
+
if (leftPinnedIndex !== -1 || rightPinnedIndex !== -1) {
|
|
62
|
+
if (leftPinnedIndex === -1) return 1;
|
|
63
|
+
if (rightPinnedIndex === -1) return -1;
|
|
64
|
+
return leftPinnedIndex - rightPinnedIndex;
|
|
65
|
+
}
|
|
66
|
+
return left.countryName.localeCompare(right.countryName, "en");
|
|
67
|
+
});
|
|
68
|
+
})();
|
|
69
|
+
|
|
1
70
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
71
|
+
* Formats a phone number as the user types. Numbers beginning with `+` infer
|
|
72
|
+
* their country from the calling code; national numbers use `country`.
|
|
4
73
|
*/
|
|
5
|
-
export const formatToPhoneInput = (
|
|
74
|
+
export const formatToPhoneInput = (
|
|
75
|
+
phoneNumber: string,
|
|
76
|
+
country: string | null = DEFAULT_PHONE_COUNTRY
|
|
77
|
+
): string => {
|
|
78
|
+
const selectedCountry = getPhoneCountry(country);
|
|
79
|
+
const parsedPhoneNumber = parsePhoneNumberFromString(
|
|
80
|
+
phoneNumber,
|
|
81
|
+
selectedCountry
|
|
82
|
+
);
|
|
83
|
+
if (parsedPhoneNumber?.isValid()) {
|
|
84
|
+
return phoneNumber.trim().startsWith("+")
|
|
85
|
+
? parsedPhoneNumber.formatInternational()
|
|
86
|
+
: parsedPhoneNumber.formatNational();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
!phoneNumber.trim().startsWith("+") &&
|
|
91
|
+
(selectedCountry === "US" || selectedCountry === "CA")
|
|
92
|
+
) {
|
|
93
|
+
return formatToUsPhoneInput(phoneNumber);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const digits = phoneNumber.replace(/\D/g, "").substring(0, 15);
|
|
97
|
+
return phoneNumber.trim().startsWith("+") ? `+${digits}` : digits;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const getCountryFromPhoneInput = (
|
|
101
|
+
phoneNumber: string,
|
|
102
|
+
country: string | null = DEFAULT_PHONE_COUNTRY
|
|
103
|
+
): CountryCode => {
|
|
104
|
+
const selectedCountry = getPhoneCountry(country);
|
|
105
|
+
if (!phoneNumber.trim().startsWith("+")) return selectedCountry;
|
|
106
|
+
const parsedPhoneNumber = parsePhoneNumberFromString(
|
|
107
|
+
phoneNumber,
|
|
108
|
+
selectedCountry
|
|
109
|
+
);
|
|
110
|
+
return parsedPhoneNumber?.country ?? selectedCountry;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getNationalPhoneNumber = (
|
|
114
|
+
phoneNumber: string,
|
|
115
|
+
country: string | null = DEFAULT_PHONE_COUNTRY
|
|
116
|
+
): string => {
|
|
117
|
+
const parsedPhoneNumber = parsePhoneNumberFromString(
|
|
118
|
+
phoneNumber,
|
|
119
|
+
getPhoneCountry(country)
|
|
120
|
+
);
|
|
121
|
+
return parsedPhoneNumber?.nationalNumber ?? phoneNumber.replace(/\D/g, "");
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const getE164PhoneNumber = (
|
|
125
|
+
phoneNumber: string,
|
|
126
|
+
country: string | null = DEFAULT_PHONE_COUNTRY
|
|
127
|
+
): string | null => {
|
|
128
|
+
const parsedPhoneNumber = parsePhoneNumberFromString(
|
|
129
|
+
phoneNumber,
|
|
130
|
+
getPhoneCountry(country)
|
|
131
|
+
);
|
|
132
|
+
return parsedPhoneNumber?.isValid() ? parsedPhoneNumber.number : null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const isValidInternationalPhoneNumber = (
|
|
136
|
+
phoneNumber: string,
|
|
137
|
+
country: string | null = DEFAULT_PHONE_COUNTRY
|
|
138
|
+
): boolean => getE164PhoneNumber(phoneNumber, country) !== null;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Text Us remains limited to +1 destinations because outbound SMS currently
|
|
142
|
+
* skips other calling codes. Keep its ten-digit behavior isolated from the
|
|
143
|
+
* international Tour and Email Us input.
|
|
144
|
+
*/
|
|
145
|
+
export const formatToUsPhoneInput = (phoneNumber: string): string => {
|
|
6
146
|
const input = phoneNumber.replace(/\D/g, "").substring(0, 10);
|
|
7
147
|
const areaCode = input.substring(0, 3);
|
|
8
148
|
const middle = input.substring(3, 6);
|
|
9
149
|
const last = input.substring(6, 10);
|
|
10
150
|
|
|
11
|
-
if (input.length > 5) {
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
if (input.length > 2) {
|
|
15
|
-
return `(${areaCode}) ${middle}`;
|
|
16
|
-
}
|
|
17
|
-
if (input.length > 0) {
|
|
18
|
-
return `(${areaCode}`;
|
|
19
|
-
}
|
|
151
|
+
if (input.length > 5) return `(${areaCode}) ${middle}-${last}`;
|
|
152
|
+
if (input.length > 2) return `(${areaCode}) ${middle}`;
|
|
153
|
+
if (input.length > 0) return `(${areaCode}`;
|
|
20
154
|
return "";
|
|
21
155
|
};
|
|
22
156
|
|