react-native-transformer-text-input 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/formatters/phone-data.js +208 -0
- package/lib/module/formatters/phone-data.js.map +1 -1
- package/lib/module/formatters/phone-number.js +145 -1
- package/lib/module/formatters/phone-number.js.map +1 -1
- package/lib/typescript/src/formatters/phone-data.d.ts +1 -0
- package/lib/typescript/src/formatters/phone-data.d.ts.map +1 -1
- package/lib/typescript/src/formatters/phone-number.d.ts +17 -1
- package/lib/typescript/src/formatters/phone-number.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/formatters/phone-data.ts +235 -0
- package/src/formatters/phone-number.ts +168 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Transformer, type Selection } from '../Transformer';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
COUNTRY_CALLING_CODES,
|
|
4
|
+
COUNTRY_PHONE_DATA,
|
|
5
|
+
type PhoneFormat,
|
|
6
|
+
} from './phone-data';
|
|
3
7
|
|
|
4
8
|
export type PhoneNumberTransformerOptions = {
|
|
5
9
|
/**
|
|
@@ -14,6 +18,14 @@ export type PhoneNumberTransformerOptions = {
|
|
|
14
18
|
* @default true
|
|
15
19
|
*/
|
|
16
20
|
includeCallingCode?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* International mode: the calling code is part of the editable text and the
|
|
23
|
+
* country is detected from it as you type (e.g. typing "+44" switches
|
|
24
|
+
* formatting to the UK). `country` is ignored in this mode. Use
|
|
25
|
+
* {@link detectCountry} to drive a flag/country indicator from the value.
|
|
26
|
+
* @default false
|
|
27
|
+
*/
|
|
28
|
+
international?: boolean;
|
|
17
29
|
/**
|
|
18
30
|
* Enable debug logging for transformer operations.
|
|
19
31
|
* @default false
|
|
@@ -21,6 +33,36 @@ export type PhoneNumberTransformerOptions = {
|
|
|
21
33
|
debug?: boolean;
|
|
22
34
|
};
|
|
23
35
|
|
|
36
|
+
// Longest calling-code prefix (1–3 digits) of `digits` that is a key of
|
|
37
|
+
// `codes`. `codes` can be any calling-code-keyed record (the lookup table or
|
|
38
|
+
// the per-code format index), so the worklet reuses its captured index.
|
|
39
|
+
const matchCallingCode = (
|
|
40
|
+
digits: string,
|
|
41
|
+
codes: Record<string, unknown>,
|
|
42
|
+
): string => {
|
|
43
|
+
'worklet';
|
|
44
|
+
const max = Math.min(3, digits.length);
|
|
45
|
+
for (let len = max; len >= 1; len--) {
|
|
46
|
+
if (codes[digits.slice(0, len)] !== undefined) {
|
|
47
|
+
return digits.slice(0, len);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect the ISO 3166-1 alpha-2 country for a phone value by its leading
|
|
55
|
+
* international calling code. Returns the primary country for the code (e.g.
|
|
56
|
+
* "US" for "+1"), or undefined if no calling code is recognized yet. Runs on
|
|
57
|
+
* the JS thread — use it to drive a flag/country indicator alongside an
|
|
58
|
+
* `international` PhoneNumberTransformer.
|
|
59
|
+
*/
|
|
60
|
+
export function detectCountry(value: string): string | undefined {
|
|
61
|
+
const digits = value.replace(/\D/g, '');
|
|
62
|
+
const code = matchCallingCode(digits, COUNTRY_CALLING_CODES);
|
|
63
|
+
return code ? COUNTRY_CALLING_CODES[code]?.[0] : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
// Extract all digits from text
|
|
25
67
|
const extractDigits = (text: string): string => {
|
|
26
68
|
'worklet';
|
|
@@ -254,8 +296,133 @@ export class PhoneNumberTransformer extends Transformer {
|
|
|
254
296
|
constructor({
|
|
255
297
|
country = 'US',
|
|
256
298
|
includeCallingCode = true,
|
|
299
|
+
international = false,
|
|
257
300
|
debug = false,
|
|
258
301
|
}: PhoneNumberTransformerOptions = {}) {
|
|
302
|
+
if (international) {
|
|
303
|
+
// Per-calling-code formatting data (primary country for each code),
|
|
304
|
+
// captured once so the worklet can detect + format any country.
|
|
305
|
+
const callingCodeData: Record<
|
|
306
|
+
string,
|
|
307
|
+
{ formats: PhoneFormat[]; nationalPrefix: string }
|
|
308
|
+
> = {};
|
|
309
|
+
for (const code in COUNTRY_CALLING_CODES) {
|
|
310
|
+
const primary = COUNTRY_CALLING_CODES[code]![0]!;
|
|
311
|
+
const d = COUNTRY_PHONE_DATA[primary];
|
|
312
|
+
if (d) {
|
|
313
|
+
callingCodeData[code] = {
|
|
314
|
+
formats: d.formats,
|
|
315
|
+
nationalPrefix: d.nationalPrefix ?? '',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const intlWorklet = (input: {
|
|
321
|
+
value: string;
|
|
322
|
+
previousValue: string;
|
|
323
|
+
selection: Selection;
|
|
324
|
+
previousSelection: Selection;
|
|
325
|
+
}) => {
|
|
326
|
+
'worklet';
|
|
327
|
+
|
|
328
|
+
const { value, selection, previousValue, previousSelection } = input;
|
|
329
|
+
|
|
330
|
+
const allDigits = extractDigits(value);
|
|
331
|
+
const prevAllDigits = extractDigits(previousValue);
|
|
332
|
+
|
|
333
|
+
if (allDigits.length === 0) {
|
|
334
|
+
return { value: '', selection: { start: 0, end: 0 } };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const digitsBeforeStart = countDigitsBefore(value, selection.start);
|
|
338
|
+
const digitsBeforeEnd = countDigitsBefore(value, selection.end);
|
|
339
|
+
|
|
340
|
+
// Backspacing a separator removes the digit before the caret instead,
|
|
341
|
+
// so deletion makes progress.
|
|
342
|
+
const isCaret = selection.start === selection.end;
|
|
343
|
+
const deletedFormattingChar =
|
|
344
|
+
isCaret &&
|
|
345
|
+
value.length < previousValue.length &&
|
|
346
|
+
allDigits.length === prevAllDigits.length &&
|
|
347
|
+
allDigits.length > 0;
|
|
348
|
+
|
|
349
|
+
let workingDigits = allDigits;
|
|
350
|
+
let finalStart = digitsBeforeStart;
|
|
351
|
+
let finalEnd = digitsBeforeEnd;
|
|
352
|
+
if (deletedFormattingChar && digitsBeforeStart > 0) {
|
|
353
|
+
workingDigits =
|
|
354
|
+
allDigits.slice(0, digitsBeforeStart - 1) +
|
|
355
|
+
allDigits.slice(digitsBeforeStart);
|
|
356
|
+
finalStart = digitsBeforeStart - 1;
|
|
357
|
+
finalEnd = finalStart;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const cursorAtEnd =
|
|
361
|
+
isCaret &&
|
|
362
|
+
selection.end >= value.length &&
|
|
363
|
+
previousSelection.end >= previousValue.length;
|
|
364
|
+
|
|
365
|
+
const callingCode = matchCallingCode(workingDigits, callingCodeData);
|
|
366
|
+
|
|
367
|
+
// No recognized calling code yet — show "+digits" as typed.
|
|
368
|
+
if (callingCode === '') {
|
|
369
|
+
const result = '+' + workingDigits;
|
|
370
|
+
if (cursorAtEnd) {
|
|
371
|
+
return {
|
|
372
|
+
value: result,
|
|
373
|
+
selection: { start: result.length, end: result.length },
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
// One leading "+" before the digits.
|
|
377
|
+
return {
|
|
378
|
+
value: result,
|
|
379
|
+
selection: { start: 1 + finalStart, end: 1 + finalEnd },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const data = callingCodeData[callingCode]!;
|
|
384
|
+
const nationalPrefix = data.nationalPrefix;
|
|
385
|
+
const nationalDigits = workingDigits.slice(callingCode.length);
|
|
386
|
+
|
|
387
|
+
let result: string;
|
|
388
|
+
if (nationalDigits.length === 0) {
|
|
389
|
+
result = '+' + callingCode;
|
|
390
|
+
} else {
|
|
391
|
+
let significantDigits = nationalDigits;
|
|
392
|
+
let trunkPrefix = '';
|
|
393
|
+
if (
|
|
394
|
+
nationalPrefix.length > 0 &&
|
|
395
|
+
nationalDigits.length > nationalPrefix.length &&
|
|
396
|
+
nationalDigits.startsWith(nationalPrefix)
|
|
397
|
+
) {
|
|
398
|
+
significantDigits = nationalDigits.slice(nationalPrefix.length);
|
|
399
|
+
trunkPrefix = nationalPrefix;
|
|
400
|
+
}
|
|
401
|
+
const format = selectFormat(significantDigits, data.formats);
|
|
402
|
+
const formattedNational = format
|
|
403
|
+
? trunkPrefix + applyFormat(significantDigits, format)
|
|
404
|
+
: nationalDigits;
|
|
405
|
+
result = '+' + callingCode + ' ' + formattedNational;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (cursorAtEnd) {
|
|
409
|
+
return {
|
|
410
|
+
value: result,
|
|
411
|
+
selection: { start: result.length, end: result.length },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// result digits are [callingCode..., national...] in input order, so
|
|
416
|
+
// map the input digit counts straight onto the formatted output.
|
|
417
|
+
const newStart = mapCursorToFormatted(result, finalStart);
|
|
418
|
+
const newEnd = mapCursorToFormatted(result, finalEnd);
|
|
419
|
+
return { value: result, selection: { start: newStart, end: newEnd } };
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
super(intlWorklet);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
259
426
|
const countryData = COUNTRY_PHONE_DATA[country];
|
|
260
427
|
if (!countryData) {
|
|
261
428
|
throw new Error(
|