diff --git a/application/Espo/Classes/FieldValidators/PhoneType.php b/application/Espo/Classes/FieldValidators/PhoneType.php
index 493ef38d3b..76d831c849 100644
--- a/application/Espo/Classes/FieldValidators/PhoneType.php
+++ b/application/Espo/Classes/FieldValidators/PhoneType.php
@@ -29,25 +29,27 @@
namespace Espo\Classes\FieldValidators;
+use Brick\PhoneNumber\PhoneNumber;
+use Brick\PhoneNumber\PhoneNumberParseException;
+use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use stdClass;
+/**
+ * @noinspection PhpUnused
+ */
class PhoneType
{
- private Metadata $metadata;
-
- private Defs $defs;
-
private const DEFAULT_MAX_LENGTH = 36;
- public function __construct(Metadata $metadata, Defs $defs)
- {
- $this->metadata = $metadata;
- $this->defs = $defs;
- }
+ public function __construct(
+ private Metadata $metadata,
+ private Defs $defs,
+ private Config $config
+ ) {}
public function checkRequired(Entity $entity, string $field): bool
{
@@ -193,7 +195,22 @@ class PhoneType
$preparedPattern = '/^' . $pattern . '$/';
- return (bool) preg_match($preparedPattern, $number);
+ if (!preg_match($preparedPattern, $number)) {
+ return false;
+ }
+
+ if (!$this->config->get('phoneNumberInternational')) {
+ return true;
+ }
+
+ try {
+ $numberObj = PhoneNumber::parse($number);
+ }
+ catch (PhoneNumberParseException) {
+ return false;
+ }
+
+ return $numberObj->isPossibleNumber();
}
protected function isNotEmpty(Entity $entity, string $field): bool
diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php
index 19e11c93d5..cd4d41399d 100644
--- a/application/Espo/Resources/defaults/config.php
+++ b/application/Espo/Resources/defaults/config.php
@@ -279,6 +279,8 @@ return [
'oidcScopes' => ['profile', 'email', 'phone'],
'listViewSettingsDisabled' => false,
'cleanupDeletedRecords' => true,
- 'phoneNumberNumericSearch' => false,
+ 'phoneNumberNumericSearch' => true,
+ 'phoneNumberInternational' => true,
+ 'phoneNumberPreferredCountryList' => ['us', 'de'],
'isInstalled' => false,
];
diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json
index 35988853ac..5986c3b5a9 100644
--- a/application/Espo/Resources/i18n/en_US/Global.json
+++ b/application/Espo/Resources/i18n/en_US/Global.json
@@ -305,6 +305,10 @@
"fieldNotMatchingPattern$uriOptionalProtocol": "{field} must be a valid URL",
"fieldInvalid": "{field} is invalid",
"fieldIsRequired": "{field} is required",
+ "fieldPhoneInvalid": "{field} is invalid",
+ "fieldPhoneInvalidCode": "Invalid country code",
+ "fieldPhoneTooShort": "{field} is too short",
+ "fieldPhoneTooLong": "{field} is too long",
"fieldPhoneInvalidCharacters": "Only digits, latin letters and characters `-+_@:#().` are allowed",
"fieldShouldBeEmail": "{field} should be a valid email",
"fieldShouldBeFloat": "{field} should be a valid float",
diff --git a/application/Espo/Resources/i18n/en_US/Import.json b/application/Espo/Resources/i18n/en_US/Import.json
index 96c5fc0c90..75a56003c9 100644
--- a/application/Espo/Resources/i18n/en_US/Import.json
+++ b/application/Espo/Resources/i18n/en_US/Import.json
@@ -71,6 +71,9 @@
"confirmRemoveImportLog" : "This will remove the import log. All imported records will be kept. You won't be able to revert import results. Are you sure?",
"removeImportLog": "This will remove the import log. All imported records will be kept. Use it if you are sure that import is fine."
},
+ "params": {
+ "phoneNumberCountry": "Telephone country code"
+ },
"fields": {
"file": "File",
"entityType": "Entity Type",
diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json
index da7f21e63b..f41d66d4ea 100644
--- a/application/Espo/Resources/i18n/en_US/Settings.json
+++ b/application/Espo/Resources/i18n/en_US/Settings.json
@@ -110,6 +110,8 @@
"adminNotificationsNewExtensionVersion": "Show notification when new versions of extensions are available",
"textFilterUseContainsForVarchar": "Use 'contains' operator when filtering varchar fields",
"phoneNumberNumericSearch": "Numeric phone number search",
+ "phoneNumberInternational": "International phone numbers",
+ "phoneNumberPreferredCountryList": "Preferred telephone country codes",
"authTokenPreventConcurrent": "Only one auth token per user",
"scopeColorsDisabled": "Disable scope colors",
"tabColorsDisabled": "Disable tab colors",
@@ -282,6 +284,7 @@
"Misc": "Misc",
"SMTP": "SMTP",
"General": "General",
+ "Phone Numbers": "Phone Numbers",
"Navbar": "Navbar",
"Dashboard": "Dashboard",
"Configuration": "Configuration",
diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json
index e8188f5972..53d6e4151d 100644
--- a/application/Espo/Resources/layouts/Settings/settings.json
+++ b/application/Espo/Resources/layouts/Settings/settings.json
@@ -10,8 +10,7 @@
{
"label": "Search",
"rows": [
- [{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}],
- [{"name": "phoneNumberNumericSearch"}, false]
+ [{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}]
]
},
{
@@ -36,6 +35,13 @@
[{"name": "addressStateList"}, false]
]
},
+ {
+ "label": "Phone Numbers",
+ "rows": [
+ [{"name": "phoneNumberInternational"}, {"name": "phoneNumberPreferredCountryList"}],
+ [{"name": "phoneNumberNumericSearch"}, false]
+ ]
+ },
{
"label": "Activities",
"rows": [
diff --git a/application/Espo/Resources/metadata/app/jsLibs.json b/application/Espo/Resources/metadata/app/jsLibs.json
index 3cfdf198d1..9a2fb82ceb 100644
--- a/application/Espo/Resources/metadata/app/jsLibs.json
+++ b/application/Espo/Resources/metadata/app/jsLibs.json
@@ -78,6 +78,18 @@
"exportsAs": "Selectize"
},
"autonumeric": {},
+ "intl-tel-input": {
+ "exportsTo": "window",
+ "exportsAs": "intlTelInput"
+ },
+ "intl-tel-input-utils": {
+ "exportsTo": "window",
+ "exportsAs": "intlTelInputUtils"
+ },
+ "intl-tel-input-globals": {
+ "exportsTo": "window",
+ "exportsAs": "intlTelInputGlobals"
+ },
"cronstrue": {
"path": "client/lib/cronstrue-i18n.js",
"devPath": "client/lib/original/cronstrue-i18n.js",
diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json
index 4371445207..a091576418 100644
--- a/application/Espo/Resources/metadata/entityDefs/Settings.json
+++ b/application/Espo/Resources/metadata/entityDefs/Settings.json
@@ -691,6 +691,13 @@
"phoneNumberNumericSearch": {
"type": "bool"
},
+ "phoneNumberInternational": {
+ "type": "bool"
+ },
+ "phoneNumberPreferredCountryList": {
+ "type": "multiEnum",
+ "view": "views/settings/fields/phone-number-preferred-country-list"
+ },
"scopeColorsDisabled": {
"type": "bool"
},
diff --git a/application/Espo/Tools/Import/Import.php b/application/Espo/Tools/Import/Import.php
index bfd9e86b8f..362840fe01 100644
--- a/application/Espo/Tools/Import/Import.php
+++ b/application/Espo/Tools/Import/Import.php
@@ -29,6 +29,8 @@
namespace Espo\Tools\Import;
+use Brick\PhoneNumber\PhoneNumber;
+use Brick\PhoneNumber\PhoneNumberParseException;
use Espo\Core\FieldValidation\Exceptions\ValidationError;
use Espo\Core\Job\JobSchedulerFactory;
use Espo\Entities\Attachment;
@@ -770,7 +772,7 @@ class Import
}
$o = (object) [
- 'phoneNumber' => $value,
+ 'phoneNumber' => $this->formatPhoneNumber($value, $params),
'primary' => true,
];
@@ -867,7 +869,7 @@ class Import
}
$o = (object) [
- 'phoneNumber' => $value,
+ 'phoneNumber' => $this->formatPhoneNumber($value, $params),
'type' => $type,
'primary' => $isPrimary,
];
@@ -917,7 +919,6 @@ class Import
{
$params = $this->params;
- /** @noinspection PhpRedundantVariableDocTypeInspection */
/** @var non-empty-string $decimalMark */
$decimalMark = $params->getDecimalMark() ?? self::DEFAULT_DECIMAL_MARK;
@@ -1255,4 +1256,26 @@ class Import
$errorIndex++;
}
+
+ private function formatPhoneNumber(string $value, Params $params): string
+ {
+ if (str_starts_with($value, '+')) {
+ return $value;
+ }
+
+ if (!$params->getPhoneNumberCountry()) {
+ return $value;
+ }
+
+ $code = strtoupper($params->getPhoneNumberCountry());
+
+ try {
+ $number = PhoneNumber::parse($value, $code);
+
+ return (string) $number;
+ }
+ catch (PhoneNumberParseException) {
+ return $value;
+ }
+ }
}
diff --git a/application/Espo/Tools/Import/Params.php b/application/Espo/Tools/Import/Params.php
index c11f4a0b8a..1a7c549461 100644
--- a/application/Espo/Tools/Import/Params.php
+++ b/application/Espo/Tools/Import/Params.php
@@ -62,6 +62,7 @@ class Params
private ?string $currency = null;
private ?string $timezone = null;
private ?string $decimalMark = null;
+ private ?string $phoneNumberCountry = null;
private function __construct()
{
@@ -88,6 +89,11 @@ class Params
return $this->personNameFormat;
}
+ public function getPhoneNumberCountry(): ?string
+ {
+ return $this->phoneNumberCountry;
+ }
+
public function isIdleMode(): bool
{
return $this->idleMode;
@@ -193,6 +199,14 @@ class Params
return $obj;
}
+ public function withPhoneNumberCountry(?string $phoneNumberCountry): self
+ {
+ $obj = clone $this;
+ $obj->phoneNumberCountry = $phoneNumberCountry;
+
+ return $obj;
+ }
+
public function withIdleMode(bool $idleMode = true): self
{
$obj = clone $this;
@@ -339,6 +353,7 @@ class Params
->withIdleMode($raw->idleMode ?? false)
->withManualMode($raw->manualMode ?? false)
->withPersonNameFormat($raw->personNameFormat ?? null)
+ ->withPhoneNumberCountry($raw->phoneNumberCountry ?? null)
->withSilentMode($raw->silentMode ?? false)
->withSkipDuplicateChecking($raw->skipDuplicateChecking ?? false)
->withStartFromLastIndex($raw->startFromLastIndex ?? false)
@@ -368,6 +383,7 @@ class Params
'skipDuplicateChecking' => $this->skipDuplicateChecking,
'startFromLastIndex' => $this->startFromLastIndex,
'textQualifier' => $this->textQualifier,
+ 'phoneNumberCountry' => $this->phoneNumberCountry,
'timeFormat' => $this->timeFormat,
'timezone' => $this->timezone,
'updateBy' => $this->updateBy,
diff --git a/client/res/templates/import/step-1.tpl b/client/res/templates/import/step-1.tpl
index bb02dae757..4c784a361c 100644
--- a/client/res/templates/import/step-1.tpl
+++ b/client/res/templates/import/step-1.tpl
@@ -109,6 +109,10 @@
diff --git a/client/src/views/fields/phone.js b/client/src/views/fields/phone.js
index 695bed8196..1e60c3399a 100644
--- a/client/src/views/fields/phone.js
+++ b/client/src/views/fields/phone.js
@@ -28,6 +28,11 @@
import VarcharFieldView from 'views/fields/varchar';
import Select from 'ui/select';
+import intlTelInput from 'intl-tel-input';
+// noinspection NpmUsedModulesInstalled
+import intlTelInputUtils from 'intl-tel-input-utils';
+// noinspection NpmUsedModulesInstalled
+import intlTelInputGlobals from 'intl-tel-input-globals';
class PhoneFieldView extends VarcharFieldView {
@@ -176,20 +181,47 @@ class PhoneFieldView extends VarcharFieldView {
.replace('{field}', this.getLabelText());
const number = row.phoneNumber;
+ const n = (i + 1).toString();
+ const selector = `div.phone-number-block:nth-child(${n}) input.phone-number`;
+
if (!regExp.test(number)) {
notValid = true;
const msg = this.translate('fieldPhoneInvalidCharacters', 'messages')
.replace('{field}', this.getLabelText());
- this.showValidationMessage(msg, 'div.phone-number-block:nth-child(' + (i + 1)
- .toString() + ') input.phone-number');
+ this.showValidationMessage(msg, selector);
+ }
+
+ if (this.useInternational) {
+ const element = this.$el.find(selector).get(0);
+
+ if (element) {
+ const obj = this.intlTelInputMap.get(element);
+
+ if (obj && !obj.isPossibleNumber()) {
+ notValid = true;
+
+ const code = obj.getValidationError();
+
+ const key = [
+ 'fieldPhoneInvalid',
+ 'fieldPhoneInvalidCode',
+ 'fieldPhoneTooShort',
+ 'fieldPhoneTooLong',
+ ][code || 0] || 'fieldPhoneInvalid';
+
+ const msg = this.translate(key, 'messages')
+ .replace('{field}', this.getLabelText());
+
+ this.showValidationMessage(msg, selector);
+ }
+ }
}
const numberClean = String(number).replace(/[\s+]/g, '');
if (~numberList.indexOf(numberClean)) {
-
this.showValidationMessage(msg, 'div.phone-number-block:nth-child(' + (i + 1)
.toString() + ') input.phone-number');
@@ -239,13 +271,17 @@ class PhoneFieldView extends VarcharFieldView {
if (phoneNumberData) {
phoneNumberData = Espo.Utils.cloneDeep(phoneNumberData);
- phoneNumberData.forEach((item) => {
+ phoneNumberData.forEach(item => {
const number = item.phoneNumber || '';
item.erased = number.indexOf(this.erasedPlaceholder) === 0;
if (!item.erased) {
item.valueForLink = number.replace(/ /g, '');
+
+ if (this.isReadMode()) {
+ item.phoneNumber = this.formatNumber(item.phoneNumber);
+ }
}
item.lineThrough = item.optOut || item.invalid || this.model.get('doNotCall');
@@ -253,13 +289,16 @@ class PhoneFieldView extends VarcharFieldView {
}
if ((!phoneNumberData || phoneNumberData.length === 0) && this.model.get(this.name)) {
-
const o = {
- phoneNumber: number,
+ phoneNumber: this.formatNumber(number),
primary: true,
valueForLink: number.replace(/ /g, ''),
};
+ if (this.isReadMode()) {
+ o.phoneNumber = this.formatNumber(o.phoneNumber);
+ }
+
if (this.mode === 'edit' && this.model.isNew()) {
o.type = this.defaultType;
}
@@ -267,6 +306,8 @@ class PhoneFieldView extends VarcharFieldView {
phoneNumberData = [o];
}
+
+
const data = {
...super.data(),
phoneNumberData: phoneNumberData,
@@ -287,6 +328,7 @@ class PhoneFieldView extends VarcharFieldView {
}
data.valueIsSet = this.model.has(this.name);
+ data.value = this.formatNumber(data.value);
}
data.itemMaxLength = this.itemMaxLength;
@@ -314,6 +356,19 @@ class PhoneFieldView extends VarcharFieldView {
this.trigger('change');
}
+ formatNumber(value) {
+ if (!value || value === '' || !this.useInternational) {
+ return value;
+ }
+
+ // noinspection JSUnresolvedReference
+ return intlTelInputUtils.formatNumber(
+ value,
+ null,
+ intlTelInputUtils.numberFormat.INTERNATIONAL
+ );
+ }
+
addPhoneNumber() {
const data = Espo.Utils.cloneDeep(this.fetchPhoneNumberData());
@@ -346,6 +401,25 @@ class PhoneFieldView extends VarcharFieldView {
}
}
+ afterRenderEdit() {
+ super.afterRenderEdit();
+
+ if (this.useInternational) {
+ const inputElements = this.element.querySelectorAll('input.phone-number');
+
+ inputElements.forEach(inputElement => {
+ const obj = intlTelInput(inputElement, {
+ separateDialCode: true,
+ showFlags: false,
+ preferredCountries: this.preferredCountryList,
+ localizedCountries: this._codeNames,
+ });
+
+ this.intlTelInputMap.set(inputElement, obj);
+ });
+ }
+ }
+
afterRenderSearch() {
super.afterRenderSearch();
@@ -424,6 +498,17 @@ class PhoneFieldView extends VarcharFieldView {
this.isInvalidFieldName = this.name + 'IsInvalid';
this.phoneNumberOptedOutByDefault = this.getConfig().get('phoneNumberIsOptedOutByDefault');
+ this.useInternational = this.getConfig().get('phoneNumberInternational') || false;
+ this.preferredCountryList = this.getConfig().get('phoneNumberPreferredCountryList') || [];
+
+ if (this.useInternational && !this.isListMode() && !this.isSearchMode()) {
+ this._codeNames = intlTelInputGlobals.getCountryData()
+ .reduce((map, item) => {
+ map[item.iso2] = item.iso2.toUpperCase();
+
+ return map;
+ }, {});
+ }
if (this.model.has('doNotCall')) {
this.listenTo(this.model, 'change:doNotCall', (model, value, o) => {
@@ -443,6 +528,16 @@ class PhoneFieldView extends VarcharFieldView {
this.itemMaxLength = this.getMetadata()
.get(['entityDefs', 'PhoneNumber', 'fields', 'name', 'maxLength']);
+
+ this.intlTelInputMap = new Map();
+
+ this.once('remove', () => {
+ for (const obj of this.intlTelInputMap.values()) {
+ obj.destroy();
+ }
+
+ this.intlTelInputMap.clear();
+ })
}
fetchPhoneNumberData() {
@@ -455,7 +550,18 @@ class PhoneFieldView extends VarcharFieldView {
const row = {};
const $d = $(d);
- row.phoneNumber = $d.find('input.phone-number').val().trim();
+ /** @type {HTMLInputElement} */
+ const inputElement = $d.find('input.phone-number').get(0);
+
+ if (!inputElement) {
+ return;
+ }
+
+ row.phoneNumber = inputElement.value.trim();
+
+ if (this.intlTelInputMap.has(inputElement)) {
+ row.phoneNumber = this.intlTelInputMap.get(inputElement).getNumber();
+ }
if (row.phoneNumber === '') {
return;
diff --git a/client/src/views/import/step1.js b/client/src/views/import/step1.js
index 4e57d28a7f..21f93de7c3 100644
--- a/client/src/views/import/step1.js
+++ b/client/src/views/import/step1.js
@@ -28,6 +28,8 @@
import View from 'view';
import Model from 'model';
+// noinspection NpmUsedModulesInstalled
+import intlTelInputGlobals from 'intl-tel-input-globals';
class Step1ImportView extends View {
@@ -100,6 +102,7 @@ class Step1ImportView extends View {
'idleMode',
'skipDuplicateChecking',
'manualMode',
+ 'phoneNumberCountry',
];
this.paramList.forEach(item => {
@@ -355,6 +358,22 @@ class Step1ImportView extends View {
tooltipText: this.translate('manualMode', 'tooltips', 'Import'),
});
+ this.createView('phoneNumberCountryField', 'views/fields/enum', {
+ selector: '.field[data-name="phoneNumberCountry"]',
+ model: this.model,
+ name: 'phoneNumberCountry',
+ mode: 'edit',
+ params: {
+ options: ['', ...intlTelInputGlobals.getCountryData().map(item => item.iso2)],
+ },
+ translatedOptions: intlTelInputGlobals.getCountryData()
+ .reduce((map, item) => {
+ map[item.iso2] = `${item.iso2.toUpperCase()} +${item.dialCode}`;
+
+ return map;
+ }, {})
+ });
+
this.listenTo(this.model, 'change', (m, o) => {
if (!o.ui) {
return;
diff --git a/client/src/views/settings/fields/phone-number-preferred-country-list.js b/client/src/views/settings/fields/phone-number-preferred-country-list.js
new file mode 100644
index 0000000000..ab0a6cdf32
--- /dev/null
+++ b/client/src/views/settings/fields/phone-number-preferred-country-list.js
@@ -0,0 +1,55 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM - Open Source CRM application.
+ * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
+ * Website: https://www.espocrm.com
+ *
+ * EspoCRM is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * EspoCRM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of this program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU General Public License version 3.
+ *
+ * In accordance with Section 7(b) of the GNU General Public License version 3,
+ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
+ ************************************************************************/
+
+import MultiEnumFieldView from 'views/fields/multi-enum';
+// noinspection NpmUsedModulesInstalled
+import intlTelInputGlobals from 'intl-tel-input-globals';
+
+class SettingsPhoneNumberPreferredCountryListFieldView extends MultiEnumFieldView {
+
+ setupOptions() {
+ try {
+ const dataList = intlTelInputGlobals.getCountryData();
+
+ this.params.options = dataList
+ .map(item => item.iso2);
+
+ this.translatedOptions = dataList.reduce((map, item) => {
+ map[item.iso2] = `${item.iso2.toUpperCase()} +${item.dialCode}`;
+
+ return map;
+ }, {});
+ }
+ catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default SettingsPhoneNumberPreferredCountryListFieldView;
diff --git a/composer.json b/composer.json
index 2deb83b455..458703346d 100644
--- a/composer.json
+++ b/composer.json
@@ -47,7 +47,8 @@
"johngrogg/ics-parser": "^3.0",
"phpseclib/phpseclib": "^3.0",
"openspout/openspout": "^4.9",
- "dompdf/dompdf": "^2.0"
+ "dompdf/dompdf": "^2.0",
+ "brick/phonenumber": "^0.5.0"
},
"require-dev": {
"phpunit/phpunit": "^9",
diff --git a/composer.lock b/composer.lock
index 4de61d6e37..484d2907c2 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2b80267e733b9b4153f66fc0cfce5315",
+ "content-hash": "d00cfc7a66d5f0fc733141d48f9eca16",
"packages": [
{
"name": "async-aws/core",
@@ -133,6 +133,59 @@
],
"time": "2021-02-01T21:10:42+00:00"
},
+ {
+ "name": "brick/phonenumber",
+ "version": "0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/phonenumber.git",
+ "reference": "6286a218375a46a6f4d0fd361323f22313882814"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/phonenumber/zipball/6286a218375a46a6f4d0fd361323f22313882814",
+ "reference": "6286a218375a46a6f4d0fd361323f22313882814",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "giggsey/libphonenumber-for-php": "^7.0 || ^8.0",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0",
+ "vimeo/psalm": "5.7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\PhoneNumber\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Phone number library",
+ "keywords": [
+ "brick",
+ "phone",
+ "phone number",
+ "phonenumber"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/phonenumber/issues",
+ "source": "https://github.com/brick/phonenumber/tree/0.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-23T21:41:31+00:00"
+ },
{
"name": "cboden/ratchet",
"version": "v0.4.4",
@@ -901,6 +954,132 @@
},
"time": "2020-11-24T22:02:12+00:00"
},
+ {
+ "name": "giggsey/libphonenumber-for-php",
+ "version": "8.13.24",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/libphonenumber-for-php.git",
+ "reference": "746ca6a565b9d4167c94c80824f43fa6fb463fd1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/746ca6a565b9d4167c94c80824f43fa6fb463fd1",
+ "reference": "746ca6a565b9d4167c94c80824f43fa6fb463fd1",
+ "shasum": ""
+ },
+ "require": {
+ "giggsey/locale": "^1.7|^2.0",
+ "php": ">=5.3.2",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "require-dev": {
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "^2.7",
+ "php-coveralls/php-coveralls": "^1.0|^2.0",
+ "symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "libphonenumber\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "/src/data/",
+ "/src/carrier/data/",
+ "/src/geocoding/data/",
+ "/src/timezone/data/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "https://giggsey.com/"
+ }
+ ],
+ "description": "PHP Port of Google's libphonenumber",
+ "homepage": "https://github.com/giggsey/libphonenumber-for-php",
+ "keywords": [
+ "geocoding",
+ "geolocation",
+ "libphonenumber",
+ "mobile",
+ "phonenumber",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
+ "source": "https://github.com/giggsey/libphonenumber-for-php"
+ },
+ "time": "2023-10-31T08:12:54+00:00"
+ },
+ {
+ "name": "giggsey/locale",
+ "version": "2.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/Locale.git",
+ "reference": "e6d4540109a01dd2bc7334cdc842d6a6a67cf239"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/Locale/zipball/e6d4540109a01dd2bc7334cdc842d6a6a67cf239",
+ "reference": "e6d4540109a01dd2bc7334cdc842d6a6a67cf239",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "ext-json": "*",
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "^2.7",
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^8.5|^9.5",
+ "symfony/console": "^5.0|^6.0",
+ "symfony/filesystem": "^5.0|^6.0",
+ "symfony/finder": "^5.0|^6.0",
+ "symfony/process": "^5.0|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Giggsey\\Locale\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "https://giggsey.com/"
+ }
+ ],
+ "description": "Locale functions required by libphonenumber-for-php",
+ "support": {
+ "issues": "https://github.com/giggsey/Locale/issues",
+ "source": "https://github.com/giggsey/Locale/tree/2.5"
+ },
+ "time": "2023-11-01T17:19:48+00:00"
+ },
{
"name": "guzzlehttp/psr7",
"version": "1.9.1",
diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less
index 6ded648fe3..06fe79023c 100644
--- a/frontend/less/espo/custom.less
+++ b/frontend/less/espo/custom.less
@@ -43,7 +43,8 @@ button.btn.active:focus-visible,
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible,
input[type=file]:focus-visible,
-label.attach-file-label:focus-visible {
+label.attach-file-label:focus-visible,
+.iti__selected-flag:focus-visible {
outline: 1px solid var(--input-border-focus-rgba);
outline-offset: -1px;
}
@@ -3536,6 +3537,88 @@ body > .autocomplete-suggestions.text-search-suggestions {
}
}
+.input-group-item > .iti {
+ top: 3px;
+ margin-left: -1px;
+ width: ~"calc(100% + 1px)";
+
+ .iti__flag-container {
+ user-select: none;
+
+ &,
+ &:hover {
+ .iti__selected-flag {
+ background-color: transparent;
+
+ &[aria-expanded="true"] {
+ background-color: var(--dropdown-link-hover-bg);
+ }
+ }
+ }
+
+ .iti__country-list {
+ z-index: 5;
+ max-height: 300px;
+ background-color: var(--dropdown-bg);
+ border: var(--dropdown-border) solid var(--dropdown-border-width);
+ border-radius: var(--dropdown-border-radius);
+ box-shadow: var(--dropdown-box-shadow);
+
+ .iti__dial-code {
+ color: var(--text-muted-color);
+ }
+
+ .iti__country {
+ color: var(--dropdown-link-color);
+ padding: 4px 10px;
+
+ &.iti__highlight {
+ background-color: var(--dropdown-link-hover-bg);
+ }
+ }
+
+ .iti__divider {
+ border-bottom-color: var(--dropdown-divider-bg);
+ }
+
+ padding: 5px 0;
+ }
+ }
+
+ .iti__selected-flag {
+ z-index: 4;
+ padding: 0 8px 0 8px;
+ color: var(--gray-soft);
+
+ > .iti__arrow {
+ visibility: hidden;
+ display: none;
+ }
+
+ &:after {
+ content: " ";
+ color: var(--text-muted-color);
+ //display: inline-block;
+ display: none;
+ width: 0;
+ height: 0;
+ top: 15px;
+ right: 8px;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: 4px dashed;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+ position: absolute;
+ }
+ }
+
+ > input {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+}
+
@import "misc/kanban.less";
@import "elements/placement.less";
diff --git a/frontend/less/espo/misc.less b/frontend/less/espo/misc.less
index b9502923a3..cac2695edb 100644
--- a/frontend/less/espo/misc.less
+++ b/frontend/less/espo/misc.less
@@ -10,5 +10,6 @@
@import "misc/colorpicker/colorpicker.less";
@import "misc/fontawesome/fontawesome.less";
@import "misc/gridstack/gridstack.less";
+@import "misc/intl-tel-input/intlTelInput.less";
@import "fonts.less";
diff --git a/frontend/less/espo/misc/intl-tel-input/intlTelInput.less b/frontend/less/espo/misc/intl-tel-input/intlTelInput.less
new file mode 100644
index 0000000000..47e76aefd3
--- /dev/null
+++ b/frontend/less/espo/misc/intl-tel-input/intlTelInput.less
@@ -0,0 +1,1278 @@
+.iti {
+ position: relative;
+ display: inline-block;
+}
+.iti * {
+ box-sizing: border-box;
+}
+.iti__hide {
+ display: none;
+}
+.iti__v-hide {
+ visibility: hidden;
+}
+.iti input,
+.iti input[type=text],
+.iti input[type=tel] {
+ position: relative;
+ z-index: 0;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ padding-right: 36px;
+ margin-right: 0;
+}
+.iti__flag-container {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ padding: 1px;
+}
+.iti__selected-flag {
+ z-index: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 100%;
+ padding: 0 6px 0 8px;
+}
+.iti__arrow {
+ margin-left: 6px;
+ width: 0;
+ height: 0;
+ border-left: 3px solid transparent;
+ border-right: 3px solid transparent;
+ border-top: 4px solid #555;
+}
+[dir=rtl] .iti__arrow {
+ margin-right: 6px;
+ margin-left: 0;
+}
+.iti__arrow--up {
+ border-top: none;
+ border-bottom: 4px solid #555;
+}
+.iti__country-list {
+ position: absolute;
+ z-index: 2;
+ list-style: none;
+ padding: 0;
+ margin: 0 0 0 -1px;
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2);
+ background-color: white;
+ border: 1px solid #ccc;
+ white-space: nowrap;
+ max-height: 200px;
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+}
+.iti__country-list--dropup {
+ bottom: 100%;
+ margin-bottom: -1px;
+}
+@media (max-width: 500px) {
+ .iti__country-list {
+ white-space: normal;
+ }
+}
+.iti__flag-box {
+ display: inline-block;
+ width: 20px;
+}
+.iti__divider {
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+ border-bottom: 1px solid #ccc;
+}
+.iti__country {
+ display: flex;
+ align-items: center;
+ padding: 5px 10px;
+ outline: none;
+}
+.iti__dial-code {
+ color: #999;
+}
+.iti__country.iti__highlight {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+.iti__flag-box, .iti__country-name {
+ margin-right: 6px;
+}
+[dir=rtl] .iti__flag-box, [dir=rtl] .iti__country-name {
+ margin-right: 0;
+ margin-left: 6px;
+}
+.iti--allow-dropdown input,
+.iti--allow-dropdown input[type=text],
+.iti--allow-dropdown input[type=tel], .iti--separate-dial-code input,
+.iti--separate-dial-code input[type=text],
+.iti--separate-dial-code input[type=tel] {
+ padding-right: 6px;
+ padding-left: 52px;
+ margin-left: 0;
+}
+[dir=rtl] .iti--allow-dropdown input,
+[dir=rtl] .iti--allow-dropdown input[type=text],
+[dir=rtl] .iti--allow-dropdown input[type=tel], [dir=rtl] .iti--separate-dial-code input,
+[dir=rtl] .iti--separate-dial-code input[type=text],
+[dir=rtl] .iti--separate-dial-code input[type=tel] {
+ padding-right: 52px;
+ padding-left: 6px;
+ margin-right: 0;
+}
+.iti--allow-dropdown .iti__flag-container, .iti--separate-dial-code .iti__flag-container {
+ right: auto;
+ left: 0;
+}
+[dir=rtl] .iti--allow-dropdown .iti__flag-container, [dir=rtl] .iti--separate-dial-code .iti__flag-container {
+ right: 0;
+ left: auto;
+}
+.iti--allow-dropdown .iti__flag-container:hover {
+ cursor: pointer;
+}
+.iti--allow-dropdown .iti__flag-container:hover .iti__selected-flag {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+.iti--allow-dropdown input[disabled] + .iti__flag-container:hover,
+.iti--allow-dropdown input[readonly] + .iti__flag-container:hover {
+ cursor: default;
+}
+.iti--allow-dropdown input[disabled] + .iti__flag-container:hover .iti__selected-flag,
+.iti--allow-dropdown input[readonly] + .iti__flag-container:hover .iti__selected-flag {
+ background-color: transparent;
+}
+.iti--separate-dial-code .iti__selected-flag {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+.iti--separate-dial-code.iti--show-flags .iti__selected-dial-code {
+ margin-left: 6px;
+}
+[dir=rtl] .iti--separate-dial-code.iti--show-flags .iti__selected-dial-code {
+ margin-left: 0;
+ margin-right: 6px;
+}
+.iti--container {
+ position: absolute;
+ top: -1000px;
+ left: -1000px;
+ z-index: 1060;
+ padding: 1px;
+}
+.iti--container:hover {
+ cursor: pointer;
+}
+
+.iti-mobile .iti--container {
+ top: 30px;
+ bottom: 30px;
+ left: 30px;
+ right: 30px;
+ position: fixed;
+}
+.iti-mobile .iti__country-list {
+ max-height: 100%;
+ width: 100%;
+}
+.iti-mobile .iti__country {
+ padding: 10px 10px;
+ line-height: 1.5em;
+}
+
+.iti__flag {
+ width: 20px;
+}
+.iti__flag.iti__be {
+ width: 18px;
+}
+.iti__flag.iti__ch {
+ width: 15px;
+}
+.iti__flag.iti__mc {
+ width: 19px;
+}
+.iti__flag.iti__ne {
+ width: 18px;
+}
+.iti__flag.iti__np {
+ width: 13px;
+}
+.iti__flag.iti__va {
+ width: 15px;
+}
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .iti__flag {
+ background-size: 5762px 15px;
+ }
+}
+.iti__flag.iti__ac {
+ height: 10px;
+ background-position: 0px 0px;
+}
+.iti__flag.iti__ad {
+ height: 14px;
+ background-position: -22px 0px;
+}
+.iti__flag.iti__ae {
+ height: 10px;
+ background-position: -44px 0px;
+}
+.iti__flag.iti__af {
+ height: 14px;
+ background-position: -66px 0px;
+}
+.iti__flag.iti__ag {
+ height: 14px;
+ background-position: -88px 0px;
+}
+.iti__flag.iti__ai {
+ height: 10px;
+ background-position: -110px 0px;
+}
+.iti__flag.iti__al {
+ height: 15px;
+ background-position: -132px 0px;
+}
+.iti__flag.iti__am {
+ height: 10px;
+ background-position: -154px 0px;
+}
+.iti__flag.iti__ao {
+ height: 14px;
+ background-position: -176px 0px;
+}
+.iti__flag.iti__aq {
+ height: 14px;
+ background-position: -198px 0px;
+}
+.iti__flag.iti__ar {
+ height: 13px;
+ background-position: -220px 0px;
+}
+.iti__flag.iti__as {
+ height: 10px;
+ background-position: -242px 0px;
+}
+.iti__flag.iti__at {
+ height: 14px;
+ background-position: -264px 0px;
+}
+.iti__flag.iti__au {
+ height: 10px;
+ background-position: -286px 0px;
+}
+.iti__flag.iti__aw {
+ height: 14px;
+ background-position: -308px 0px;
+}
+.iti__flag.iti__ax {
+ height: 13px;
+ background-position: -330px 0px;
+}
+.iti__flag.iti__az {
+ height: 10px;
+ background-position: -352px 0px;
+}
+.iti__flag.iti__ba {
+ height: 10px;
+ background-position: -374px 0px;
+}
+.iti__flag.iti__bb {
+ height: 14px;
+ background-position: -396px 0px;
+}
+.iti__flag.iti__bd {
+ height: 12px;
+ background-position: -418px 0px;
+}
+.iti__flag.iti__be {
+ height: 15px;
+ background-position: -440px 0px;
+}
+.iti__flag.iti__bf {
+ height: 14px;
+ background-position: -460px 0px;
+}
+.iti__flag.iti__bg {
+ height: 12px;
+ background-position: -482px 0px;
+}
+.iti__flag.iti__bh {
+ height: 12px;
+ background-position: -504px 0px;
+}
+.iti__flag.iti__bi {
+ height: 12px;
+ background-position: -526px 0px;
+}
+.iti__flag.iti__bj {
+ height: 14px;
+ background-position: -548px 0px;
+}
+.iti__flag.iti__bl {
+ height: 14px;
+ background-position: -570px 0px;
+}
+.iti__flag.iti__bm {
+ height: 10px;
+ background-position: -592px 0px;
+}
+.iti__flag.iti__bn {
+ height: 10px;
+ background-position: -614px 0px;
+}
+.iti__flag.iti__bo {
+ height: 14px;
+ background-position: -636px 0px;
+}
+.iti__flag.iti__bq {
+ height: 14px;
+ background-position: -658px 0px;
+}
+.iti__flag.iti__br {
+ height: 14px;
+ background-position: -680px 0px;
+}
+.iti__flag.iti__bs {
+ height: 10px;
+ background-position: -702px 0px;
+}
+.iti__flag.iti__bt {
+ height: 14px;
+ background-position: -724px 0px;
+}
+.iti__flag.iti__bv {
+ height: 15px;
+ background-position: -746px 0px;
+}
+.iti__flag.iti__bw {
+ height: 14px;
+ background-position: -768px 0px;
+}
+.iti__flag.iti__by {
+ height: 10px;
+ background-position: -790px 0px;
+}
+.iti__flag.iti__bz {
+ height: 12px;
+ background-position: -812px 0px;
+}
+.iti__flag.iti__ca {
+ height: 10px;
+ background-position: -834px 0px;
+}
+.iti__flag.iti__cc {
+ height: 10px;
+ background-position: -856px 0px;
+}
+.iti__flag.iti__cd {
+ height: 15px;
+ background-position: -878px 0px;
+}
+.iti__flag.iti__cf {
+ height: 14px;
+ background-position: -900px 0px;
+}
+.iti__flag.iti__cg {
+ height: 14px;
+ background-position: -922px 0px;
+}
+.iti__flag.iti__ch {
+ height: 15px;
+ background-position: -944px 0px;
+}
+.iti__flag.iti__ci {
+ height: 14px;
+ background-position: -961px 0px;
+}
+.iti__flag.iti__ck {
+ height: 10px;
+ background-position: -983px 0px;
+}
+.iti__flag.iti__cl {
+ height: 14px;
+ background-position: -1005px 0px;
+}
+.iti__flag.iti__cm {
+ height: 14px;
+ background-position: -1027px 0px;
+}
+.iti__flag.iti__cn {
+ height: 14px;
+ background-position: -1049px 0px;
+}
+.iti__flag.iti__co {
+ height: 14px;
+ background-position: -1071px 0px;
+}
+.iti__flag.iti__cp {
+ height: 14px;
+ background-position: -1093px 0px;
+}
+.iti__flag.iti__cq {
+ height: 12px;
+ background-position: -1115px 0px;
+}
+.iti__flag.iti__cr {
+ height: 12px;
+ background-position: -1137px 0px;
+}
+.iti__flag.iti__cu {
+ height: 10px;
+ background-position: -1159px 0px;
+}
+.iti__flag.iti__cv {
+ height: 12px;
+ background-position: -1181px 0px;
+}
+.iti__flag.iti__cw {
+ height: 14px;
+ background-position: -1203px 0px;
+}
+.iti__flag.iti__cx {
+ height: 10px;
+ background-position: -1225px 0px;
+}
+.iti__flag.iti__cy {
+ height: 14px;
+ background-position: -1247px 0px;
+}
+.iti__flag.iti__cz {
+ height: 14px;
+ background-position: -1269px 0px;
+}
+.iti__flag.iti__de {
+ height: 12px;
+ background-position: -1291px 0px;
+}
+.iti__flag.iti__dg {
+ height: 10px;
+ background-position: -1313px 0px;
+}
+.iti__flag.iti__dj {
+ height: 14px;
+ background-position: -1335px 0px;
+}
+.iti__flag.iti__dk {
+ height: 15px;
+ background-position: -1357px 0px;
+}
+.iti__flag.iti__dm {
+ height: 10px;
+ background-position: -1379px 0px;
+}
+.iti__flag.iti__do {
+ height: 14px;
+ background-position: -1401px 0px;
+}
+.iti__flag.iti__dz {
+ height: 14px;
+ background-position: -1423px 0px;
+}
+.iti__flag.iti__ea {
+ height: 14px;
+ background-position: -1445px 0px;
+}
+.iti__flag.iti__ec {
+ height: 14px;
+ background-position: -1467px 0px;
+}
+.iti__flag.iti__ee {
+ height: 13px;
+ background-position: -1489px 0px;
+}
+.iti__flag.iti__eg {
+ height: 14px;
+ background-position: -1511px 0px;
+}
+.iti__flag.iti__eh {
+ height: 10px;
+ background-position: -1533px 0px;
+}
+.iti__flag.iti__er {
+ height: 10px;
+ background-position: -1555px 0px;
+}
+.iti__flag.iti__es {
+ height: 14px;
+ background-position: -1577px 0px;
+}
+.iti__flag.iti__et {
+ height: 10px;
+ background-position: -1599px 0px;
+}
+.iti__flag.iti__eu {
+ height: 14px;
+ background-position: -1621px 0px;
+}
+.iti__flag.iti__ez {
+ height: 14px;
+ background-position: -1643px 0px;
+}
+.iti__flag.iti__fi {
+ height: 12px;
+ background-position: -1665px 0px;
+}
+.iti__flag.iti__fj {
+ height: 10px;
+ background-position: -1687px 0px;
+}
+.iti__flag.iti__fk {
+ height: 10px;
+ background-position: -1709px 0px;
+}
+.iti__flag.iti__fm {
+ height: 11px;
+ background-position: -1731px 0px;
+}
+.iti__flag.iti__fo {
+ height: 15px;
+ background-position: -1753px 0px;
+}
+.iti__flag.iti__fr {
+ height: 14px;
+ background-position: -1775px 0px;
+}
+.iti__flag.iti__fx {
+ height: 14px;
+ background-position: -1797px 0px;
+}
+.iti__flag.iti__ga {
+ height: 15px;
+ background-position: -1819px 0px;
+}
+.iti__flag.iti__gb {
+ height: 10px;
+ background-position: -1841px 0px;
+}
+.iti__flag.iti__gd {
+ height: 12px;
+ background-position: -1863px 0px;
+}
+.iti__flag.iti__ge {
+ height: 14px;
+ background-position: -1885px 0px;
+}
+.iti__flag.iti__gf {
+ height: 14px;
+ background-position: -1907px 0px;
+}
+.iti__flag.iti__gg {
+ height: 14px;
+ background-position: -1929px 0px;
+}
+.iti__flag.iti__gh {
+ height: 14px;
+ background-position: -1951px 0px;
+}
+.iti__flag.iti__gi {
+ height: 10px;
+ background-position: -1973px 0px;
+}
+.iti__flag.iti__gl {
+ height: 14px;
+ background-position: -1995px 0px;
+}
+.iti__flag.iti__gm {
+ height: 14px;
+ background-position: -2017px 0px;
+}
+.iti__flag.iti__gn {
+ height: 14px;
+ background-position: -2039px 0px;
+}
+.iti__flag.iti__gp {
+ height: 14px;
+ background-position: -2061px 0px;
+}
+.iti__flag.iti__gq {
+ height: 14px;
+ background-position: -2083px 0px;
+}
+.iti__flag.iti__gr {
+ height: 14px;
+ background-position: -2105px 0px;
+}
+.iti__flag.iti__gs {
+ height: 10px;
+ background-position: -2127px 0px;
+}
+.iti__flag.iti__gt {
+ height: 13px;
+ background-position: -2149px 0px;
+}
+.iti__flag.iti__gu {
+ height: 11px;
+ background-position: -2171px 0px;
+}
+.iti__flag.iti__gw {
+ height: 10px;
+ background-position: -2193px 0px;
+}
+.iti__flag.iti__gy {
+ height: 12px;
+ background-position: -2215px 0px;
+}
+.iti__flag.iti__hk {
+ height: 14px;
+ background-position: -2237px 0px;
+}
+.iti__flag.iti__hm {
+ height: 10px;
+ background-position: -2259px 0px;
+}
+.iti__flag.iti__hn {
+ height: 10px;
+ background-position: -2281px 0px;
+}
+.iti__flag.iti__hr {
+ height: 10px;
+ background-position: -2303px 0px;
+}
+.iti__flag.iti__ht {
+ height: 12px;
+ background-position: -2325px 0px;
+}
+.iti__flag.iti__hu {
+ height: 10px;
+ background-position: -2347px 0px;
+}
+.iti__flag.iti__ic {
+ height: 14px;
+ background-position: -2369px 0px;
+}
+.iti__flag.iti__id {
+ height: 14px;
+ background-position: -2391px 0px;
+}
+.iti__flag.iti__ie {
+ height: 10px;
+ background-position: -2413px 0px;
+}
+.iti__flag.iti__il {
+ height: 15px;
+ background-position: -2435px 0px;
+}
+.iti__flag.iti__im {
+ height: 10px;
+ background-position: -2457px 0px;
+}
+.iti__flag.iti__in {
+ height: 14px;
+ background-position: -2479px 0px;
+}
+.iti__flag.iti__io {
+ height: 10px;
+ background-position: -2501px 0px;
+}
+.iti__flag.iti__iq {
+ height: 14px;
+ background-position: -2523px 0px;
+}
+.iti__flag.iti__ir {
+ height: 12px;
+ background-position: -2545px 0px;
+}
+.iti__flag.iti__is {
+ height: 15px;
+ background-position: -2567px 0px;
+}
+.iti__flag.iti__it {
+ height: 14px;
+ background-position: -2589px 0px;
+}
+.iti__flag.iti__je {
+ height: 12px;
+ background-position: -2611px 0px;
+}
+.iti__flag.iti__jm {
+ height: 10px;
+ background-position: -2633px 0px;
+}
+.iti__flag.iti__jo {
+ height: 10px;
+ background-position: -2655px 0px;
+}
+.iti__flag.iti__jp {
+ height: 14px;
+ background-position: -2677px 0px;
+}
+.iti__flag.iti__ke {
+ height: 14px;
+ background-position: -2699px 0px;
+}
+.iti__flag.iti__kg {
+ height: 12px;
+ background-position: -2721px 0px;
+}
+.iti__flag.iti__kh {
+ height: 13px;
+ background-position: -2743px 0px;
+}
+.iti__flag.iti__ki {
+ height: 10px;
+ background-position: -2765px 0px;
+}
+.iti__flag.iti__km {
+ height: 12px;
+ background-position: -2787px 0px;
+}
+.iti__flag.iti__kn {
+ height: 14px;
+ background-position: -2809px 0px;
+}
+.iti__flag.iti__kp {
+ height: 10px;
+ background-position: -2831px 0px;
+}
+.iti__flag.iti__kr {
+ height: 14px;
+ background-position: -2853px 0px;
+}
+.iti__flag.iti__kw {
+ height: 10px;
+ background-position: -2875px 0px;
+}
+.iti__flag.iti__ky {
+ height: 10px;
+ background-position: -2897px 0px;
+}
+.iti__flag.iti__kz {
+ height: 10px;
+ background-position: -2919px 0px;
+}
+.iti__flag.iti__la {
+ height: 14px;
+ background-position: -2941px 0px;
+}
+.iti__flag.iti__lb {
+ height: 14px;
+ background-position: -2963px 0px;
+}
+.iti__flag.iti__lc {
+ height: 10px;
+ background-position: -2985px 0px;
+}
+.iti__flag.iti__li {
+ height: 12px;
+ background-position: -3007px 0px;
+}
+.iti__flag.iti__lk {
+ height: 10px;
+ background-position: -3029px 0px;
+}
+.iti__flag.iti__lr {
+ height: 11px;
+ background-position: -3051px 0px;
+}
+.iti__flag.iti__ls {
+ height: 14px;
+ background-position: -3073px 0px;
+}
+.iti__flag.iti__lt {
+ height: 12px;
+ background-position: -3095px 0px;
+}
+.iti__flag.iti__lu {
+ height: 12px;
+ background-position: -3117px 0px;
+}
+.iti__flag.iti__lv {
+ height: 10px;
+ background-position: -3139px 0px;
+}
+.iti__flag.iti__ly {
+ height: 10px;
+ background-position: -3161px 0px;
+}
+.iti__flag.iti__ma {
+ height: 14px;
+ background-position: -3183px 0px;
+}
+.iti__flag.iti__mc {
+ height: 15px;
+ background-position: -3205px 0px;
+}
+.iti__flag.iti__md {
+ height: 10px;
+ background-position: -3226px 0px;
+}
+.iti__flag.iti__me {
+ height: 10px;
+ background-position: -3248px 0px;
+}
+.iti__flag.iti__mf {
+ height: 14px;
+ background-position: -3270px 0px;
+}
+.iti__flag.iti__mg {
+ height: 14px;
+ background-position: -3292px 0px;
+}
+.iti__flag.iti__mh {
+ height: 11px;
+ background-position: -3314px 0px;
+}
+.iti__flag.iti__mk {
+ height: 10px;
+ background-position: -3336px 0px;
+}
+.iti__flag.iti__ml {
+ height: 14px;
+ background-position: -3358px 0px;
+}
+.iti__flag.iti__mm {
+ height: 14px;
+ background-position: -3380px 0px;
+}
+.iti__flag.iti__mn {
+ height: 10px;
+ background-position: -3402px 0px;
+}
+.iti__flag.iti__mo {
+ height: 14px;
+ background-position: -3424px 0px;
+}
+.iti__flag.iti__mp {
+ height: 10px;
+ background-position: -3446px 0px;
+}
+.iti__flag.iti__mq {
+ height: 14px;
+ background-position: -3468px 0px;
+}
+.iti__flag.iti__mr {
+ height: 14px;
+ background-position: -3490px 0px;
+}
+.iti__flag.iti__ms {
+ height: 10px;
+ background-position: -3512px 0px;
+}
+.iti__flag.iti__mt {
+ height: 14px;
+ background-position: -3534px 0px;
+}
+.iti__flag.iti__mu {
+ height: 14px;
+ background-position: -3556px 0px;
+}
+.iti__flag.iti__mv {
+ height: 14px;
+ background-position: -3578px 0px;
+}
+.iti__flag.iti__mw {
+ height: 14px;
+ background-position: -3600px 0px;
+}
+.iti__flag.iti__mx {
+ height: 12px;
+ background-position: -3622px 0px;
+}
+.iti__flag.iti__my {
+ height: 10px;
+ background-position: -3644px 0px;
+}
+.iti__flag.iti__mz {
+ height: 14px;
+ background-position: -3666px 0px;
+}
+.iti__flag.iti__na {
+ height: 14px;
+ background-position: -3688px 0px;
+}
+.iti__flag.iti__nc {
+ height: 10px;
+ background-position: -3710px 0px;
+}
+.iti__flag.iti__ne {
+ height: 15px;
+ background-position: -3732px 0px;
+}
+.iti__flag.iti__nf {
+ height: 10px;
+ background-position: -3752px 0px;
+}
+.iti__flag.iti__ng {
+ height: 10px;
+ background-position: -3774px 0px;
+}
+.iti__flag.iti__ni {
+ height: 12px;
+ background-position: -3796px 0px;
+}
+.iti__flag.iti__nl {
+ height: 14px;
+ background-position: -3818px 0px;
+}
+.iti__flag.iti__no {
+ height: 15px;
+ background-position: -3840px 0px;
+}
+.iti__flag.iti__np {
+ height: 15px;
+ background-position: -3862px 0px;
+}
+.iti__flag.iti__nr {
+ height: 10px;
+ background-position: -3877px 0px;
+}
+.iti__flag.iti__nu {
+ height: 10px;
+ background-position: -3899px 0px;
+}
+.iti__flag.iti__nz {
+ height: 10px;
+ background-position: -3921px 0px;
+}
+.iti__flag.iti__om {
+ height: 10px;
+ background-position: -3943px 0px;
+}
+.iti__flag.iti__pa {
+ height: 14px;
+ background-position: -3965px 0px;
+}
+.iti__flag.iti__pe {
+ height: 14px;
+ background-position: -3987px 0px;
+}
+.iti__flag.iti__pf {
+ height: 14px;
+ background-position: -4009px 0px;
+}
+.iti__flag.iti__pg {
+ height: 15px;
+ background-position: -4031px 0px;
+}
+.iti__flag.iti__ph {
+ height: 10px;
+ background-position: -4053px 0px;
+}
+.iti__flag.iti__pk {
+ height: 14px;
+ background-position: -4075px 0px;
+}
+.iti__flag.iti__pl {
+ height: 13px;
+ background-position: -4097px 0px;
+}
+.iti__flag.iti__pm {
+ height: 14px;
+ background-position: -4119px 0px;
+}
+.iti__flag.iti__pn {
+ height: 10px;
+ background-position: -4141px 0px;
+}
+.iti__flag.iti__pr {
+ height: 14px;
+ background-position: -4163px 0px;
+}
+.iti__flag.iti__ps {
+ height: 10px;
+ background-position: -4185px 0px;
+}
+.iti__flag.iti__pt {
+ height: 14px;
+ background-position: -4207px 0px;
+}
+.iti__flag.iti__pw {
+ height: 13px;
+ background-position: -4229px 0px;
+}
+.iti__flag.iti__py {
+ height: 11px;
+ background-position: -4251px 0px;
+}
+.iti__flag.iti__qa {
+ height: 8px;
+ background-position: -4273px 0px;
+}
+.iti__flag.iti__re {
+ height: 14px;
+ background-position: -4295px 0px;
+}
+.iti__flag.iti__ro {
+ height: 14px;
+ background-position: -4317px 0px;
+}
+.iti__flag.iti__rs {
+ height: 14px;
+ background-position: -4339px 0px;
+}
+.iti__flag.iti__ru {
+ height: 14px;
+ background-position: -4361px 0px;
+}
+.iti__flag.iti__rw {
+ height: 14px;
+ background-position: -4383px 0px;
+}
+.iti__flag.iti__sa {
+ height: 14px;
+ background-position: -4405px 0px;
+}
+.iti__flag.iti__sb {
+ height: 10px;
+ background-position: -4427px 0px;
+}
+.iti__flag.iti__sc {
+ height: 10px;
+ background-position: -4449px 0px;
+}
+.iti__flag.iti__sd {
+ height: 10px;
+ background-position: -4471px 0px;
+}
+.iti__flag.iti__se {
+ height: 13px;
+ background-position: -4493px 0px;
+}
+.iti__flag.iti__sg {
+ height: 14px;
+ background-position: -4515px 0px;
+}
+.iti__flag.iti__sh {
+ height: 10px;
+ background-position: -4537px 0px;
+}
+.iti__flag.iti__si {
+ height: 10px;
+ background-position: -4559px 0px;
+}
+.iti__flag.iti__sj {
+ height: 15px;
+ background-position: -4581px 0px;
+}
+.iti__flag.iti__sk {
+ height: 14px;
+ background-position: -4603px 0px;
+}
+.iti__flag.iti__sl {
+ height: 14px;
+ background-position: -4625px 0px;
+}
+.iti__flag.iti__sm {
+ height: 15px;
+ background-position: -4647px 0px;
+}
+.iti__flag.iti__sn {
+ height: 14px;
+ background-position: -4669px 0px;
+}
+.iti__flag.iti__so {
+ height: 14px;
+ background-position: -4691px 0px;
+}
+.iti__flag.iti__sr {
+ height: 14px;
+ background-position: -4713px 0px;
+}
+.iti__flag.iti__ss {
+ height: 10px;
+ background-position: -4735px 0px;
+}
+.iti__flag.iti__st {
+ height: 10px;
+ background-position: -4757px 0px;
+}
+.iti__flag.iti__su {
+ height: 10px;
+ background-position: -4779px 0px;
+}
+.iti__flag.iti__sv {
+ height: 12px;
+ background-position: -4801px 0px;
+}
+.iti__flag.iti__sx {
+ height: 14px;
+ background-position: -4823px 0px;
+}
+.iti__flag.iti__sy {
+ height: 14px;
+ background-position: -4845px 0px;
+}
+.iti__flag.iti__sz {
+ height: 14px;
+ background-position: -4867px 0px;
+}
+.iti__flag.iti__ta {
+ height: 10px;
+ background-position: -4889px 0px;
+}
+.iti__flag.iti__tc {
+ height: 10px;
+ background-position: -4911px 0px;
+}
+.iti__flag.iti__td {
+ height: 14px;
+ background-position: -4933px 0px;
+}
+.iti__flag.iti__tf {
+ height: 14px;
+ background-position: -4955px 0px;
+}
+.iti__flag.iti__tg {
+ height: 13px;
+ background-position: -4977px 0px;
+}
+.iti__flag.iti__th {
+ height: 14px;
+ background-position: -4999px 0px;
+}
+.iti__flag.iti__tj {
+ height: 10px;
+ background-position: -5021px 0px;
+}
+.iti__flag.iti__tk {
+ height: 10px;
+ background-position: -5043px 0px;
+}
+.iti__flag.iti__tl {
+ height: 10px;
+ background-position: -5065px 0px;
+}
+.iti__flag.iti__tm {
+ height: 14px;
+ background-position: -5087px 0px;
+}
+.iti__flag.iti__tn {
+ height: 14px;
+ background-position: -5109px 0px;
+}
+.iti__flag.iti__to {
+ height: 10px;
+ background-position: -5131px 0px;
+}
+.iti__flag.iti__tr {
+ height: 14px;
+ background-position: -5153px 0px;
+}
+.iti__flag.iti__tt {
+ height: 12px;
+ background-position: -5175px 0px;
+}
+.iti__flag.iti__tv {
+ height: 10px;
+ background-position: -5197px 0px;
+}
+.iti__flag.iti__tw {
+ height: 14px;
+ background-position: -5219px 0px;
+}
+.iti__flag.iti__tz {
+ height: 14px;
+ background-position: -5241px 0px;
+}
+.iti__flag.iti__ua {
+ height: 14px;
+ background-position: -5263px 0px;
+}
+.iti__flag.iti__ug {
+ height: 14px;
+ background-position: -5285px 0px;
+}
+.iti__flag.iti__uk {
+ height: 10px;
+ background-position: -5307px 0px;
+}
+.iti__flag.iti__um {
+ height: 11px;
+ background-position: -5329px 0px;
+}
+.iti__flag.iti__un {
+ height: 14px;
+ background-position: -5351px 0px;
+}
+.iti__flag.iti__us {
+ height: 11px;
+ background-position: -5373px 0px;
+}
+.iti__flag.iti__uy {
+ height: 14px;
+ background-position: -5395px 0px;
+}
+.iti__flag.iti__uz {
+ height: 10px;
+ background-position: -5417px 0px;
+}
+.iti__flag.iti__va {
+ height: 15px;
+ background-position: -5439px 0px;
+}
+.iti__flag.iti__vc {
+ height: 14px;
+ background-position: -5456px 0px;
+}
+.iti__flag.iti__ve {
+ height: 14px;
+ background-position: -5478px 0px;
+}
+.iti__flag.iti__vg {
+ height: 10px;
+ background-position: -5500px 0px;
+}
+.iti__flag.iti__vi {
+ height: 14px;
+ background-position: -5522px 0px;
+}
+.iti__flag.iti__vn {
+ height: 14px;
+ background-position: -5544px 0px;
+}
+.iti__flag.iti__vu {
+ height: 12px;
+ background-position: -5566px 0px;
+}
+.iti__flag.iti__wf {
+ height: 14px;
+ background-position: -5588px 0px;
+}
+.iti__flag.iti__ws {
+ height: 10px;
+ background-position: -5610px 0px;
+}
+.iti__flag.iti__xk {
+ height: 15px;
+ background-position: -5632px 0px;
+}
+.iti__flag.iti__ye {
+ height: 14px;
+ background-position: -5654px 0px;
+}
+.iti__flag.iti__yt {
+ height: 14px;
+ background-position: -5676px 0px;
+}
+.iti__flag.iti__za {
+ height: 14px;
+ background-position: -5698px 0px;
+}
+.iti__flag.iti__zm {
+ height: 14px;
+ background-position: -5720px 0px;
+}
+.iti__flag.iti__zw {
+ height: 10px;
+ background-position: -5742px 0px;
+}
+
+.iti__flag {
+ height: 15px;
+ box-shadow: 0px 0px 1px 0px #888;
+ background-image: url("../img/flags.png?1");
+ background-repeat: no-repeat;
+ background-color: #dbdbdb;
+ background-position: 20px 0;
+}
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .iti__flag {
+ background-image: url("../img/flags@2x.png?1");
+ }
+}
+
+.iti__flag.iti__np {
+ background-color: transparent;
+}
\ No newline at end of file
diff --git a/frontend/libs.json b/frontend/libs.json
index f59eab6be0..6f36fa85df 100644
--- a/frontend/libs.json
+++ b/frontend/libs.json
@@ -114,6 +114,17 @@
"bundle": true,
"amdId": "autonumeric"
},
+ {
+ "src": "node_modules/intl-tel-input/build/js/intlTelInput.js",
+ "bundle": true,
+ "amdId": "intl-tel-input",
+ "suppressAmd": true
+ },
+ {
+ "src": "node_modules/intl-tel-input/build/js/utils.js",
+ "bundle": true,
+ "amdId": "intl-tel-input-utils"
+ },
{
"src": "node_modules/summernote/dist/summernote.js",
"amdId": "summernote",
diff --git a/package-lock.json b/package-lock.json
index 25348a4989..a534bf06ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"getobject": ">=1.0.0",
"gridstack": "github:yurikuzn/gridstack.js#5.1.1.e3",
"handlebars": "^4.7.7",
+ "intl-tel-input": "^18.2.1",
"jquery": "^3.7.0",
"jquery-textcomplete": "^1.8.5",
"jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2",
@@ -4725,6 +4726,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
},
+ "node_modules/intl-tel-input": {
+ "version": "18.2.1",
+ "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-18.2.1.tgz",
+ "integrity": "sha512-wOm0/61kTtpYjOOW1bhHzC4G8Om+atTxHmg31FS0KD0LQ8k8BpgO925npyi4jlT/EK4+joABABZzz0/XeSgupQ=="
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -10688,6 +10694,11 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
},
+ "intl-tel-input": {
+ "version": "18.2.1",
+ "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-18.2.1.tgz",
+ "integrity": "sha512-wOm0/61kTtpYjOOW1bhHzC4G8Om+atTxHmg31FS0KD0LQ8k8BpgO925npyi4jlT/EK4+joABABZzz0/XeSgupQ=="
+ },
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
diff --git a/package.json b/package.json
index d929e4a2c7..79793ebcaf 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"getobject": ">=1.0.0",
"gridstack": "github:yurikuzn/gridstack.js#5.1.1.e3",
"handlebars": "^4.7.7",
+ "intl-tel-input": "^18.2.1",
"jquery": "^3.7.0",
"jquery-textcomplete": "^1.8.5",
"jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2",
diff --git a/upgrades/8.1/data.json b/upgrades/8.1/data.json
new file mode 100644
index 0000000000..60ec91eb56
--- /dev/null
+++ b/upgrades/8.1/data.json
@@ -0,0 +1,10 @@
+{
+ "manifest": {
+ "delete": [
+ "client/lib/original/espo.js",
+ "client/lib/espo.min.js",
+ "client/lib/espo.min.js.map",
+ "client/cfg/pre-load.json"
+ ]
+ }
+}
diff --git a/upgrades/8.1/scripts/AfterUpgrade.php b/upgrades/8.1/scripts/AfterUpgrade.php
new file mode 100644
index 0000000000..215b868c6c
--- /dev/null
+++ b/upgrades/8.1/scripts/AfterUpgrade.php
@@ -0,0 +1,48 @@
+getByClass(InjectableFactory::class)
+ ->create(ConfigWriter::class);
+
+ $configWriter->setMultiple([
+ 'phoneNumberNumericSearch' => false,
+ 'phoneNumberInternational' => false,
+ ]);
+
+ $configWriter->save();
+ }
+}