I had a similiar issue. There is also the fact that there can be invisible unicode characters. For example, when copy-pasting from Apple Contacts or other sources, which my client was doing a lot and that lead to a bunch of difficulties with both validation and when using the numbers in some places. I’ll share my solution in case others find it useful.
You can create a custom validator for your phone field to check for unwanted spaces, but that might not be a great user experience, especially when there are invisible marks involved.
So I ended up doing a custom field extending the default ‘tel’ field, replacing the save method. But I didn’t just want to remove all spaces and odd characters. I needed all phone numbers to be formatted in a standardized way (with spaces and dashes in the correct position and country code added if not provided). I share the code here, but phone number standards varies by country and I only needed to deal with swedish number patterns.
index.php
<?php
/*
Clean up phone numbers and add swedish country code if not provided
Invisible characters removed
Ignores invisible or non-printing Unicode characters often found when copying from Apple Contacts or similar sources.
Characters removed by this regex:
- U+00A0 : NO-BREAK SPACE
- U+2000–U+200F : Various invisible spaces and directional marks
- U+202A–U+202E : Bidirectional formatting codes
- U+2066–U+2069 : Bidirectional isolates
- U+FEFF : ZERO WIDTH NO-BREAK SPACE (aka BOM - Byte Order Mark)
- U+FFF9–U+FFFB : Interlinear annotation controls (rare but safe to strip)
*/
Kirby::plugin('tamburlane/telExtended', [
'fields' => [
'telX' => [
'extends' => 'tel',
'save' => function ($value) {
// Step 1: Clean invisible characters and trim whitespace
$value = preg_replace('/[\x{00A0}\x{2000}-\x{200F}\x{202A}-\x{202E}\x{2066}-\x{2069}\x{FEFF}\x{FFF9}-\x{FFFB}]/u', '', $value);
$value = trim($value);
// Step 2: Remove all formatting characters (spaces, dashes, parentheses)
$number = preg_replace('/[^\d+]/', '', $value);
// Step 3: Convert leading 0 to +46 (assume swedish country code)
if (preg_match('/^0(\d{6,9})$/', $number, $m)) {
$number = '+46' . $m[1];
}
// Step 4: Format known Swedish number types
// Mobile numbers: 07X-XXX XX XX (always 9 digits after +46 7X)
if (preg_match('/^\+467(\d)(\d{3})(\d{2})(\d{2})$/', $number, $m)) {
return "+46 7{$m[1]}-{$m[2]} {$m[3]} {$m[4]}";
}
// Stockholm landline: 08-XX XX XX
if (preg_match('/^\+468(\d{2})(\d{2})(\d{2})$/', $number, $m)) {
return "+46 8-{$m[1]} {$m[2]} {$m[3]}";
}
// Stockholm landline: 08-XXX XX X
if (preg_match('/^\+468(\d{3})(\d{2})(\d)$/', $number, $m)) {
return "+46 8-{$m[1]} {$m[2]} {$m[3]}";
}
// Stockholm landline: 08-XXX XXX
if (preg_match('/^\+468(\d{3})(\d{3})$/', $number, $m)) {
return "+46 8-{$m[1]} {$m[2]}";
}
// Landlines with 2-digit area code (e.g. Göteborg: 031)
// Format: 031-XX XX XX
if (preg_match('/^\+46(\d{2})(\d{2})(\d{2})(\d{2})$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]} {$m[4]}";
}
// Format: 031-XXX XX
if (preg_match('/^\+46(\d{2})(\d{3})(\d{2})$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]}";
}
// Format: 031-XX XX
if (preg_match('/^\+46(\d{2})(\d{2})(\d{2})$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]}";
}
// Landlines with 3-digit area code (e.g. Gotland: 0498)
// Format: 0498-XXX XX X
if (preg_match('/^\+46(\d{3})(\d{3})(\d{2})(\d)$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]} {$m[4]}";
}
// Format: 0498-XXX XX
if (preg_match('/^\+46(\d{3})(\d{3})(\d{2})$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]}";
}
// Format: 0498-XX XX
if (preg_match('/^\+46(\d{3})(\d{2})(\d{2})$/', $number, $m)) {
return "+46 {$m[1]}-{$m[2]} {$m[3]}";
}
// Fallback: return cleaned number without formatting
return $number;
}
]
]
]);
index.js
panel.plugin("tamburlane/telExtended", {
fields: {
telX: {
extends: "k-tel-field"
}
}
});
Then, I made a custom panel form validator that checks min/max length and allowed characters, while ignoring whitespace and invisible characters instead of complaining about them (as the field plugin will clean those on save anyway).
<?php
use Kirby\Toolkit\V;
Kirby::plugin("tamburlane/validators", [
"validators" => [
/* A loose check, pretty forgiving
* 7-17 chars
* 0-9, +, -, space
*/
"phone" => function ($value) {
// Spaces, invisible spaces, formatting marks etc are ignored so we can count length and check that remaining characters looks like a phone number
$value = preg_replace('/[\x{00A0}\x{2000}-\x{200F}\x{202A}-\x{202E}\x{2066}-\x{2069}\x{FEFF}\x{FFF9}-\x{FFFB}]/u', '', $value);
//dump(bin2hex($value));
if (
V::minLength($value, 7) && V::maxLength($value, 17)
&& (V::startsWith($value, '+') || V::startsWith($value, '0'))
)
{
return V::match($value, '/^([0-9 +-])+$/i');
}
else {
return false;
}
}
],
]);