ENH: Phone Input Component (#189)
* #170-ENH: Created custom dropdown phone input * #170-ENH: Added phone_number rules * #170-ENH: Added phone_number rules * #170-ENH: Added separate Rule for phone number input, starting 0 phone number is ignored, added regex to ignore non digit phone input * #170-ENH: Removed global registration of CountryFlag * #170-ENH: Using VSelect component for country selection, added prop for dropdown styling * #170-ENH: Updated phone number rule * #170-ENH: Added margins to country selector --------- Co-authored-by: Sutirtha <sdas@republicfinance.com> Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
f775ab84c7
commit
a53677d2ed
|
@ -11,6 +11,7 @@ use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Rules\ValidHCaptcha;
|
use App\Rules\ValidHCaptcha;
|
||||||
|
use App\Rules\ValidPhoneInputRule;
|
||||||
|
|
||||||
class AnswerFormRequest extends FormRequest
|
class AnswerFormRequest extends FormRequest
|
||||||
{
|
{
|
||||||
|
@ -150,7 +151,6 @@ class AnswerFormRequest extends FormRequest
|
||||||
{
|
{
|
||||||
switch ($property['type']) {
|
switch ($property['type']) {
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'phone_number':
|
|
||||||
case 'signature':
|
case 'signature':
|
||||||
return ['string'];
|
return ['string'];
|
||||||
case 'number':
|
case 'number':
|
||||||
|
@ -189,6 +189,8 @@ class AnswerFormRequest extends FormRequest
|
||||||
return ['array', 'min:2'];
|
return ['array', 'min:2'];
|
||||||
}
|
}
|
||||||
return $this->getRulesForDate($property);
|
return $this->getRulesForDate($property);
|
||||||
|
case 'phone_number':
|
||||||
|
return [new ValidPhoneInputRule];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ValidPhoneInputRule implements Rule
|
||||||
|
{
|
||||||
|
public function passes($attribute, $value)
|
||||||
|
{
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Str::startsWith($value, '+')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$parts = explode(' ', $value);
|
||||||
|
if (count($parts) < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return strlen($parts[1]) >= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
return 'The :attribute must be a string that starts with a "+" character and must be at least 5 digits long.';
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div :class="wrapperClass" :style="inputStyle">
|
||||||
|
<slot name="label">
|
||||||
|
<label v-if="label" :for="id ? id : name"
|
||||||
|
:class="[theme.default.label, { 'uppercase text-xs': uppercaseLabels, 'text-sm': !uppercaseLabels }]">
|
||||||
|
{{ label }}
|
||||||
|
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||||
|
</label>
|
||||||
|
</slot>
|
||||||
|
<div v-if="help && helpPosition == 'above_input'" class="flex mb-1">
|
||||||
|
<small :class="theme.default.help" class="grow">
|
||||||
|
<slot name="help"><span class="field-help" v-html="help" /></slot>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div :id="id ? id : name" :disabled="disabled" :name="name" :style="inputStyle" class="flex items-center">
|
||||||
|
<v-select class="w-1/4 mt-1" :data="countries" :value="selectedCountryCode" :inner-style="{ width: '474px' }"
|
||||||
|
:searchable="true" :search-keys="['name']" :option-key="'code'" :color="'#3B82F6'"
|
||||||
|
:placeholder="'Select a country'" :uppercase-labels="true" :theme="theme" @input="onCountryChange">
|
||||||
|
<template #option="props">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<country-flag :country="props.option.code" />
|
||||||
|
<span>{{ props.option.name }}</span>
|
||||||
|
<span>{{ props.option.code }}</span>
|
||||||
|
<span>{{ props.option.dial_code }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #selected="props">
|
||||||
|
<div class="flex items-center space-x-2 justify-start">
|
||||||
|
<country-flag :country="props.option.code" :style="{
|
||||||
|
'margin-top': '-0.7em',
|
||||||
|
'margin-left': '-0.9em',
|
||||||
|
'margin-right': '-0.6em',
|
||||||
|
'margin-bottom': '-0.7em'
|
||||||
|
}" />
|
||||||
|
<span>{{ props.option.dial_code }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
<input v-model="inputVal" type="text" class="inline-flex-grow ml-5"
|
||||||
|
:class="[theme.default.input, { '!ring-red-500 !ring-2': hasValidation && form.errors.has(name), '!cursor-not-allowed !bg-gray-200': disabled }]"
|
||||||
|
:placeholder="placeholder" :style="inputStyle" @input="onInput">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { directive as onClickaway } from 'vue-clickaway'
|
||||||
|
import inputMixin from '~/mixins/forms/input.js'
|
||||||
|
import countryCodes from '../../../data/country_codes.json'
|
||||||
|
import CountryFlag from 'vue-country-flag'
|
||||||
|
import VSelect from './components/VSelect.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
phone: 'PhoneInput',
|
||||||
|
components: {
|
||||||
|
CountryFlag, VSelect
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
onClickaway: onClickaway
|
||||||
|
},
|
||||||
|
mixins: [inputMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedCountryCode: countryCodes[234],
|
||||||
|
countries: countryCodes,
|
||||||
|
isOpen: false,
|
||||||
|
inputVal: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
inputVal(newVal, oldVal) {
|
||||||
|
if (newVal.startsWith('0')) {
|
||||||
|
newVal = newVal.replace(/^0+/, '')
|
||||||
|
}
|
||||||
|
this.compVal = this.selectedCountryCode.dial_code + ' ' + newVal
|
||||||
|
},
|
||||||
|
selectedCountryCode(newVal, oldVal) {
|
||||||
|
if (this.compVal) {
|
||||||
|
this.compVal = this.compVal.replace(oldVal.dial_code, newVal.dial_code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onCountryChange(country) {
|
||||||
|
this.selectedCountryCode = country
|
||||||
|
this.closeDropdown()
|
||||||
|
},
|
||||||
|
closeDropdown() {
|
||||||
|
this.isOpen = false
|
||||||
|
},
|
||||||
|
onInput(event) {
|
||||||
|
const input = event.target.value
|
||||||
|
const digitsOnly = input.replace(/[^0-9]/g, '')
|
||||||
|
this.inputVal = digitsOnly
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -43,6 +43,7 @@
|
||||||
<!-- Select popover, show/hide based on select state. -->
|
<!-- Select popover, show/hide based on select state. -->
|
||||||
<div v-show="isOpen" :dusk="dusk+'_dropdown' "
|
<div v-show="isOpen" :dusk="dusk+'_dropdown' "
|
||||||
class="absolute mt-1 w-full rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10"
|
class="absolute mt-1 w-full rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10"
|
||||||
|
:style="innerStyle"
|
||||||
>
|
>
|
||||||
<ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3"
|
<ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3"
|
||||||
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
||||||
|
@ -99,6 +100,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
data: Array,
|
data: Array,
|
||||||
value: { default: null },
|
value: { default: null },
|
||||||
|
innerStyle: { type: Object, default: null },
|
||||||
label: { type: String, default: null },
|
label: { type: String, default: null },
|
||||||
dusk: { type: String, default: null },
|
dusk: { type: String, default: null },
|
||||||
loading: { type: Boolean, default: false },
|
loading: { type: Boolean, default: false },
|
||||||
|
|
|
@ -41,3 +41,4 @@ import ToggleSwitchInput from './ToggleSwitchInput.vue'
|
||||||
Vue.component('SignatureInput', () => import('./SignatureInput.vue'))
|
Vue.component('SignatureInput', () => import('./SignatureInput.vue'))
|
||||||
Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput.vue'))
|
Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput.vue'))
|
||||||
Vue.component('DateInput', () => import('./DateInput.vue'))
|
Vue.component('DateInput', () => import('./DateInput.vue'))
|
||||||
|
Vue.component('PhoneInput', () => import('./PhoneInput.vue'))
|
||||||
|
|
|
@ -69,11 +69,14 @@
|
||||||
<script>
|
<script>
|
||||||
import FormLogicPropertyResolver from '../../../forms/FormLogicPropertyResolver.js'
|
import FormLogicPropertyResolver from '../../../forms/FormLogicPropertyResolver.js'
|
||||||
import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js'
|
import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js'
|
||||||
|
import PhoneInput from '../../forms/PhoneInput.vue'
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OpenFormField',
|
name: 'OpenFormField',
|
||||||
components: { },
|
components: {
|
||||||
|
PhoneInput
|
||||||
|
},
|
||||||
mixins: [FormPendingSubmissionKey],
|
mixins: [FormPendingSubmissionKey],
|
||||||
props: {
|
props: {
|
||||||
form: {
|
form: {
|
||||||
|
@ -122,7 +125,7 @@ export default {
|
||||||
checkbox: 'CheckboxInput',
|
checkbox: 'CheckboxInput',
|
||||||
url: 'TextInput',
|
url: 'TextInput',
|
||||||
email: 'TextInput',
|
email: 'TextInput',
|
||||||
phone_number: 'TextInput',
|
phone_number: 'PhoneInput'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
@ -246,7 +249,7 @@ export default {
|
||||||
placeholder: field.placeholder,
|
placeholder: field.placeholder,
|
||||||
help: field.help,
|
help: field.help,
|
||||||
helpPosition: (field.help_position) ? field.help_position : 'below_input',
|
helpPosition: (field.help_position) ? field.help_position : 'below_input',
|
||||||
uppercaseLabels: this.form.uppercase_labels,
|
uppercaseLabels: this.form.uppercase_labels == 1 || this.form.uppercase_labels == true,
|
||||||
theme: this.theme,
|
theme: this.theme,
|
||||||
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000,
|
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000,
|
||||||
showCharLimit: field.show_char_limit || false
|
showCharLimit: field.show_char_limit || false
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
||||||
checkbox: 'CheckboxInput',
|
checkbox: 'CheckboxInput',
|
||||||
url: 'TextInput',
|
url: 'TextInput',
|
||||||
email: 'TextInput',
|
email: 'TextInput',
|
||||||
phone_number: 'TextInput',
|
phone_number: 'PhoneInput'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -44,7 +44,7 @@ class FormSubmissionDataFactory
|
||||||
$value = $this->faker->url();
|
$value = $this->faker->url();
|
||||||
break;
|
break;
|
||||||
case 'phone_number':
|
case 'phone_number':
|
||||||
$value = $this->faker->phoneNumber();
|
$value = '+1 ' .$this->faker->phoneNumber();
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
$value = $this->faker->date();
|
$value = $this->faker->date();
|
||||||
|
|
Loading…
Reference in New Issue