feat: Custom phone input in pre-chat form (#7275)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -7,9 +7,14 @@ import store from '../widget/store';
|
||||
import App from '../widget/App.vue';
|
||||
import ActionCableConnector from '../widget/helpers/actionCable';
|
||||
import i18n from '../widget/i18n';
|
||||
import { isPhoneE164OrEmpty } from 'shared/helpers/Validators';
|
||||
import {
|
||||
startsWithPlus,
|
||||
isPhoneNumberValidWithDialCode,
|
||||
} from 'shared/helpers/Validators';
|
||||
import router from '../widget/router';
|
||||
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
|
||||
const PhoneInput = () => import('../widget/components/Form/PhoneInput');
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(Vuelidate);
|
||||
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||
@@ -19,8 +24,18 @@ const i18nConfig = new VueI18n({
|
||||
messages: i18n,
|
||||
});
|
||||
Vue.use(VueFormulate, {
|
||||
library: {
|
||||
phoneInput: {
|
||||
classification: 'number',
|
||||
component: PhoneInput,
|
||||
slotProps: {
|
||||
component: ['placeholder', 'hasErrorInPhoneInput'],
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
isPhoneE164OrEmpty: ({ value }) => isPhoneE164OrEmpty(value),
|
||||
startsWithPlus: ({ value }) => startsWithPlus(value),
|
||||
isValidPhoneNumber: ({ value }) => isPhoneNumberValidWithDialCode(value),
|
||||
},
|
||||
classes: {
|
||||
outer: 'mb-4 wrapper',
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
|
||||
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
|
||||
"checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
|
||||
"chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z",
|
||||
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
|
||||
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
|
||||
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
||||
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",
|
||||
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
|
||||
"globe-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z",
|
||||
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
export const isPhoneE164 = value => !!value.match(/^\+[1-9]\d{1,14}$/);
|
||||
|
||||
export const isPhoneNumberValid = (value, dialCode) => {
|
||||
const number = value.replace(dialCode, '');
|
||||
return !!number.match(/^[0-9]{1,14}$/);
|
||||
};
|
||||
|
||||
export const isPhoneE164OrEmpty = value => isPhoneE164(value) || value === '';
|
||||
|
||||
export const isPhoneNumberValidWithDialCode = value => {
|
||||
const number = value.replace(/^\+/, ''); // Remove the '+' sign
|
||||
return !!number.match(/^[1-9]\d{4,}$/); // Validate the phone number with minimum 5 digits
|
||||
};
|
||||
|
||||
export const startsWithPlus = value => value.startsWith('+');
|
||||
|
||||
export const shouldBeUrl = (value = '') =>
|
||||
value ? value.startsWith('http') : true;
|
||||
|
||||
export const isValidPassword = value => {
|
||||
const containsUppercase = /[A-Z]/.test(value);
|
||||
const containsLowercase = /[a-z]/.test(value);
|
||||
@@ -20,7 +31,9 @@ export const isValidPassword = value => {
|
||||
containsSpecialCharacter
|
||||
);
|
||||
};
|
||||
|
||||
export const isNumber = value => /^\d+$/.test(value);
|
||||
|
||||
export const isDomain = value => {
|
||||
if (value !== '') {
|
||||
const domainRegex = /^([\p{L}0-9]+(-[\p{L}0-9]+)*\.)+[a-z]{2,}$/gmu;
|
||||
|
||||
@@ -1,12 +1,56 @@
|
||||
import { shouldBeUrl } from '../Validators';
|
||||
import { isValidPassword } from '../Validators';
|
||||
import { isNumber } from '../Validators';
|
||||
import { isDomain } from '../Validators';
|
||||
import {
|
||||
shouldBeUrl,
|
||||
isPhoneNumberValidWithDialCode,
|
||||
isPhoneE164OrEmpty,
|
||||
isPhoneE164,
|
||||
startsWithPlus,
|
||||
isValidPassword,
|
||||
isPhoneNumberValid,
|
||||
isNumber,
|
||||
isDomain,
|
||||
} from '../Validators';
|
||||
|
||||
describe('#shouldBeUrl', () => {
|
||||
it('should return correct url', () => {
|
||||
expect(shouldBeUrl('http')).toEqual(true);
|
||||
});
|
||||
it('should return wrong url', () => {
|
||||
expect(shouldBeUrl('')).toEqual(true);
|
||||
expect(shouldBeUrl('abc')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneE164', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneE164('+1234567890')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneE164('1234567890')).toEqual(false);
|
||||
expect(isPhoneE164('12345678A9')).toEqual(false);
|
||||
expect(isPhoneE164('+12345678901234567890')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneE164OrEmpty', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneE164OrEmpty('+1234567890')).toEqual(true);
|
||||
expect(isPhoneE164OrEmpty('')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneE164OrEmpty('1234567890')).toEqual(false);
|
||||
expect(isPhoneE164OrEmpty('12345678A9')).toEqual(false);
|
||||
expect(isPhoneE164OrEmpty('+12345678901234567890')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneNumberValid', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneNumberValid('1234567890', '+91')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneNumberValid('12345A67890', '+1')).toEqual(false);
|
||||
expect(isPhoneNumberValid('12345A6789120', '+1')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isValidPassword', () => {
|
||||
@@ -51,3 +95,23 @@ describe('#isDomain', () => {
|
||||
expect(isDomain('https://test.in')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isPhoneNumberValidWithDialCode', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(isPhoneNumberValidWithDialCode('+123456789')).toEqual(true);
|
||||
expect(isPhoneNumberValidWithDialCode('+12345')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(isPhoneNumberValidWithDialCode('+123')).toEqual(false);
|
||||
expect(isPhoneNumberValidWithDialCode('+1234')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#startsWithPlus', () => {
|
||||
it('should return correct phone number', () => {
|
||||
expect(startsWithPlus('+123456789')).toEqual(true);
|
||||
});
|
||||
it('should return wrong phone number', () => {
|
||||
expect(startsWithPlus('123456789')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
310
app/javascript/widget/components/Form/PhoneInput.vue
Normal file
310
app/javascript/widget/components/Form/PhoneInput.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="phone-input--wrap relative mt-2">
|
||||
<div
|
||||
class="phone-input rounded w-full flex items-center justify-start outline-none border border-solid"
|
||||
:class="inputHasError"
|
||||
>
|
||||
<div
|
||||
class="country-emoji--wrap h-full cursor-pointer flex items-center justify-between px-2 py-2"
|
||||
:class="dropdownClass"
|
||||
@click="toggleCountryDropdown"
|
||||
>
|
||||
<h5 v-if="activeCountry.emoji" class="text-xl mb-0">
|
||||
{{ activeCountry.emoji }}
|
||||
</h5>
|
||||
<fluent-icon v-else icon="globe" class="fluent-icon" size="20" />
|
||||
<fluent-icon icon="chevron-down" class="fluent-icon" size="12" />
|
||||
</div>
|
||||
<span
|
||||
v-if="activeDialCode"
|
||||
class="py-2 pr-0 pl-2 text-base"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ activeDialCode }}
|
||||
</span>
|
||||
<input
|
||||
:value="phoneNumber"
|
||||
type="phoneInput"
|
||||
class="border-0 w-full py-2 pl-2 pr-3 leading-tight outline-none h-full rounded-r"
|
||||
name="phoneNumber"
|
||||
:placeholder="placeholder"
|
||||
:class="inputLightAndDarkModeColor"
|
||||
@input="onChange"
|
||||
@blur="context.blurHandler"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
ref="dropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
:class="dropdownBackgroundClass"
|
||||
class="country-dropdown h-48 overflow-y-auto z-10 absolute top-12 px-0 pt-0 pl-1 pr-1 pb-1 rounded shadow-lg"
|
||||
>
|
||||
<div class="sticky top-0" :class="dropdownBackgroundClass">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="searchCountry"
|
||||
type="text"
|
||||
placeholder="Search country"
|
||||
class="dropdown-search h-8 text-sm mb-1 mt-1 w-full rounded py-2 px-3 outline-none border border-solid"
|
||||
:class="[$dm('bg-slate-50', 'dark:bg-slate-600'), inputBorderColor]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(country, index) in items"
|
||||
ref="dropdownItem"
|
||||
:key="index"
|
||||
class="country-dropdown--item h-8 flex items-center cursor-pointer rounded py-2 px-2"
|
||||
:class="[
|
||||
dropdownItemClass,
|
||||
country.id === activeCountryCode ? activeDropdownItemClass : '',
|
||||
index === selectedIndex ? focusedDropdownItemClass : '',
|
||||
]"
|
||||
@click="onSelectCountry(country)"
|
||||
>
|
||||
<span v-if="country.emoji" class="mr-2 text-xl">{{
|
||||
country.emoji
|
||||
}}</span>
|
||||
<span class="truncate text-sm leading-5">
|
||||
{{ country.name }}
|
||||
</span>
|
||||
<span class="ml-2 text-xs">{{ country.dial_code }}</span>
|
||||
</div>
|
||||
<div v-if="items.length === 0">
|
||||
<span
|
||||
class="text-sm mt-4 justify-center text-center flex"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ this.$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_EMPTY') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js';
|
||||
import FormulateInputMixin from '@braid/vue-formulate/src/FormulateInputMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [
|
||||
mentionSelectionKeyboardMixin,
|
||||
FormulateInputMixin,
|
||||
darkModeMixin,
|
||||
clickaway,
|
||||
],
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasErrorInPhoneInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: -1,
|
||||
showDropdown: false,
|
||||
searchCountry: '',
|
||||
activeCountryCode: '',
|
||||
activeDialCode: '',
|
||||
phoneNumber: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
countries() {
|
||||
return [
|
||||
{
|
||||
name: this.dropdownFirstItemName,
|
||||
dial_code: '',
|
||||
emoji: '',
|
||||
id: '',
|
||||
},
|
||||
...countries,
|
||||
];
|
||||
},
|
||||
dropdownFirstItemName() {
|
||||
return this.activeCountryCode ? 'Clear selection' : 'Select Country';
|
||||
},
|
||||
dropdownClass() {
|
||||
return `${this.$dm('bg-slate-100', 'dark:bg-slate-700')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
dropdownBackgroundClass() {
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-700')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
dropdownItemClass() {
|
||||
return `${this.$dm('text-slate-700', 'dark:text-slate-50')} ${this.$dm(
|
||||
'hover:bg-slate-50',
|
||||
'dark:hover:bg-slate-600'
|
||||
)}`;
|
||||
},
|
||||
activeDropdownItemClass() {
|
||||
return `active ${this.$dm('bg-slate-100', 'dark:bg-slate-800')}`;
|
||||
},
|
||||
focusedDropdownItemClass() {
|
||||
return `focus ${this.$dm('bg-slate-50', 'dark:bg-slate-600')}`;
|
||||
},
|
||||
inputHasError() {
|
||||
return this.hasErrorInPhoneInput
|
||||
? `border-red-200 hover:border-red-300 focus:border-red-300 ${this.inputLightAndDarkModeColor}`
|
||||
: `hover:border-black-300 focus:border-black-300 ${this.inputLightAndDarkModeColor} ${this.inputBorderColor}`;
|
||||
},
|
||||
inputBorderColor() {
|
||||
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||
},
|
||||
inputLightAndDarkModeColor() {
|
||||
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||
'text-slate-700',
|
||||
'dark:text-slate-50'
|
||||
)}`;
|
||||
},
|
||||
items() {
|
||||
return this.countries.filter(country => {
|
||||
const { name, dial_code, id } = country;
|
||||
const search = this.searchCountry.toLowerCase();
|
||||
return (
|
||||
name.toLowerCase().includes(search) ||
|
||||
dial_code.toLowerCase().includes(search) ||
|
||||
id.toLowerCase().includes(search)
|
||||
);
|
||||
});
|
||||
},
|
||||
activeCountry() {
|
||||
return (
|
||||
this.countries.find(country => country.id === this.activeCountryCode) ||
|
||||
''
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setContextValue(code) {
|
||||
// This function is used to set the context value.
|
||||
// The context value is used to set the value of the phone number field in the pre-chat form.
|
||||
this.context.model = `${code}${this.phoneNumber}`;
|
||||
},
|
||||
dynamicallySetCountryCode(value) {
|
||||
// This function is used to set the country code dynamically.
|
||||
// The country and dial code is used to set from the value of the phone number field in the pre-chat form.
|
||||
if (!value) return;
|
||||
|
||||
// check the number first four digit and check weather it is available in the countries array or not.
|
||||
const country = countries.find(code => value.startsWith(code.dial_code));
|
||||
if (country) {
|
||||
// if it is available then set the country code and dial code.
|
||||
this.activeCountryCode = country.id;
|
||||
this.activeDialCode = country.dial_code;
|
||||
// set the phone number without dial code.
|
||||
this.phoneNumber = value.replace(country.dial_code, '');
|
||||
}
|
||||
},
|
||||
onChange(e) {
|
||||
this.phoneNumber = e.target.value;
|
||||
this.dynamicallySetCountryCode(this.phoneNumber);
|
||||
// This function is used to set the context value when the user types in the phone number field.
|
||||
this.setContextValue(this.activeDialCode);
|
||||
},
|
||||
dropdownItem() {
|
||||
// This function is used to get all the items in the dropdown.
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
'div.country-dropdown div.country-dropdown--item'
|
||||
)
|
||||
);
|
||||
},
|
||||
focusedOrActiveItem(className) {
|
||||
// This function is used to get the focused or active item in the dropdown.
|
||||
return Array.from(
|
||||
this.$refs.dropdown.querySelectorAll(
|
||||
`div.country-dropdown div.country-dropdown--item.${className}`
|
||||
)
|
||||
);
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
if (this.showDropdown) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('focus'));
|
||||
}
|
||||
},
|
||||
onSelect() {
|
||||
this.onSelectCountry(this.items[this.selectedIndex]);
|
||||
},
|
||||
scrollToFocusedOrActiveItem(item) {
|
||||
// This function is used to scroll the dropdown to the focused or active item.
|
||||
const focusedOrActiveItem = item;
|
||||
if (focusedOrActiveItem.length > 0) {
|
||||
const dropdown = this.$refs.dropdown;
|
||||
const dropdownHeight = dropdown.clientHeight;
|
||||
const itemTop = focusedOrActiveItem[0].offsetTop;
|
||||
const itemHeight = focusedOrActiveItem[0].offsetHeight;
|
||||
const scrollPosition = itemTop - dropdownHeight / 2 + itemHeight / 2;
|
||||
dropdown.scrollTo({
|
||||
top: scrollPosition,
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
},
|
||||
onSelectCountry(country) {
|
||||
this.activeCountryCode = country.id;
|
||||
this.searchCountry = '';
|
||||
this.activeDialCode = country.dial_code ? country.dial_code : '';
|
||||
this.setContextValue(country.dial_code);
|
||||
this.closeDropdown();
|
||||
},
|
||||
toggleCountryDropdown() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
this.selectedIndex = -1;
|
||||
if (this.showDropdown) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.searchbar.focus();
|
||||
// This is used to scroll the dropdown to the active item.
|
||||
this.scrollToFocusedOrActiveItem(this.focusedOrActiveItem('active'));
|
||||
});
|
||||
}
|
||||
},
|
||||
closeDropdown() {
|
||||
this.selectedIndex = -1;
|
||||
this.showDropdown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
.phone-input--wrap {
|
||||
.phone-input {
|
||||
height: 2.8rem;
|
||||
|
||||
input:placeholder-shown {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.country-emoji--wrap {
|
||||
border-bottom-left-radius: 0.18rem;
|
||||
border-top-left-radius: 0.18rem;
|
||||
min-width: 3.6rem;
|
||||
width: 3.6rem;
|
||||
}
|
||||
|
||||
.country-dropdown {
|
||||
min-width: 6rem;
|
||||
max-width: 14.8rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,10 +22,14 @@
|
||||
:label-class="context => labelClass(context)"
|
||||
:input-class="context => inputClass(context)"
|
||||
:validation-messages="{
|
||||
isPhoneE164OrEmpty: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'),
|
||||
startsWithPlus: $t(
|
||||
'PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DIAL_CODE_VALID_ERROR'
|
||||
),
|
||||
isValidPhoneNumber: $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR'),
|
||||
email: $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'),
|
||||
required: $t('PRE_CHAT_FORM.REQUIRED'),
|
||||
}"
|
||||
:has-error-in-phone-input="hasErrorInPhoneInput"
|
||||
/>
|
||||
<FormulateInput
|
||||
v-if="!hasActiveCampaign"
|
||||
@@ -64,6 +68,7 @@ import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||
import configMixin from 'widget/mixins/configMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomButton,
|
||||
@@ -79,6 +84,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
locale: this.$root.$i18n.locale,
|
||||
hasErrorInPhoneInput: false,
|
||||
message: '',
|
||||
formValues: {},
|
||||
labels: {
|
||||
@@ -143,7 +149,10 @@ export default {
|
||||
.filter(field => field.enabled)
|
||||
.map(field => ({
|
||||
...field,
|
||||
type: this.findFieldType(field.type),
|
||||
type:
|
||||
field.name === 'phoneNumber'
|
||||
? 'phoneInput'
|
||||
: this.findFieldType(field.type),
|
||||
}));
|
||||
},
|
||||
conversationCustomAttributes() {
|
||||
@@ -202,6 +211,9 @@ export default {
|
||||
if (classification === 'box' && type === 'checkbox') {
|
||||
return '';
|
||||
}
|
||||
if (type === 'phoneInput') {
|
||||
this.hasErrorInPhoneInput = hasErrors;
|
||||
}
|
||||
if (!hasErrors) {
|
||||
return `${this.inputStyles} hover:border-black-300 focus:border-black-300 ${this.isInputDarkOrLightMode} ${this.inputBorderColor}`;
|
||||
}
|
||||
@@ -224,12 +236,9 @@ export default {
|
||||
return this.formValues[name] || null;
|
||||
},
|
||||
getValidation({ type, name }) {
|
||||
if (!this.isContactFieldRequired(name)) {
|
||||
return '';
|
||||
}
|
||||
const validations = {
|
||||
emailAddress: 'email',
|
||||
phoneNumber: 'isPhoneE164OrEmpty',
|
||||
phoneNumber: 'startsWithPlus|isValidPhoneNumber',
|
||||
url: 'url',
|
||||
date: 'date',
|
||||
text: null,
|
||||
@@ -238,11 +247,17 @@ export default {
|
||||
checkbox: false,
|
||||
};
|
||||
const validationKeys = Object.keys(validations);
|
||||
const validation = 'bail|required';
|
||||
const isRequired = this.isContactFieldRequired(name);
|
||||
const validation = isRequired ? 'bail|required' : 'bail|optional';
|
||||
|
||||
if (validationKeys.includes(name) || validationKeys.includes(type)) {
|
||||
const validationType = validations[type] || validations[name];
|
||||
return validationType ? `${validation}|${validationType}` : validation;
|
||||
const validationString = validationType
|
||||
? `${validation}|${validationType}`
|
||||
: validation;
|
||||
return validationString;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
findFieldType(type) {
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
"LABEL": "Phone Number",
|
||||
"PLACEHOLDER": "Please enter your phone number",
|
||||
"REQUIRED_ERROR": "Phone Number is required",
|
||||
"VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555"
|
||||
"DIAL_CODE_VALID_ERROR": "Please select a country code",
|
||||
"VALID_ERROR": "Please enter a valid phone number",
|
||||
"DROPDOWN_EMPTY": "No results found"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
|
||||
Reference in New Issue
Block a user