mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { z } from 'zod';
|
|
import { AsYouType, Metadata, getCountryCallingCode, isValidPhoneNumber, parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js';
|
|
|
|
/**
|
|
* Accepts any international number in E.164 (`+<country><number>`) or a
|
|
* bare national number, which is assumed Ethiopian (`0911223344` ->
|
|
* `+251911223344`) for backward compatibility with existing records.
|
|
*/
|
|
export const phoneNumber = z
|
|
.string()
|
|
.trim()
|
|
.transform((v) => parsePhoneNumberFromString(v, 'ET')?.number ?? v)
|
|
.superRefine((v, ctx) => {
|
|
if (!v) ctx.addIssue({ code: 'custom', message: 'Phone number is required' });
|
|
else if (!isValidPhoneNumber(v)) ctx.addIssue({ code: 'custom', message: 'Enter a valid phone number' });
|
|
});
|
|
|
|
/** Same rules, but blank is allowed. */
|
|
export const optionalPhoneNumber = z.union([z.literal(''), phoneNumber]).optional();
|
|
|
|
/**
|
|
* Digits the field should hold after an edit. A keystroke that only removed
|
|
* a formatting character (backspacing the ')' out of "(415)") leaves the
|
|
* digits unchanged, which would otherwise make the caret stick — drop a real
|
|
* digit in that case.
|
|
*/
|
|
export function nextNationalDigits(text: string, prevDisplay: string): string {
|
|
const digits = text.replace(/\D/g, '');
|
|
const deleting = text.length < prevDisplay.length;
|
|
if (deleting && digits === prevDisplay.replace(/\D/g, '')) return digits.slice(0, -1);
|
|
return digits;
|
|
}
|
|
|
|
/** National digits of a stored value, for display in the number box. */
|
|
export function toNationalDigits(value: string, country: CountryCode): string {
|
|
if (!value) return '';
|
|
// `country` also resolves legacy national records ("0911223344").
|
|
const parsed = parsePhoneNumberFromString(value, country);
|
|
if (parsed) return parsed.nationalNumber;
|
|
if (value.startsWith('+')) {
|
|
const prefix = `+${getCountryCallingCode(country)}`;
|
|
if (value.startsWith(prefix)) return value.slice(prefix.length).replace(/\D/g, '');
|
|
}
|
|
return value.replace(/\D/g, '');
|
|
}
|
|
|
|
/**
|
|
* E.164 for the digits typed so far. Incomplete numbers don't parse, so they
|
|
* fall back to a plain dial-code concatenation rather than collapsing to ''
|
|
* — the value has to survive mid-typing for the field to be usable.
|
|
*/
|
|
export function toE164(digits: string, country: CountryCode): string {
|
|
if (!digits) return '';
|
|
return (
|
|
parsePhoneNumberFromString(digits, country)?.number ??
|
|
`+${getCountryCallingCode(country)}${digits}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Digits grouped for display ("91 122 3344"). Formatted as an international
|
|
* number with the dial code cut off: AsYouType's national mode leaves the
|
|
* number ungrouped unless the trunk prefix was typed.
|
|
*/
|
|
export function formatNational(digits: string, country: CountryCode): string {
|
|
if (!digits) return '';
|
|
const prefix = `+${getCountryCallingCode(country)}`;
|
|
const formatted = new AsYouType().input(prefix + digits);
|
|
return formatted.startsWith(prefix) ? formatted.slice(prefix.length).trimStart() : digits;
|
|
}
|
|
|
|
const metadata = new Metadata();
|
|
|
|
/** Longest national number the country's numbering plan allows. */
|
|
export function maxNationalLength(country: CountryCode): number {
|
|
metadata.selectNumberingPlan(country);
|
|
return Math.max(...(metadata.numberingPlan?.possibleLengths() ?? [15]));
|
|
}
|
|
|
|
/**
|
|
* True when the typed digits exceed the country's longest number. Judged on
|
|
* the national part once it parses, so a trunk prefix (0911223344) or typed
|
|
* country code (251911223344) isn't counted against the limit.
|
|
*/
|
|
export function exceedsMaxLength(digits: string, country: CountryCode): boolean {
|
|
const national = parsePhoneNumberFromString(digits, country)?.nationalNumber ?? digits;
|
|
return national.length > maxNationalLength(country);
|
|
}
|