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.
@@ -1,5 +1,9 @@
1
1
  import { Transformer, type Selection } from '../Transformer';
2
- import { COUNTRY_PHONE_DATA, type PhoneFormat } from './phone-data';
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(