ngx-digits-only 1.0.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.
@@ -0,0 +1,1847 @@
1
+ import * as i0 from '@angular/core';
2
+ import { forwardRef, HostListener, Input, Inject, Directive } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+ import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
5
+
6
+ /**
7
+ * digits-only.regex.ts
8
+ *
9
+ * Every regular expression used by the digitsOnly directive lives here.
10
+ * Each constant has a plain-English name, a detailed breakdown, and live examples
11
+ * so you never have to decode cryptic patterns inside the directive itself.
12
+ *
13
+ * Import what you need:
14
+ * import { REGEX } from './digits-only.regex';
15
+ * REGEX.SINGLE_DIGIT.test('5') // true
16
+ */
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // HOW TO READ REGEX ANATOMY
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ //
21
+ // /pattern/flags
22
+ // │ │
23
+ // │ └── modifiers: g = global (find ALL matches, not just first)
24
+ // │ i = case-insensitive
25
+ // │ m = multiline
26
+ // │
27
+ // └── special characters:
28
+ // ^ start of string (or line with m flag)
29
+ // $ end of string (or line with m flag)
30
+ // . any single character except newline
31
+ // \d any digit [0-9]
32
+ // \D any NON-digit
33
+ // \w word character [a-zA-Z0-9_]
34
+ // \s whitespace
35
+ // \B NON-word boundary position
36
+ // [abc] character class – matches a, b, or c
37
+ // [^abc] negated class – matches anything EXCEPT a, b, c
38
+ // (abc) capture group
39
+ // (?=x) positive lookahead – "followed by x" (zero-width, no consume)
40
+ // (?!x) negative lookahead – "NOT followed by x"
41
+ // {n} exactly n repetitions
42
+ // {n,} n or more repetitions
43
+ // + one or more (greedy)
44
+ // * zero or more (greedy)
45
+ // ? zero or one (optional)
46
+ // \. escaped dot – a literal '.' character
47
+ // \\ escaped backslash – a literal '\'
48
+ // \- escaped hyphen inside [] – a literal '-'
49
+ // $& in replacement strings: the entire matched text
50
+ const REGEX = {
51
+ // ──────────────────────────────────────────────────────────────────────────
52
+ // 1. SINGLE_DIGIT
53
+ // Is this one keyboard character exactly a digit and nothing else?
54
+ // ──────────────────────────────────────────────────────────────────────────
55
+ //
56
+ // Pattern breakdown:
57
+ // ^ – anchor: string must START here
58
+ // \d – one digit character (0, 1, 2 … 9)
59
+ // $ – anchor: string must END here
60
+ //
61
+ // The ^ and $ together mean the whole string must be exactly one digit.
62
+ // Without them, /\d/ would match '5' inside 'abc5xyz' too.
63
+ //
64
+ // Used in: onKeyDown() — to decide if a pressed key is a digit
65
+ //
66
+ // ✔ matches: '0' '5' '9'
67
+ // ✖ no match: 'a' '-' '.' 'Enter' '12' ''
68
+ //
69
+ SINGLE_DIGIT: /^\d$/,
70
+ // ──────────────────────────────────────────────────────────────────────────
71
+ // 2. NON_DIGIT_CHARACTERS
72
+ // Find (and remove) every character that is NOT a digit.
73
+ // ──────────────────────────────────────────────────────────────────────────
74
+ //
75
+ // Pattern breakdown:
76
+ // [^0-9] – negated character class
77
+ // ^ inside [] = NOT
78
+ // 0-9 = any digit
79
+ // → "any character that is not 0 through 9"
80
+ // g – global flag: match ALL occurrences, not just the first
81
+ //
82
+ // Used in: onKeyDown (count digits), validate (count digits), onPaste
83
+ //
84
+ // Purpose: Calling .replace(NON_DIGIT_CHARACTERS, '') strips everything
85
+ // except digits so we can measure the pure digit length,
86
+ // ignoring separators, dots, minus signs, prefix/suffix.
87
+ //
88
+ // Examples:
89
+ // '$1,234.56'.replace(REGEX.NON_DIGIT_CHARACTERS, '') → '123456'
90
+ // '-99_000'.replace(REGEX.NON_DIGIT_CHARACTERS, '') → '99000'
91
+ // '007'.replace(REGEX.NON_DIGIT_CHARACTERS, '') → '007'
92
+ //
93
+ NON_DIGIT_CHARACTERS: /[^0-9]/g,
94
+ // ──────────────────────────────────────────────────────────────────────────
95
+ // 3. ONLY_DIGITS_AND_MINUS
96
+ // Remove everything except digits and the minus sign.
97
+ // Used for integer-mode paste sanitization.
98
+ // ──────────────────────────────────────────────────────────────────────────
99
+ //
100
+ // Pattern breakdown:
101
+ // [^0-9\-] – negated class that keeps digits and the literal hyphen
102
+ // \- = escaped hyphen (a bare '-' in the middle of [] is
103
+ // treated as a range operator, e.g. [a-z];
104
+ // escaping with \ makes it a literal minus character)
105
+ // g – global
106
+ //
107
+ // Used in: sanitizePastedText() when decimalPlaces === 0
108
+ //
109
+ // Examples (integers only, negatives allowed):
110
+ // '1,234.56'.replace(REGEX.ONLY_DIGITS_AND_MINUS, '') → '123456'
111
+ // '-99.9%'.replace(REGEX.ONLY_DIGITS_AND_MINUS, '') → '-999'
112
+ // '007'.replace(REGEX.ONLY_DIGITS_AND_MINUS, '') → '007'
113
+ //
114
+ ONLY_DIGITS_AND_MINUS: /[^0-9\-]/g,
115
+ // ──────────────────────────────────────────────────────────────────────────
116
+ // 4. ONLY_DIGITS_DOT_AND_MINUS
117
+ // Remove everything except digits, a decimal point, and the minus sign.
118
+ // Used for decimal-number paste sanitization.
119
+ // ──────────────────────────────────────────────────────────────────────────
120
+ //
121
+ // Pattern breakdown:
122
+ // [^0-9.\-] – negated class
123
+ // . inside [] is a literal dot (no escape needed inside [])
124
+ // \- is the literal minus
125
+ // g – global
126
+ //
127
+ // Difference from ONLY_DIGITS_AND_MINUS:
128
+ // This one also KEEPS the dot '.' so decimal numbers like '12.5' survive.
129
+ //
130
+ // Used in: sanitizePastedText() when decimalPlaces > 0
131
+ //
132
+ // Examples:
133
+ // '$-1,234.56 USD'.replace(REGEX.ONLY_DIGITS_DOT_AND_MINUS, '') → '-1234.56'
134
+ // '€ 99.9%'.replace(REGEX.ONLY_DIGITS_DOT_AND_MINUS, '') → '99.9'
135
+ // 'abc123def'.replace(REGEX.ONLY_DIGITS_DOT_AND_MINUS, '') → '123'
136
+ //
137
+ ONLY_DIGITS_DOT_AND_MINUS: /[^0-9.\-]/g,
138
+ // ──────────────────────────────────────────────────────────────────────────
139
+ // 5. ONLY_DIGITS
140
+ // Remove everything except bare digits.
141
+ // Used for string-mode (identifiers like card numbers) where dots and
142
+ // minus signs are also illegal.
143
+ // ──────────────────────────────────────────────────────────────────────────
144
+ //
145
+ // Pattern breakdown:
146
+ // [^0-9] – keep only digit characters
147
+ // g – global
148
+ //
149
+ // Used in: sanitizePastedText() in string / pattern mode,
150
+ // onPaste() in pattern mode,
151
+ // _applyPattern() to extract digit string before formatting
152
+ //
153
+ // Examples:
154
+ // '4111-1111-1111-1111'.replace(REGEX.ONLY_DIGITS, '') → '4111111111111111'
155
+ // '(555) 867-5309'.replace(REGEX.ONLY_DIGITS, '') → '5558675309'
156
+ // '01/01/1990'.replace(REGEX.ONLY_DIGITS, '') → '01011990'
157
+ //
158
+ ONLY_DIGITS: /[^0-9]/g,
159
+ // ──────────────────────────────────────────────────────────────────────────
160
+ // 6. ALL_DOTS
161
+ // Remove every dot/period from a string.
162
+ // ──────────────────────────────────────────────────────────────────────────
163
+ //
164
+ // Pattern breakdown:
165
+ // \. – escaped dot.
166
+ // In regex, a bare '.' means "any character except newline".
167
+ // The backslash \ escapes it to mean a LITERAL period character.
168
+ // Without the backslash, /./g would erase EVERY character.
169
+ // g – global: remove all dots, not just the first
170
+ //
171
+ // Used in: onInput() and onPaste() when decimalPlaces === 0 or pattern is set.
172
+ // If integers are expected, any dot that slips through (e.g. from paste)
173
+ // must be stripped.
174
+ //
175
+ // Examples:
176
+ // '12.34'.replace(REGEX.ALL_DOTS, '') → '1234'
177
+ // '1.2.3'.replace(REGEX.ALL_DOTS, '') → '123'
178
+ // '100'.replace(REGEX.ALL_DOTS, '') → '100' (no dot, unchanged)
179
+ //
180
+ ALL_DOTS: /\./g,
181
+ // ──────────────────────────────────────────────────────────────────────────
182
+ // 7. ALL_MINUS_SIGNS
183
+ // Remove every minus / hyphen character from a string.
184
+ // ──────────────────────────────────────────────────────────────────────────
185
+ //
186
+ // Pattern breakdown:
187
+ // - – a literal hyphen/minus character
188
+ // (safe outside of a character class [] without escaping)
189
+ // g – global
190
+ //
191
+ // Used in: sanitizePastedText() to strip minus signs when allowNegative is false
192
+ // or when outputType is 'string' (identifiers can't be negative).
193
+ //
194
+ // Examples:
195
+ // '-99'.replace(REGEX.ALL_MINUS_SIGNS, '') → '99'
196
+ // '1-2-3'.replace(REGEX.ALL_MINUS_SIGNS, '') → '123'
197
+ //
198
+ ALL_MINUS_SIGNS: /-/g,
199
+ // ──────────────────────────────────────────────────────────────────────────
200
+ // 8. THOUSANDS_SEPARATOR_POSITIONS
201
+ // Find every position in the INTEGER PART of a number where a thousands
202
+ // separator (comma, dot, space, underscore) should be inserted.
203
+ // This is the most sophisticated regex in the codebase.
204
+ // ──────────────────────────────────────────────────────────────────────────
205
+ //
206
+ // Full pattern: /\B(?=(\d{3})+(?!\d))/g
207
+ //
208
+ // Token-by-token breakdown:
209
+ //
210
+ // \B – NON-word-boundary position.
211
+ // Matches a position that is NOT at the very start of the
212
+ // string and NOT right after a non-word character.
213
+ // This prevents a separator being inserted before the first
214
+ // digit (e.g. ",1,234" would be wrong).
215
+ //
216
+ // (?= – POSITIVE LOOKAHEAD — zero-width assertion.
217
+ // "The following characters must match, but don't consume them."
218
+ // The regex engine peeks ahead without advancing its position.
219
+ //
220
+ // (\d{3})+ – one or more groups of EXACTLY THREE digits.
221
+ // \d = any digit
222
+ // {3} = exactly 3 of them
223
+ // + = one or more such groups (so 3, 6, 9, 12 … digits ahead)
224
+ //
225
+ // (?!\d) – NEGATIVE LOOKAHEAD.
226
+ // "What follows the groups of 3 is NOT another digit."
227
+ // This anchors the groups to the END of the number so the
228
+ // separator only lands at multiples of 3 from the RIGHT.
229
+ //
230
+ // ) – close the positive lookahead
231
+ // g – global: insert at every valid position
232
+ //
233
+ // How the engine works step-by-step on '1234567':
234
+ //
235
+ // Position between '1' and '2' → lookahead sees '234567' → 6 digits → 6=3×2 ✔ → INSERT
236
+ // Position between '2' and '3' → lookahead sees '34567' → 5 digits → not ÷3 ✖
237
+ // Position between '3' and '4' → lookahead sees '4567' → 4 digits → not ÷3 ✖
238
+ // Position between '4' and '5' → lookahead sees '567' → 3 digits → 3=3×1 ✔ → INSERT
239
+ // … and so on
240
+ // Result → '1,234,567'
241
+ //
242
+ // Works with ANY separator — just change the replacement string:
243
+ // '1234567'.replace(REGEX.THOUSANDS_SEPARATOR_POSITIONS, ',') → '1,234,567'
244
+ // '1234567'.replace(REGEX.THOUSANDS_SEPARATOR_POSITIONS, '.') → '1.234.567'
245
+ // '1234567'.replace(REGEX.THOUSANDS_SEPARATOR_POSITIONS, ' ') → '1 234 567'
246
+ // '1234567'.replace(REGEX.THOUSANDS_SEPARATOR_POSITIONS, '_') → '1_234_567'
247
+ //
248
+ // More examples:
249
+ // '1234' → '1,234'
250
+ // '100' → '100' (fewer than 4 digits — no separator)
251
+ // '1000000' → '1,000,000'
252
+ //
253
+ THOUSANDS_SEPARATOR_POSITIONS: /\B(?=(\d{3})+(?!\d))/g,
254
+ // ──────────────────────────────────────────────────────────────────────────
255
+ // 9. REGEX_SPECIAL_CHARACTERS
256
+ // Escape any character that has a special meaning inside a RegExp pattern.
257
+ // Used before passing a user-supplied string into new RegExp(…).
258
+ // ──────────────────────────────────────────────────────────────────────────
259
+ //
260
+ // Why this is needed:
261
+ // The thousandSeparator input can be '.', which inside a regex means
262
+ // "any character", not a literal dot. Before building
263
+ // new RegExp(thousandSeparator, 'g') we must escape it to '\.'
264
+ // so it matches only a literal period.
265
+ //
266
+ // The replacement string '\\$&' means:
267
+ // \\ – insert a literal backslash
268
+ // $& – followed by the matched character itself
269
+ // So '.' becomes '\.' and '(' becomes '\(' etc.
270
+ //
271
+ // Pattern breakdown (character class contents):
272
+ // . – dot
273
+ // * – asterisk
274
+ // + – plus
275
+ // ? – question mark
276
+ // ^ – caret
277
+ // $ – dollar sign
278
+ // { } – curly braces
279
+ // ( ) – parentheses
280
+ // | – pipe
281
+ // [ – open bracket
282
+ // \] – close bracket (must be escaped to close the class correctly)
283
+ // \\ – backslash (represents a single \)
284
+ // g – global
285
+ //
286
+ // Used in: _stripDecoration(), _stripPatternSeparators()
287
+ // Anywhere a dynamic string is inserted into new RegExp(…)
288
+ //
289
+ // Examples:
290
+ // '.'.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&') → '\\.'
291
+ // ','.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&') → ',' (no change)
292
+ // '('.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&') → '\\('
293
+ // ' '.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&') → ' ' (no change)
294
+ //
295
+ REGEX_SPECIAL_CHARACTERS: /[.*+?^${}()|[\]\\]/g,
296
+ // ──────────────────────────────────────────────────────────────────────────
297
+ // 10. LEADING_ZEROS
298
+ // Remove leading zeros from the integer part, keeping at least one digit.
299
+ // ──────────────────────────────────────────────────────────────────────────
300
+ //
301
+ // Pattern breakdown:
302
+ // ^ – anchor at the start of the string
303
+ // 0+ – one or more '0' characters (the leading zeros to remove)
304
+ // ( – open capture group 1
305
+ // \d – one digit (the first SIGNIFICANT digit after the zeros)
306
+ // ) – close capture group 1
307
+ //
308
+ // No 'g' flag — we only care about the beginning of the string.
309
+ //
310
+ // Replacement '$1':
311
+ // $1 = capture group 1 = the first significant digit.
312
+ // The entire match (zeros + that digit) is replaced by just that digit.
313
+ // Any digits AFTER it in the string are left untouched.
314
+ //
315
+ // Used in: _stripLeadingZeros()
316
+ //
317
+ // Examples:
318
+ // '007'.replace(REGEX.LEADING_ZEROS, '$1') → '7'
319
+ // '0042'.replace(REGEX.LEADING_ZEROS, '$1') → '42'
320
+ // '0.5'.replace(REGEX.LEADING_ZEROS, '$1') → '0.5' (no match — char after 0 is '.')
321
+ // '100'.replace(REGEX.LEADING_ZEROS, '$1') → '100' (no match — first char is '1')
322
+ // '0'.replace(REGEX.LEADING_ZEROS, '$1') → '0' (no match — no digit after the 0)
323
+ //
324
+ LEADING_ZEROS: /^0+(\d)/,
325
+ // ──────────────────────────────────────────────────────────────────────────
326
+ // 11. ARABIC_DIGITS
327
+ // Match Arabic-Indic digit characters (used across Middle East & South Asia).
328
+ // ──────────────────────────────────────────────────────────────────────────
329
+ //
330
+ // Unicode range:
331
+ // \u0660 = Arabic-Indic digit zero ٠
332
+ // \u0669 = Arabic-Indic digit nine ٩
333
+ // [\u0660-\u0669] matches any of the 10 Arabic-Indic digit characters.
334
+ //
335
+ // g flag: replace ALL occurrences in one pass.
336
+ //
337
+ // Used in: convertEasternDigits() in eastern-numerals.ts
338
+ //
339
+ // Background:
340
+ // Arabic keyboards and some IMEs output these instead of 0–9.
341
+ // Unicode offset from Western digit: charCode - 0x0660 + 48 ('0')
342
+ // e.g. '٥'.charCodeAt(0) = 0x0665 → 0x0665 - 0x0660 = 5 → '5'
343
+ //
344
+ // Examples:
345
+ // '١٢٣'.replace(REGEX.ARABIC_DIGITS, …) → '123'
346
+ // '٠٩'.replace(REGEX.ARABIC_DIGITS, …) → '09'
347
+ //
348
+ ARABIC_DIGITS: /[\u0660-\u0669]/g,
349
+ // ──────────────────────────────────────────────────────────────────────────
350
+ // 12. PERSIAN_DIGITS
351
+ // Match Extended Arabic-Indic digit characters used in Persian / Farsi.
352
+ // ──────────────────────────────────────────────────────────────────────────
353
+ //
354
+ // Unicode range:
355
+ // \u06F0 = Extended Arabic-Indic digit zero ۰
356
+ // \u06F9 = Extended Arabic-Indic digit nine ۹
357
+ //
358
+ // Persian digits are a DIFFERENT Unicode block from Arabic digits even though
359
+ // they look similar:
360
+ // Arabic zero ٠ = U+0660
361
+ // Persian zero ۰ = U+06F0 ← different code point!
362
+ //
363
+ // Both blocks need separate patterns.
364
+ //
365
+ // g flag: replace ALL occurrences.
366
+ //
367
+ // Used in: convertEasternDigits() in eastern-numerals.ts
368
+ //
369
+ // Examples:
370
+ // '۱۲۳'.replace(REGEX.PERSIAN_DIGITS, …) → '123'
371
+ // '۰۹'.replace(REGEX.PERSIAN_DIGITS, …) → '09'
372
+ //
373
+ PERSIAN_DIGITS: /[\u06F0-\u06F9]/g,
374
+ // ──────────────────────────────────────────────────────────────────────────
375
+ // 13. ARABIC_DECIMAL_COMMA
376
+ // Match the Arabic decimal comma character ٫ (U+066B).
377
+ // ──────────────────────────────────────────────────────────────────────────
378
+ //
379
+ // In Arabic locale, the decimal separator is ٫ (Arabic Decimal Separator)
380
+ // not the Western period '.'. We convert it to '.' so the rest of the
381
+ // directive can process it uniformly.
382
+ //
383
+ // Used in: convertEasternDigits() in eastern-numerals.ts
384
+ //
385
+ // Example:
386
+ // '١٢٫٥'.replace(REGEX.ARABIC_DECIMAL_COMMA, '.') → '١٢.٥'
387
+ // (then after digit conversion) → '12.5'
388
+ //
389
+ ARABIC_DECIMAL_COMMA: /\u066B/g,
390
+ // ──────────────────────────────────────────────────────────────────────────
391
+ // 14. ARABIC_THOUSANDS_SEPARATOR
392
+ // Match the Arabic thousands separator ٬ (U+066C).
393
+ // ──────────────────────────────────────────────────────────────────────────
394
+ //
395
+ // Similar to the decimal comma above: the Arabic locale uses U+066C as its
396
+ // thousands grouping character. We strip it so the directive does not
397
+ // mistake it for a digit.
398
+ //
399
+ // Used in: convertEasternDigits() in eastern-numerals.ts
400
+ //
401
+ // Example:
402
+ // '١٬٢٣٤'.replace(REGEX.ARABIC_THOUSANDS_SEPARATOR, '') → '١٢٣٤'
403
+ //
404
+ ARABIC_THOUSANDS_SEPARATOR: /\u066C/g,
405
+ // ──────────────────────────────────────────────────────────────────────────
406
+ // 15. ARABIC_DIGIT_RANGE
407
+ // The Unicode code-point boundaries for Arabic-Indic digit characters.
408
+ // ──────────────────────────────────────────────────────────────────────────
409
+ //
410
+ // Unlike ARABIC_DIGITS (which is a /g regex used with .replace()), these
411
+ // are plain string constants used for a boundary COMPARISON in onKeyDown():
412
+ //
413
+ // pressedKey >= REGEX.ARABIC_DIGIT_RANGE.FIRST &&
414
+ // pressedKey <= REGEX.ARABIC_DIGIT_RANGE.LAST
415
+ //
416
+ // Why not reuse ARABIC_DIGITS here?
417
+ // ARABIC_DIGITS has the global 'g' flag and is designed for .replace()
418
+ // across an entire string. A single-character range check via >= / <=
419
+ // is simpler, has no flag state to worry about, and is O(1).
420
+ // JavaScript compares strings by Unicode code point, so this works
421
+ // correctly for single characters.
422
+ //
423
+ // Used in: onKeyDown() — to allow Eastern keystrokes through before
424
+ // onInput() converts them to Western digits.
425
+ //
426
+ // Examples:
427
+ // '١' >= REGEX.ARABIC_DIGIT_RANGE.FIRST → true (U+0661 >= U+0660)
428
+ // '١' <= REGEX.ARABIC_DIGIT_RANGE.LAST → true (U+0661 <= U+0669)
429
+ // 'a' >= REGEX.ARABIC_DIGIT_RANGE.FIRST → false (U+0061 < U+0660)
430
+ //
431
+ ARABIC_DIGIT_RANGE: {
432
+ FIRST: '\u0660', // ٠ Arabic-Indic digit ZERO
433
+ LAST: '\u0669', // ٩ Arabic-Indic digit NINE
434
+ },
435
+ // ──────────────────────────────────────────────────────────────────────────
436
+ // 16. PERSIAN_DIGIT_RANGE
437
+ // The Unicode code-point boundaries for Persian digit characters.
438
+ // ──────────────────────────────────────────────────────────────────────────
439
+ //
440
+ // Same idea as ARABIC_DIGIT_RANGE above, but for the Persian/Extended
441
+ // Arabic-Indic block (U+06F0–U+06F9).
442
+ //
443
+ // Persian digits live in a DIFFERENT Unicode block from Arabic digits
444
+ // even though they look similar:
445
+ // Arabic zero ٠ = U+0660
446
+ // Persian zero ۰ = U+06F0 ← different block, needs its own range
447
+ //
448
+ // Used in: onKeyDown() — same purpose as ARABIC_DIGIT_RANGE.
449
+ //
450
+ // Examples:
451
+ // '۶' >= REGEX.PERSIAN_DIGIT_RANGE.FIRST → true (U+06F6 >= U+06F0)
452
+ // '۶' <= REGEX.PERSIAN_DIGIT_RANGE.LAST → true (U+06F6 <= U+06F9)
453
+ // '١' >= REGEX.PERSIAN_DIGIT_RANGE.FIRST → false (U+0661 < U+06F0)
454
+ //
455
+ PERSIAN_DIGIT_RANGE: {
456
+ FIRST: '\u06F0', // ۰ Persian digit ZERO
457
+ LAST: '\u06F9', // ۹ Persian digit NINE
458
+ },
459
+ };
460
+ // ─────────────────────────────────────────────────────────────────────────────
461
+ // QUICK-REFERENCE TABLE
462
+ // ─────────────────────────────────────────────────────────────────────────────
463
+ //
464
+ // Key Pattern Purpose
465
+ // ────────────────────────── ───────────────────────────── ────────────────────────────────────────
466
+ // SINGLE_DIGIT /^\d$/ Is a keypress exactly one digit?
467
+ // NON_DIGIT_CHARACTERS /[^0-9]/g Strip everything except digits
468
+ // ONLY_DIGITS_AND_MINUS /[^0-9\-]/g Keep digits + minus (integer paste)
469
+ // ONLY_DIGITS_DOT_AND_MINUS /[^0-9.\-]/g Keep digits + dot + minus (decimal paste)
470
+ // ONLY_DIGITS /[^0-9]/g Keep digits only (string/pattern paste)
471
+ // ALL_DOTS /\./g Remove all literal dot characters
472
+ // ALL_MINUS_SIGNS /-/g Remove all minus/hyphen characters
473
+ // THOUSANDS_SEPARATOR_POSITIONS /\B(?=(\d{3})+(?!\d))/g Find positions for thousands separator
474
+ // REGEX_SPECIAL_CHARACTERS /[.*+?^${}()|[\]\\]/g Escape chars before new RegExp(…)
475
+ // LEADING_ZEROS /^0+(\d)/ Strip leading zeros, keep ≥ 1 digit
476
+ // ARABIC_DIGITS /[\u0660-\u0669]/g Match Arabic-Indic digits ٠-٩
477
+ // PERSIAN_DIGITS /[\u06F0-\u06F9]/g Match Persian digits ۰-۹
478
+ // ARABIC_DECIMAL_COMMA /\u066B/g Arabic decimal separator ٫ → '.'
479
+ // ARABIC_THOUSANDS_SEPARATOR /\u066C/g Arabic grouping separator ٬ → strip
480
+ // ARABIC_DIGIT_RANGE { FIRST: '\u0660', LAST: '\u0669' } Boundary check for Arabic digits in keydown
481
+ // PERSIAN_DIGIT_RANGE { FIRST: '\u06F0', LAST: '\u06F9' } Boundary check for Persian digits in keydown
482
+
483
+ /**
484
+ * eastern-numerals.ts
485
+ *
486
+ * A fully standalone, zero-dependency TypeScript utility that converts
487
+ * Arabic-Indic and Persian/Farsi digit characters to their Western (ASCII)
488
+ * equivalents.
489
+ *
490
+ * ─── Zero dependencies ────────────────────────────────────────────────────────
491
+ *
492
+ * This file has NO imports. Copy it into ANY TypeScript or JavaScript project
493
+ * and use it immediately — Angular, React, Vue, Node.js, plain TS, or a browser
494
+ * script. Nothing else is required.
495
+ *
496
+ * ─── Why this exists ──────────────────────────────────────────────────────────
497
+ *
498
+ * Arabic and Persian keyboards — and many mobile IMEs across the Middle East
499
+ * and South Asia — produce digit characters from a DIFFERENT Unicode block
500
+ * than the ASCII digits 0–9 most Western software expects:
501
+ *
502
+ * Western / ASCII 0 1 2 3 4 5 6 7 8 9
503
+ * Code points U+0030 ── U+0039
504
+ *
505
+ * Arabic-Indic ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩
506
+ * Code points U+0660 ── U+0669
507
+ *
508
+ * Persian / Farsi ۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹
509
+ * Code points U+06F0 ── U+06F9
510
+ *
511
+ * When a user types ١٢٣ on an Arabic keyboard the browser sends those exact
512
+ * Unicode code points, NOT '123'. Without conversion:
513
+ * • /^\d$/ does NOT match them (it only matches U+0030–U+0039)
514
+ * • Number('١٢٣') returns NaN
515
+ * • parseInt('١٢٣', 10) returns NaN
516
+ *
517
+ * ─── Safety guarantee ─────────────────────────────────────────────────────────
518
+ *
519
+ * This conversion is completely conflict-free because the Arabic and Persian
520
+ * digit Unicode blocks have ZERO overlap with:
521
+ * • ASCII digits (U+0030–U+0039)
522
+ * • ASCII letters (U+0041–U+007A)
523
+ * • ASCII punctuation (. , - + $ € % @ # etc.)
524
+ *
525
+ * Converting ٥ → '5' can NEVER accidentally transform a separator, currency
526
+ * symbol, or letter into something else. The substitution is exact and
527
+ * unambiguous.
528
+ *
529
+ * ─── What is NOT converted ────────────────────────────────────────────────────
530
+ *
531
+ * • Arabic letters ا ب ت ث … — no Western equivalent, left as-is
532
+ * • Arabic diacritics (harakat) — no Western equivalent, left as-is
533
+ * • Any character outside the two digit blocks described above
534
+ *
535
+ * ─── Exports ──────────────────────────────────────────────────────────────────
536
+ *
537
+ * convertEasternDigits(input) Main conversion — Eastern digits → 0-9
538
+ * hasEasternDigits(input) Quick check — does the string contain any?
539
+ * getDigitScript(input) Identify which script the digits are in
540
+ * EASTERN_DIGIT_MAP Full lookup table (for testing / docs)
541
+ * EASTERN_NUMERAL_REGEX The two RegExp patterns (for advanced use)
542
+ *
543
+ * ─── Quick usage ──────────────────────────────────────────────────────────────
544
+ *
545
+ * import { convertEasternDigits } from './eastern-numerals';
546
+ *
547
+ * convertEasternDigits('١٢٣٫٤٥') // '123.45'
548
+ * convertEasternDigits('۶۷۸') // '678'
549
+ * convertEasternDigits('price: ١٠٠') // 'price: 100' (non-digits untouched)
550
+ * convertEasternDigits('123') // '123' (already Western)
551
+ * convertEasternDigits(null) // ''
552
+ */
553
+ // ─────────────────────────────────────────────────────────────────────────────
554
+ // Internal regex constants
555
+ // (Inline here so the file has zero dependencies and can be used anywhere.)
556
+ // ─────────────────────────────────────────────────────────────────────────────
557
+ /**
558
+ * Regex patterns used internally by this module.
559
+ * Also exported so callers can use them directly if needed.
560
+ *
561
+ * These are the same patterns documented in digits-only.regex.ts entries
562
+ * ARABIC_DIGITS, PERSIAN_DIGITS, ARABIC_DECIMAL_COMMA, ARABIC_THOUSANDS_SEPARATOR
563
+ * but duplicated here so this file remains self-contained.
564
+ */
565
+ const EASTERN_NUMERAL_REGEX = {
566
+ /**
567
+ * Matches any Arabic-Indic digit character (U+0660 – U+0669).
568
+ * These are the digits used on Arabic keyboards: ٠١٢٣٤٥٦٧٨٩
569
+ * The g flag ensures ALL occurrences in a string are replaced, not just the first.
570
+ */
571
+ ARABIC_DIGITS: /[\u0660-\u0669]/g,
572
+ /**
573
+ * Matches any Persian / Extended Arabic-Indic digit (U+06F0 – U+06F9).
574
+ * These are the digits used on Persian / Farsi keyboards: ۰۱۲۳۴۵۶۷۸۹
575
+ * Note: a DIFFERENT Unicode block from Arabic even though they look similar.
576
+ * Arabic zero ٠ = U+0660
577
+ * Persian zero ۰ = U+06F0 ← different!
578
+ */
579
+ PERSIAN_DIGITS: /[\u06F0-\u06F9]/g,
580
+ /**
581
+ * Matches the Arabic Decimal Separator ٫ (U+066B).
582
+ * In Arabic locale this character plays the role of the Western period '.'
583
+ * as a decimal point. We normalize it to '.' so parseFloat / Number work.
584
+ */
585
+ ARABIC_DECIMAL_COMMA: /\u066B/g,
586
+ /**
587
+ * Matches the Arabic Thousands Separator ٬ (U+066C).
588
+ * In Arabic locale this character plays the role of the Western comma ','
589
+ * as a digit-group separator. We strip it so it doesn't pollute the number.
590
+ */
591
+ ARABIC_THOUSANDS_SEPARATOR: /\u066C/g,
592
+ };
593
+ // ─────────────────────────────────────────────────────────────────────────────
594
+ // Full Unicode character map (exported for testing and documentation)
595
+ // ─────────────────────────────────────────────────────────────────────────────
596
+ /**
597
+ * Every Eastern digit character mapped to its Western ASCII equivalent.
598
+ *
599
+ * Key = Eastern character (as it arrives from the keyboard / clipboard)
600
+ * Value = Western ASCII character it should become
601
+ *
602
+ * This map is exported for:
603
+ * • Unit tests that want to iterate over every case
604
+ * • Documentation / debugging
605
+ * • Manual lookups without running a conversion
606
+ */
607
+ const EASTERN_DIGIT_MAP = {
608
+ // ── Arabic-Indic digits (U+0660 – U+0669) ────────────────────────────────
609
+ // Used in: Arabic, Urdu, Sindhi, and other Arabic-script languages
610
+ '٠': '0', // U+0660 Arabic-Indic digit zero
611
+ '١': '1', // U+0661 Arabic-Indic digit one
612
+ '٢': '2', // U+0662 Arabic-Indic digit two
613
+ '٣': '3', // U+0663 Arabic-Indic digit three
614
+ '٤': '4', // U+0664 Arabic-Indic digit four
615
+ '٥': '5', // U+0665 Arabic-Indic digit five
616
+ '٦': '6', // U+0666 Arabic-Indic digit six
617
+ '٧': '7', // U+0667 Arabic-Indic digit seven
618
+ '٨': '8', // U+0668 Arabic-Indic digit eight
619
+ '٩': '9', // U+0669 Arabic-Indic digit nine
620
+ // ── Persian / Extended Arabic-Indic digits (U+06F0 – U+06F9) ─────────────
621
+ // Used in: Persian (Farsi), Pashto, Kurdish, and related languages
622
+ '۰': '0', // U+06F0 Extended Arabic-Indic digit zero
623
+ '۱': '1', // U+06F1 Extended Arabic-Indic digit one
624
+ '۲': '2', // U+06F2 Extended Arabic-Indic digit two
625
+ '۳': '3', // U+06F3 Extended Arabic-Indic digit three
626
+ '۴': '4', // U+06F4 Extended Arabic-Indic digit four
627
+ '۵': '5', // U+06F5 Extended Arabic-Indic digit five
628
+ '۶': '6', // U+06F6 Extended Arabic-Indic digit six
629
+ '۷': '7', // U+06F7 Extended Arabic-Indic digit seven
630
+ '۸': '8', // U+06F8 Extended Arabic-Indic digit eight
631
+ '۹': '9', // U+06F9 Extended Arabic-Indic digit nine
632
+ // ── Arabic punctuation normalized as part of number handling ─────────────
633
+ '٫': '.', // U+066B Arabic Decimal Separator → Western decimal point
634
+ '٬': '', // U+066C Arabic Thousands Separator → stripped (empty string)
635
+ };
636
+ // ─────────────────────────────────────────────────────────────────────────────
637
+ // Core conversion function
638
+ // ─────────────────────────────────────────────────────────────────────────────
639
+ /**
640
+ * Convert all Arabic-Indic and Persian digit characters in a string
641
+ * to their Western ASCII equivalents (0–9).
642
+ *
643
+ * The function runs four passes in order:
644
+ * 1. Strip Arabic thousands separator ٬ (U+066C)
645
+ * 2. Convert Arabic decimal comma ٫ (U+066B) → '.'
646
+ * 3. Convert Arabic-Indic digits ٠-٩ (U+0660–U+0669) → '0'-'9'
647
+ * 4. Convert Persian digits ۰-۹ (U+06F0–U+06F9) → '0'-'9'
648
+ *
649
+ * The function is PURE — it never mutates its input and has no side effects.
650
+ *
651
+ * @param input Any string, number, null, or undefined.
652
+ * Numbers are converted via String() first.
653
+ * null / undefined / '' all return ''.
654
+ * @returns A new string with every Eastern digit replaced by its Western
655
+ * equivalent. Non-digit characters are passed through unchanged.
656
+ *
657
+ * @example
658
+ * convertEasternDigits('١٢٣') // '123'
659
+ * convertEasternDigits('۶۷۸') // '678'
660
+ * convertEasternDigits('١٢٣٫٤٥') // '123.45' (decimal comma)
661
+ * convertEasternDigits('١٬٢٣٤') // '1234' (thousands sep stripped)
662
+ * convertEasternDigits('price: ٩٩') // 'price: 99' (non-digits untouched)
663
+ * convertEasternDigits('123') // '123' (already Western)
664
+ * convertEasternDigits('') // ''
665
+ * convertEasternDigits(null) // ''
666
+ * convertEasternDigits(undefined) // ''
667
+ * convertEasternDigits(42) // '42' (number input)
668
+ */
669
+ function convertEasternDigits(input) {
670
+ // ── Guard ─────────────────────────────────────────────────────────────────
671
+ // Handle null, undefined, and empty-string early so the rest of the
672
+ // function can assume it has a non-empty string to work with.
673
+ if (input === null || input === undefined || input === '') {
674
+ return '';
675
+ }
676
+ // Coerce numbers to strings so the function also works when called
677
+ // directly on a numeric value (e.g. after a model value update).
678
+ const text = typeof input === 'number' ? String(input) : input;
679
+ return text
680
+ // ── Step 1: Remove Arabic thousands separator ──────────────────────────
681
+ //
682
+ // ٬ (U+066C) looks like a comma but is a completely different code point.
683
+ // It must be stripped FIRST — before digit conversion — so it cannot
684
+ // accidentally become part of the number string.
685
+ //
686
+ // '١٬٢٣٤' → '١٢٣٤' (grouping character removed)
687
+ .replace(EASTERN_NUMERAL_REGEX.ARABIC_THOUSANDS_SEPARATOR, '')
688
+ // ── Step 2: Arabic decimal comma → Western decimal point ──────────────
689
+ //
690
+ // ٫ (U+066B) is the decimal separator in Arabic locale.
691
+ // We convert it to '.' so parseFloat / Number can parse the result.
692
+ //
693
+ // '١٢٫٥' → '١٢.٥' (decimal comma replaced, digits still Arabic)
694
+ .replace(EASTERN_NUMERAL_REGEX.ARABIC_DECIMAL_COMMA, '.')
695
+ // ── Step 3: Arabic-Indic digits → Western digits ───────────────────────
696
+ //
697
+ // The conversion formula uses the Unicode offset of the Arabic zero (0x0660):
698
+ //
699
+ // Example with '٥':
700
+ // '٥'.charCodeAt(0) = 0x0665 (1637 decimal)
701
+ // 0x0665 - 0x0660 = 5 (the digit's numeric value)
702
+ // 5 + 0x30 = 53 (ASCII code of '5')
703
+ // String.fromCharCode(53) = '5'
704
+ //
705
+ // '٠١٢٣٤٥٦٧٨٩' → '0123456789'
706
+ .replace(EASTERN_NUMERAL_REGEX.ARABIC_DIGITS, (arabicChar) => {
707
+ const ARABIC_ZERO_CODE_POINT = 0x0660; // Unicode code point of Arabic ٠
708
+ const WESTERN_ZERO_CODE_POINT = 0x0030; // Unicode code point of ASCII 0
709
+ const digitValue = arabicChar.charCodeAt(0) - ARABIC_ZERO_CODE_POINT;
710
+ return String.fromCharCode(digitValue + WESTERN_ZERO_CODE_POINT);
711
+ })
712
+ // ── Step 4: Persian digits → Western digits ────────────────────────────
713
+ //
714
+ // Identical math to step 3 but using the Persian zero offset (0x06F0).
715
+ // Persian and Arabic digits look similar but are different code points —
716
+ // both blocks need their own replacement pass.
717
+ //
718
+ // '۰۱۲۳۴۵۶۷۸۹' → '0123456789'
719
+ .replace(EASTERN_NUMERAL_REGEX.PERSIAN_DIGITS, (persianChar) => {
720
+ const PERSIAN_ZERO_CODE_POINT = 0x06F0; // Unicode code point of Persian ۰
721
+ const WESTERN_ZERO_CODE_POINT = 0x0030; // Unicode code point of ASCII 0
722
+ const digitValue = persianChar.charCodeAt(0) - PERSIAN_ZERO_CODE_POINT;
723
+ return String.fromCharCode(digitValue + WESTERN_ZERO_CODE_POINT);
724
+ });
725
+ }
726
+ // ─────────────────────────────────────────────────────────────────────────────
727
+ // Helper utilities
728
+ // ─────────────────────────────────────────────────────────────────────────────
729
+ /**
730
+ * Returns true if the string contains at least one Arabic-Indic or
731
+ * Persian digit character.
732
+ *
733
+ * Useful as a cheap pre-check before calling convertEasternDigits — if this
734
+ * returns false the string is already all-Western and no conversion is needed.
735
+ *
736
+ * @param input Any string to check.
737
+ * @returns true if any Eastern digit is present, false otherwise.
738
+ *
739
+ * @example
740
+ * hasEasternDigits('١٢٣') // true — Arabic
741
+ * hasEasternDigits('۴۵۶') // true — Persian
742
+ * hasEasternDigits('٥0') // true — mix of Arabic and Western
743
+ * hasEasternDigits('123') // false — already Western
744
+ * hasEasternDigits('hello') // false — no digits at all
745
+ * hasEasternDigits('') // false
746
+ */
747
+ function hasEasternDigits(input) {
748
+ if (!input)
749
+ return false;
750
+ // We must reset lastIndex before using a /g regex with .test()
751
+ // because stateful regex objects remember where they left off.
752
+ // The simplest safe approach is to re-create a clean regex each time.
753
+ return /[\u0660-\u0669\u06F0-\u06F9]/.test(input);
754
+ }
755
+ function getDigitScript(input) {
756
+ if (!input)
757
+ return 'western';
758
+ const containsArabic = /[\u0660-\u0669]/.test(input);
759
+ const containsPersian = /[\u06F0-\u06F9]/.test(input);
760
+ const containsWestern = /[0-9]/.test(input);
761
+ // Mixed: more than one script present simultaneously
762
+ const scriptCount = [containsArabic, containsPersian, containsWestern]
763
+ .filter(Boolean).length;
764
+ if (scriptCount > 1)
765
+ return 'mixed';
766
+ if (containsArabic)
767
+ return 'arabic';
768
+ if (containsPersian)
769
+ return 'persian';
770
+ return 'western';
771
+ }
772
+ /**
773
+ * Convert a string that may contain Eastern digits to a JavaScript number.
774
+ * A convenience wrapper around convertEasternDigits + parseFloat.
775
+ *
776
+ * Returns NaN for strings that don't represent a valid number
777
+ * (same contract as parseFloat / Number).
778
+ *
779
+ * @param input A string possibly containing Eastern digits.
780
+ * @returns A JavaScript number, or NaN if parsing fails.
781
+ *
782
+ * @example
783
+ * toWesternNumber('١٢٣') // 123
784
+ * toWesternNumber('١٢٣٫٤٥') // 123.45
785
+ * toWesternNumber('۹٩') // 99 (mixed Arabic + Persian → still works)
786
+ * toWesternNumber('abc') // NaN
787
+ * toWesternNumber('') // NaN
788
+ */
789
+ function toWesternNumber(input) {
790
+ const converted = convertEasternDigits(input);
791
+ return parseFloat(converted);
792
+ }
793
+
794
+ /**
795
+ * digits-only.directive.ts
796
+ *
797
+ * A self-contained Angular directive that replicates the key numeric features
798
+ * of ngx-mask without any external dependency.
799
+ *
800
+ * ─── Files in this module ─────────────────────────────────────────────────────
801
+ *
802
+ * digits-only.directive.ts ← you are here — the Angular directive
803
+ * digits-only.regex.ts ← every RegExp constant with detailed explanations
804
+ * digits-only.module.ts ← NgModule that declares and exports the directive
805
+ * eastern-numerals.ts ← standalone Arabic / Persian → Western digit converter
806
+ *
807
+ * ─── Features ─────────────────────────────────────────────────────────────────
808
+ *
809
+ * [digitsOnly] Selector — add to any <input> element
810
+ * [decimalPlaces] Number of decimal places allowed (default: 0 = integers)
811
+ * [thousandSeparator] Visual grouping character: ',' | '.' | ' ' | '_' | ''
812
+ * [prefix] Visual prefix shown in input (e.g. '$') — stripped from model
813
+ * [suffix] Visual suffix shown in input (e.g. '%') — stripped from model
814
+ * [allowNegative] Allow a leading minus sign (default: false)
815
+ * [leadingZeros] Preserve leading zeros like '007' (default: false)
816
+ * [maxLength] Maximum number of raw digits allowed
817
+ * [min] Minimum numeric value (validation, number mode only)
818
+ * [max] Maximum numeric value (validation, number mode only)
819
+ * [outputType] 'number' (default) | 'string' — type emitted to the model
820
+ * [pattern] Display-format mask: '0000 0000 0000 0000' or named alias
821
+ * [convertEasternNumerals] Convert Arabic/Persian digits to Western on input (default: true)
822
+ *
823
+ * ─── outputType ───────────────────────────────────────────────────────────────
824
+ *
825
+ * 'number' (default)
826
+ * Model receives: number | null
827
+ * Use for: prices, quantities, temperatures — values used in arithmetic
828
+ * Note: Leading zeros are lost (007 → 7); values beyond
829
+ * Number.MAX_SAFE_INTEGER may lose precision
830
+ *
831
+ * 'string'
832
+ * Model receives: string | null
833
+ * Use for: card numbers, phone numbers, postal codes, SSNs, OTPs —
834
+ * digit strings that are identifiers, not quantities
835
+ * Note: Leading zeros are always preserved; no precision limit
836
+ *
837
+ * ─── pattern ──────────────────────────────────────────────────────────────────
838
+ *
839
+ * A mask string made of '0' (digit slot) and any other character (separator).
840
+ * Separators are inserted in the display automatically but NEVER reach the model.
841
+ * Setting a pattern forces outputType='string' and derives maxLength automatically.
842
+ *
843
+ * Named aliases:
844
+ * 'card' → '0000 0000 0000 0000' Visa / Mastercard (16 digits)
845
+ *
846
+ * ─── convertEasternNumerals ───────────────────────────────────────────────────
847
+ *
848
+ * When true (default), Arabic-Indic (٠١٢٣٤٥٦٧٨٩) and Persian (۰۱۲۳۴۵۶۷۸۹)
849
+ * digit characters are silently converted to their Western equivalents before
850
+ * any processing. This allows users on Arabic / Persian keyboards to type
851
+ * naturally into any digitsOnly field.
852
+ * Set to false only if you specifically need to block Eastern numerals.
853
+ *
854
+ * ─── Usage examples ───────────────────────────────────────────────────────────
855
+ *
856
+ * <!-- Integer amount, $ prefix, comma thousands -->
857
+ * <input digitsOnly thousandSeparator="," prefix="$" formControlName="salary" />
858
+ *
859
+ * <!-- Decimal price, 2 places -->
860
+ * <input digitsOnly [decimalPlaces]="2" thousandSeparator="," formControlName="price" />
861
+ *
862
+ * <!-- Card number with automatic space grouping -->
863
+ * <input digitsOnly pattern="card" formControlName="cardNumber" />
864
+ *
865
+ * <!-- Custom pattern -->
866
+ * <input digitsOnly pattern="000-00-0000" formControlName="ssn" />
867
+ *
868
+ * <!-- Opt out of Eastern numeral conversion -->
869
+ * <input digitsOnly [convertEasternNumerals]="false" formControlName="code" />
870
+ */
871
+ // ─────────────────────────────────────────────────────────────────────────────
872
+ // Unicode directional constants
873
+ // ─────────────────────────────────────────────────────────────────────────────
874
+ /**
875
+ * Left-to-Right Mark (U+200E) — an invisible zero-width Unicode character.
876
+ *
877
+ * WHY IT IS NEEDED:
878
+ * In RTL documents (dir="rtl"), the browser's Unicode Bidirectional Algorithm
879
+ * (UBA) may try to mirror numeric content — reordering digits or misplacing
880
+ * separators like commas. For example, "1,234" can display as "234,1" in
881
+ * some bidi contexts.
882
+ *
883
+ * Prepending LRM to the input value tells the bidi algorithm:
884
+ * "treat everything that follows as left-to-right text".
885
+ * Numbers, thousands separators, and decimal points always read correctly.
886
+ *
887
+ * ZERO IMPACT ON MODEL:
888
+ * LRM is stripped inside stripDecorationsFrom() before any value reaches
889
+ * the model, so your component never sees this character.
890
+ *
891
+ * BROWSER SUPPORT:
892
+ * All modern browsers respect U+200E in input values.
893
+ */
894
+ const LRM = '\u200E'; // Left-to-Right Mark — invisible, prevents bidi digit mirroring
895
+ /**
896
+ * Maps every NamedPattern alias to its full mask string.
897
+ * Typed as Record<NamedPattern, string> so TypeScript catches missing entries —
898
+ * add a new entry here and the type forces you to add it to NamedPattern too.
899
+ */
900
+ const NAMED_PATTERNS = {
901
+ 'card': '0000 0000 0000 0000',
902
+ };
903
+ /**
904
+ * Type guard that narrows an arbitrary string to NamedPattern.
905
+ *
906
+ * Using the `in` operator against NAMED_PATTERNS tells TypeScript:
907
+ * "if this key exists in the object, treat it as NamedPattern from here on".
908
+ * This is the only way to safely index Record<NamedPattern, string> with a
909
+ * value that might be a NamedPattern OR a plain custom pattern string.
910
+ *
911
+ * @example
912
+ * isNamedPattern('card') // true → safe to use as NAMED_PATTERNS key
913
+ * isNamedPattern('000-0000') // false → treat as raw custom pattern
914
+ */
915
+ function isNamedPattern(value) {
916
+ return value in NAMED_PATTERNS;
917
+ }
918
+ // ─────────────────────────────────────────────────────────────────────────────
919
+ // Directive
920
+ // ─────────────────────────────────────────────────────────────────────────────
921
+ class digitsOnlyDirective {
922
+ el;
923
+ renderer;
924
+ document;
925
+ // ─── Public inputs ──────────────────────────────────────────────────────────
926
+ /** How many decimal places are allowed. 0 = integers only. Ignored when pattern is set. */
927
+ decimalPlaces = 0;
928
+ /** Visual grouping character inserted between digit groups. Ignored when pattern is set. */
929
+ thousandSeparator = '';
930
+ /** Visual text prepended to the displayed value (e.g. '$'). Never reaches the model. */
931
+ prefix = '';
932
+ /** Visual text appended to the displayed value (e.g. '%'). Never reaches the model. */
933
+ suffix = '';
934
+ /** When true, a leading minus '-' is allowed for negative numbers. */
935
+ allowNegative = false;
936
+ /** When true, leading zeros (e.g. '007') are preserved in the model value. */
937
+ leadingZeros = false;
938
+ /** Maximum number of raw digit characters the user may enter. */
939
+ maxLength = null;
940
+ /** Minimum allowed numeric value. Produces a 'min' validation error when violated. */
941
+ min = null;
942
+ /** Maximum allowed numeric value. Produces a 'max' validation error when violated. */
943
+ max = null;
944
+ /**
945
+ * Controls the JavaScript type emitted to the form model.
946
+ * 'number' (default) → number | null
947
+ * 'string' → string | null
948
+ * Automatically overridden to 'string' when [pattern] is set.
949
+ */
950
+ outputType = 'number';
951
+ /**
952
+ * When true (default), Arabic-Indic and Persian digit characters typed or
953
+ * pasted by the user are automatically converted to Western digits 0-9
954
+ * before any processing happens.
955
+ *
956
+ * Safe to leave enabled for all use cases — the conversion only touches
957
+ * characters in the Unicode ranges U+0660–U+0669 and U+06F0–U+06F9,
958
+ * which have zero overlap with any ASCII character.
959
+ */
960
+ convertEasternNumerals = true;
961
+ /**
962
+ * Display-format mask string or a named alias (see NamedPattern type).
963
+ *
964
+ * Typed as NamedPattern | (string & {}) so that:
965
+ * • Named aliases are autocompleted in IDEs and validated at compile time.
966
+ * • Custom raw patterns like '000-0000' are still accepted without error.
967
+ *
968
+ * '0' = digit slot, any other char = literal separator auto-inserted in display.
969
+ * Setting this forces outputType='string' and derives maxLength from slot count.
970
+ *
971
+ * @example
972
+ * pattern="card" → '0000 0000 0000 0000'
973
+ * pattern="00/00/0000" → custom date pattern
974
+ */
975
+ set pattern(value) {
976
+ // Type guard: check whether the value is one of the known NamedPattern keys
977
+ // before indexing NAMED_PATTERNS with it.
978
+ //
979
+ // Why this is needed:
980
+ // NAMED_PATTERNS is typed as Record<NamedPattern, string>, meaning its
981
+ // index signature only accepts NamedPattern keys — not arbitrary strings.
982
+ // The @Input type is NamedPattern | (string & {}), so `value` could be
983
+ // a custom raw pattern like '000-0000' that is NOT a key of NAMED_PATTERNS.
984
+ // Indexing with it directly causes TS error ts(7053):
985
+ // "No index signature with a parameter of type 'string' was found".
986
+ //
987
+ // The fix: use `value in NAMED_PATTERNS` as a type guard. The `in` operator
988
+ // narrows the type of `value` to NamedPattern inside the if-branch, making
989
+ // NAMED_PATTERNS[value] type-safe. In the else-branch `value` is treated
990
+ // as a raw custom pattern string and used directly.
991
+ if (isNamedPattern(value)) {
992
+ this._resolvedPattern = NAMED_PATTERNS[value]; // value is NamedPattern
993
+ }
994
+ else {
995
+ this._resolvedPattern = value; // value is a raw custom pattern
996
+ }
997
+ }
998
+ // ─── Internal state ──────────────────────────────────────────────────────────
999
+ /** Full pattern string after named-alias resolution. Empty string = no pattern. */
1000
+ _resolvedPattern = '';
1001
+ /**
1002
+ * The canonical internal value: pure digits (and optional '.' and '-').
1003
+ * No prefix, suffix, separators, or pattern characters ever live here.
1004
+ * Every other method reads or writes this field.
1005
+ */
1006
+ _rawValue = '';
1007
+ // ControlValueAccessor callbacks registered by Angular forms
1008
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1009
+ _onChange = () => { };
1010
+ _onTouched = () => { };
1011
+ _onValidatorChange = () => { };
1012
+ // ─── Computed properties ────────────────────────────────────────────────────
1013
+ /** True when a format pattern has been set on this input. */
1014
+ get hasPattern() {
1015
+ return this._resolvedPattern.length > 0;
1016
+ }
1017
+ /**
1018
+ * The set of literal separator characters defined in the current pattern.
1019
+ *
1020
+ * Pattern '(000) 000-0000' → separators: {'(', ')', ' ', '-'}
1021
+ * Pattern '0000 0000' → separators: {' '}
1022
+ *
1023
+ * Used when stripping decorations back off a displayed value.
1024
+ */
1025
+ get patternSeparatorChars() {
1026
+ return new Set(this._resolvedPattern.split('').filter(char => char !== '0'));
1027
+ }
1028
+ /**
1029
+ * The total number of digit slots ('0' characters) in the current pattern.
1030
+ * 'card' pattern has 16 slots; 'expiry' has 4; etc.
1031
+ */
1032
+ get patternDigitSlotCount() {
1033
+ return this._resolvedPattern.split('').filter(char => char === '0').length;
1034
+ }
1035
+ /**
1036
+ * The effective maximum digit length to enforce.
1037
+ * In pattern mode, this is always the slot count (overrides any [maxLength] input).
1038
+ * In non-pattern mode, it comes from the [maxLength] input (may be null = unlimited).
1039
+ */
1040
+ get effectiveMaxLength() {
1041
+ return this.hasPattern ? this.patternDigitSlotCount : this.maxLength;
1042
+ }
1043
+ /**
1044
+ * The effective output type.
1045
+ * Pattern mode always emits strings because patterns are for identifiers.
1046
+ * Otherwise respects the [outputType] input.
1047
+ */
1048
+ get effectiveOutputType() {
1049
+ return this.hasPattern ? 'string' : this.outputType;
1050
+ }
1051
+ /**
1052
+ * The decimal separator character used in the DISPLAY.
1053
+ * If the thousands separator is '.' (European style), we must use ','
1054
+ * as the decimal character to avoid ambiguity (1.234,56 is valid; 1,234,56 is not).
1055
+ * Otherwise we use the standard '.'.
1056
+ */
1057
+ get displayDecimalChar() {
1058
+ return this.thousandSeparator === '.' ? ',' : '.';
1059
+ }
1060
+ /**
1061
+ * Returns true when the input element (or any of its ancestors) is in
1062
+ * right-to-left mode.
1063
+ *
1064
+ * Detection order:
1065
+ * 1. The input element's own computed `direction` style — this reflects
1066
+ * any `dir` attribute on the element itself, a CSS `direction` rule,
1067
+ * or inheritance from a parent element with dir="rtl".
1068
+ * 2. Falls back to false (LTR) if the window / getComputedStyle is
1069
+ * unavailable (e.g. server-side rendering).
1070
+ *
1071
+ * This is a getter (not a cached field) so it always reflects the current
1072
+ * DOM state, even if the direction changes dynamically at runtime.
1073
+ */
1074
+ get isRtl() {
1075
+ const win = this.document.defaultView;
1076
+ if (!win)
1077
+ return false;
1078
+ const dir = win.getComputedStyle(this.el.nativeElement).direction;
1079
+ return dir === 'rtl';
1080
+ }
1081
+ /**
1082
+ * In RTL mode the string stored in the input is prefixed with an LRM mark
1083
+ * so that the bidi algorithm does not reorder digits or separators.
1084
+ * This helper returns the prefix length including the LRM when RTL is active.
1085
+ *
1086
+ * Used wherever we need to know "how many characters before the editable
1087
+ * number portion start" — cursor clamping, minus-key position check, etc.
1088
+ */
1089
+ get editableStartOffset() {
1090
+ // In RTL: LRM + suffix occupies the left side of the display string.
1091
+ // In LTR: prefix occupies the left side.
1092
+ return this.isRtl
1093
+ ? LRM.length + this.suffix.length // RTL: [LRM][suffix][number][prefix]
1094
+ : this.prefix.length; // LTR: [prefix][number][suffix]
1095
+ }
1096
+ /**
1097
+ * The number of characters at the RIGHT end of the display string that
1098
+ * belong to the suffix (LTR) or prefix (RTL) — i.e. characters the cursor
1099
+ * must never move past on the right side.
1100
+ */
1101
+ get editableEndPadding() {
1102
+ return this.isRtl ? this.prefix.length : this.suffix.length;
1103
+ }
1104
+ constructor(el, renderer, document) {
1105
+ this.el = el;
1106
+ this.renderer = renderer;
1107
+ this.document = document;
1108
+ }
1109
+ // ─── Angular lifecycle ────────────────────────────────────────────────────────
1110
+ ngOnInit() {
1111
+ // Always use numeric inputMode so mobile keyboards show the number pad
1112
+ // regardless of document direction.
1113
+ this.renderer.setAttribute(this.el.nativeElement, 'inputmode', 'numeric');
1114
+ // Render the initial empty state into the input element.
1115
+ // refreshDisplay() also sets the dir attribute on first call.
1116
+ this.refreshDisplay();
1117
+ }
1118
+ ngOnChanges(changes) {
1119
+ const { prefix, suffix, thousandSeparator, decimalPlaces, outputType, pattern } = changes;
1120
+ // Re-render whenever any display-affecting input changes
1121
+ const displayInputsChanged = prefix ||
1122
+ suffix ||
1123
+ thousandSeparator ||
1124
+ decimalPlaces ||
1125
+ outputType ||
1126
+ pattern;
1127
+ if (displayInputsChanged) {
1128
+ this.refreshDisplay();
1129
+ }
1130
+ }
1131
+ // ─── ControlValueAccessor ────────────────────────────────────────────────────
1132
+ // These four methods are the bridge between the directive and Angular forms.
1133
+ /** Angular calls this when the form model value changes programmatically. */
1134
+ writeValue(value) {
1135
+ if (value === null || value === undefined || value === '') {
1136
+ this._rawValue = '';
1137
+ }
1138
+ else {
1139
+ const stringValue = String(value);
1140
+ if (this.hasPattern) {
1141
+ // Strip any separators that may already be in the incoming string value
1142
+ this._rawValue = this.stripPatternSeparatorsFrom(stringValue);
1143
+ }
1144
+ else {
1145
+ // Normalize decimal separator to '.' for internal storage
1146
+ this._rawValue = stringValue.replace(',', '.');
1147
+ }
1148
+ }
1149
+ this.refreshDisplay();
1150
+ }
1151
+ /** Angular calls this to register the function we must call when our value changes. */
1152
+ registerOnChange(fn) {
1153
+ this._onChange = fn;
1154
+ }
1155
+ /** Angular calls this to register the function we must call when the field is touched. */
1156
+ registerOnTouched(fn) {
1157
+ this._onTouched = fn;
1158
+ }
1159
+ /** Angular calls this to enable or disable the input. */
1160
+ setDisabledState(isDisabled) {
1161
+ this.renderer.setProperty(this.el.nativeElement, 'disabled', isDisabled);
1162
+ }
1163
+ // ─── Validator ───────────────────────────────────────────────────────────────
1164
+ /** Angular calls this to ask whether the current value has any errors. */
1165
+ validate(_control) {
1166
+ const errors = {};
1167
+ const raw = this._rawValue;
1168
+ // Empty or partial-minus input: let [required] handle the "is it filled?" check
1169
+ if (raw === '' || raw === '-') {
1170
+ return null;
1171
+ }
1172
+ // ── Pattern mode validation ──────────────────────────────────────────────
1173
+ if (this.hasPattern) {
1174
+ const filledDigitCount = raw.replace(REGEX.NON_DIGIT_CHARACTERS, '').length;
1175
+ const requiredDigitCount = this.patternDigitSlotCount;
1176
+ // The user must fill ALL slots before the value is considered complete
1177
+ if (filledDigitCount < requiredDigitCount) {
1178
+ errors['patternIncomplete'] = {
1179
+ required: requiredDigitCount,
1180
+ actual: filledDigitCount,
1181
+ pattern: this._resolvedPattern,
1182
+ };
1183
+ }
1184
+ return Object.keys(errors).length > 0 ? errors : null;
1185
+ }
1186
+ // ── Non-pattern: maxLength check ─────────────────────────────────────────
1187
+ const maxLen = this.effectiveMaxLength;
1188
+ if (maxLen !== null) {
1189
+ const digitCount = raw.replace(REGEX.NON_DIGIT_CHARACTERS, '').length;
1190
+ if (digitCount > maxLen) {
1191
+ errors['maxLength'] = { maxLength: maxLen, actual: digitCount };
1192
+ }
1193
+ }
1194
+ // ── String output: no numeric range validation needed ────────────────────
1195
+ // Identifiers (card numbers, phones) don't have a meaningful min/max.
1196
+ if (this.effectiveOutputType === 'string') {
1197
+ return Object.keys(errors).length > 0 ? errors : null;
1198
+ }
1199
+ // ── Number output: range validation ──────────────────────────────────────
1200
+ const numericValue = parseFloat(raw);
1201
+ if (isNaN(numericValue)) {
1202
+ errors['digits'] = { actual: raw };
1203
+ }
1204
+ if (this.min !== null && numericValue < this.min) {
1205
+ errors['min'] = { min: this.min, actual: numericValue };
1206
+ }
1207
+ if (this.max !== null && numericValue > this.max) {
1208
+ errors['max'] = { max: this.max, actual: numericValue };
1209
+ }
1210
+ return Object.keys(errors).length > 0 ? errors : null;
1211
+ }
1212
+ /** Angular calls this to register the function we call when validation rules change. */
1213
+ registerOnValidatorChange(fn) {
1214
+ this._onValidatorChange = fn;
1215
+ }
1216
+ // ─── DOM event handlers ────────────────────────────────────────────────────
1217
+ /**
1218
+ * Intercept keydown BEFORE the character lands in the input.
1219
+ * This is our first line of defense — blocking bad keys entirely
1220
+ * so the input event never fires for them.
1221
+ */
1222
+ onKeyDown(event) {
1223
+ // Always pass through control keys (Backspace, arrows, Ctrl+C, etc.)
1224
+ if (this.isControlKey(event)) {
1225
+ return;
1226
+ }
1227
+ const pressedKey = event.key;
1228
+ // ── Allow minus sign (number mode only, not pattern mode) ─────────────
1229
+ const isMinusKey = pressedKey === '-';
1230
+ const minusIsAllowed = this.allowNegative &&
1231
+ !this.hasPattern &&
1232
+ this.effectiveOutputType === 'number';
1233
+ // The "start of the editable number region" depends on direction:
1234
+ // LTR: cursor must be at prefix.length (characters the prefix occupies)
1235
+ // RTL: cursor must be at LRM.length + suffix.length (LRM + suffix on left)
1236
+ // editableStartOffset encapsulates this logic so onKeyDown stays clean.
1237
+ const cursorPosition = this.el.nativeElement.selectionStart ?? 0;
1238
+ const cursorIsAtStart = cursorPosition === this.editableStartOffset;
1239
+ const noMinusYet = !this._rawValue.startsWith('-');
1240
+ if (isMinusKey && minusIsAllowed && cursorIsAtStart && noMinusYet) {
1241
+ return; // allow the minus through
1242
+ }
1243
+ // ── Allow decimal point (number mode only, no pattern) ────────────────
1244
+ const isDecimalKey = pressedKey === '.' || pressedKey === ',';
1245
+ const decimalIsAllowed = this.decimalPlaces > 0 && !this.hasPattern;
1246
+ const noDecimalYet = !this._rawValue.includes('.');
1247
+ if (isDecimalKey && decimalIsAllowed && noDecimalYet) {
1248
+ return; // allow the decimal separator through
1249
+ }
1250
+ // ── Allow digit keys (0–9 AND Eastern Arabic/Persian digits) ────────────
1251
+ //
1252
+ // IMPORTANT: REGEX.SINGLE_DIGIT only matches ASCII digits /^\d$/ (U+0030–U+0039).
1253
+ // Arabic-Indic digits ٠-٩ (U+0660–U+0669) and Persian digits ۰-۹ (U+06F0–U+06F9)
1254
+ // are NOT matched by \d, so they would fall through to the "block everything else"
1255
+ // branch and be prevented — even when convertEasternNumerals is true.
1256
+ //
1257
+ // The fix: also check for Eastern digit characters here in keydown.
1258
+ // The actual conversion from Eastern → Western happens later in onInput(),
1259
+ // so all we need to do here is let the keystroke land in the input.
1260
+ const isWesternDigit = REGEX.SINGLE_DIGIT.test(pressedKey);
1261
+ const isArabicDigit = pressedKey >= REGEX.ARABIC_DIGIT_RANGE.FIRST &&
1262
+ pressedKey <= REGEX.ARABIC_DIGIT_RANGE.LAST;
1263
+ const isPersianDigit = pressedKey >= REGEX.PERSIAN_DIGIT_RANGE.FIRST &&
1264
+ pressedKey <= REGEX.PERSIAN_DIGIT_RANGE.LAST;
1265
+ const isEasternDigit = this.convertEasternNumerals && (isArabicDigit || isPersianDigit);
1266
+ const isDigitKey = isWesternDigit || isEasternDigit;
1267
+ if (isDigitKey) {
1268
+ // Enforce the digit slot limit BEFORE the character is inserted.
1269
+ // Count only the raw digits already in the value (exclude separators/prefix/suffix).
1270
+ const maxLen = this.effectiveMaxLength;
1271
+ if (maxLen !== null) {
1272
+ const currentDigitCount = this._rawValue.replace(REGEX.NON_DIGIT_CHARACTERS, '').length;
1273
+ const selectedChars = Math.abs((this.el.nativeElement.selectionEnd ?? 0) -
1274
+ (this.el.nativeElement.selectionStart ?? 0));
1275
+ const nothingSelected = selectedChars === 0;
1276
+ if (nothingSelected && currentDigitCount >= maxLen) {
1277
+ event.preventDefault(); // already at limit — block the digit
1278
+ return;
1279
+ }
1280
+ }
1281
+ return; // digit is allowed — Eastern digits will be converted in onInput()
1282
+ }
1283
+ // ── Block everything else ─────────────────────────────────────────────
1284
+ event.preventDefault();
1285
+ }
1286
+ /**
1287
+ * Process input AFTER the character has landed in the input element.
1288
+ * We read the new display value, strip decorations to get raw digits,
1289
+ * apply rules (decimal limits, maxLength, leading zeros), then re-render.
1290
+ */
1291
+ onInput() {
1292
+ const inputEl = this.el.nativeElement;
1293
+ const cursorPositionBeforeReformat = inputEl.selectionStart ?? 0;
1294
+ const displayLengthBeforeReformat = inputEl.value.length;
1295
+ // Convert Eastern numerals FIRST, before anything else touches the value
1296
+ if (this.convertEasternNumerals) {
1297
+ const converted = convertEasternDigits(inputEl.value);
1298
+ if (converted !== inputEl.value) {
1299
+ // Write the converted value directly into the DOM so our stripping below
1300
+ // works on clean ASCII characters
1301
+ this.renderer.setProperty(inputEl, 'value', converted);
1302
+ }
1303
+ }
1304
+ // Strip prefix, suffix, and visual separators — extract the raw digit core
1305
+ let raw = this.stripDecorationsFrom(inputEl.value);
1306
+ if (this.hasPattern) {
1307
+ // Pattern mode: keep only digits, cap at slot count
1308
+ raw = raw.replace(REGEX.ONLY_DIGITS, '');
1309
+ raw = raw.slice(0, this.patternDigitSlotCount);
1310
+ }
1311
+ else {
1312
+ // Non-pattern mode: normalize decimal separator
1313
+ raw = raw.replace(',', '.');
1314
+ if (this.decimalPlaces === 0 || this.effectiveOutputType === 'string') {
1315
+ // Integer or string mode: no decimal points allowed
1316
+ raw = raw.replace(REGEX.ALL_DOTS, '');
1317
+ }
1318
+ else {
1319
+ // Decimal mode: allow exactly one dot with up to N decimal places
1320
+ raw = this.enforceDecimalRules(raw);
1321
+ }
1322
+ // Strip leading zeros unless explicitly preserved
1323
+ const shouldStripLeadingZeros = !this.leadingZeros && this.effectiveOutputType === 'number';
1324
+ if (shouldStripLeadingZeros) {
1325
+ raw = this.stripLeadingZeros(raw);
1326
+ }
1327
+ }
1328
+ this._rawValue = raw;
1329
+ this.refreshDisplay(cursorPositionBeforeReformat, displayLengthBeforeReformat);
1330
+ this._onChange(this.buildModelValue());
1331
+ this._onValidatorChange();
1332
+ }
1333
+ /**
1334
+ * When the user leaves the field, mark it as touched (triggers validation
1335
+ * display) and clean up any trailing decimal point (e.g. the user typed
1336
+ * '12.' and then tabbed away).
1337
+ */
1338
+ onBlur() {
1339
+ this._onTouched();
1340
+ const hasTrailingDot = !this.hasPattern &&
1341
+ this.effectiveOutputType === 'number' &&
1342
+ this._rawValue.endsWith('.');
1343
+ if (hasTrailingDot) {
1344
+ this._rawValue = this._rawValue.slice(0, -1);
1345
+ this.refreshDisplay();
1346
+ }
1347
+ }
1348
+ /**
1349
+ * Handle paste events manually so we can sanitize the pasted content
1350
+ * before it reaches the input, just like we do for individual keystrokes.
1351
+ */
1352
+ onPaste(event) {
1353
+ event.preventDefault(); // stop the browser from pasting the raw text
1354
+ const pastedText = event.clipboardData?.getData('text') ?? '';
1355
+ // Convert Eastern numerals in pasted content too
1356
+ const normalized = this.convertEasternNumerals
1357
+ ? convertEasternDigits(pastedText)
1358
+ : pastedText;
1359
+ // Sanitize the pasted string to only the characters our mode allows
1360
+ const sanitized = this.hasPattern
1361
+ ? normalized.replace(REGEX.ONLY_DIGITS, '') // pattern: digits only
1362
+ : this.sanitizePastedText(normalized); // non-pattern: mode-aware
1363
+ // Bail out if there's nothing usable to paste.
1364
+ // (Previously this also tried to special-case a paste of just '0', but
1365
+ // comparing a value to both '' and '0' in the same check is a logical
1366
+ // impossibility — a string can never be both at once — so that branch
1367
+ // was unreachable dead code and TypeScript correctly flagged it.)
1368
+ if (sanitized === '')
1369
+ return;
1370
+ const inputEl = this.el.nativeElement;
1371
+ // Figure out where the cursor/selection is inside the RAW digit string
1372
+ const rawInsertStart = this.hasPattern
1373
+ ? this.mapDisplayCursorToRawIndex(inputEl.selectionStart ?? 0)
1374
+ : (inputEl.selectionStart ?? 0);
1375
+ const rawInsertEnd = this.hasPattern
1376
+ ? this.mapDisplayCursorToRawIndex(inputEl.selectionEnd ?? 0)
1377
+ : (inputEl.selectionEnd ?? 0);
1378
+ // Insert sanitized content into the raw value at the cursor position
1379
+ const beforeCursor = this._rawValue.slice(0, rawInsertStart);
1380
+ const afterCursor = this._rawValue.slice(rawInsertEnd);
1381
+ let newRaw = beforeCursor + sanitized + afterCursor;
1382
+ if (this.hasPattern) {
1383
+ // Pattern mode: keep digits only, cap at slot count
1384
+ newRaw = newRaw.replace(REGEX.ONLY_DIGITS, '').slice(0, this.patternDigitSlotCount);
1385
+ }
1386
+ else {
1387
+ // Non-pattern mode: apply the same rules as onInput
1388
+ if (this.decimalPlaces === 0 || this.effectiveOutputType === 'string') {
1389
+ newRaw = newRaw.replace(REGEX.ALL_DOTS, '');
1390
+ }
1391
+ else {
1392
+ newRaw = this.enforceDecimalRules(newRaw);
1393
+ }
1394
+ const maxLen = this.effectiveMaxLength;
1395
+ const rawDigitCount = newRaw.replace(REGEX.NON_DIGIT_CHARACTERS, '').length;
1396
+ if (maxLen !== null && rawDigitCount > maxLen) {
1397
+ return; // pasted content would exceed digit limit — discard
1398
+ }
1399
+ const shouldStripLeadingZeros = !this.leadingZeros && this.effectiveOutputType === 'number';
1400
+ if (shouldStripLeadingZeros) {
1401
+ newRaw = this.stripLeadingZeros(newRaw);
1402
+ }
1403
+ }
1404
+ this._rawValue = newRaw;
1405
+ this.refreshDisplay();
1406
+ this._onChange(this.buildModelValue());
1407
+ this._onValidatorChange();
1408
+ }
1409
+ /**
1410
+ * Block drag-and-drop into the field.
1411
+ * Without this a user could drag text from elsewhere and bypass our rules.
1412
+ */
1413
+ onDrop(event) {
1414
+ event.preventDefault();
1415
+ }
1416
+ // ─── Private methods ───────────────────────────────────────────────────────
1417
+ /**
1418
+ * Build the value that gets emitted to the Angular form model.
1419
+ *
1420
+ * Pattern / string mode → string | null
1421
+ * '' → null (empty field)
1422
+ * '123' → '123' (raw digits, leading zeros preserved, no separators)
1423
+ *
1424
+ * Number mode → number | null
1425
+ * '' → null (empty field)
1426
+ * '-' → null (user typed a minus but no digits yet)
1427
+ * '123' → 123
1428
+ * '1.5' → 1.5
1429
+ * '-7' → -7
1430
+ */
1431
+ buildModelValue() {
1432
+ if (this._rawValue === '') {
1433
+ return null;
1434
+ }
1435
+ if (this.effectiveOutputType === 'string') {
1436
+ return this._rawValue || null;
1437
+ }
1438
+ // Number mode
1439
+ if (this._rawValue === '-') {
1440
+ return null; // not a valid number yet
1441
+ }
1442
+ const num = parseFloat(this._rawValue);
1443
+ return isNaN(num) ? null : num;
1444
+ }
1445
+ /**
1446
+ * Write the formatted display string back to the <input> DOM element
1447
+ * and try to restore the cursor to a sensible position.
1448
+ *
1449
+ * @param cursorBefore Cursor position in the display string BEFORE we reformatted
1450
+ * @param lengthBefore Total display string length BEFORE we reformatted
1451
+ */
1452
+ refreshDisplay(cursorBefore, lengthBefore) {
1453
+ // ── Step 1: format the number portion ─────────────────────────────────
1454
+ const formattedContent = this.hasPattern
1455
+ ? this.applyPatternFormatting(this._rawValue)
1456
+ : this.effectiveOutputType === 'string'
1457
+ ? this._rawValue // string mode: no formatting
1458
+ : this.applyThousandsFormatting(this._rawValue); // number mode: comma groups
1459
+ // ── Step 2: assemble the full display string ───────────────────────────
1460
+ //
1461
+ // RTL layout: [LRM][suffix][formattedNumber][prefix]
1462
+ // LTR layout: [prefix][formattedNumber][suffix]
1463
+ //
1464
+ // WHY SWAP PREFIX AND SUFFIX IN RTL?
1465
+ // In RTL reading order, the "start" of a line is the right side.
1466
+ // A currency symbol like "﷼" (Rial) naturally sits on the RIGHT of
1467
+ // the number in Arabic/Persian convention, while a unit like "kg"
1468
+ // sits on the LEFT. That maps to: prefix visually right, suffix left.
1469
+ // By swapping their positions in the string, the bidi algorithm places
1470
+ // them correctly without any CSS changes.
1471
+ //
1472
+ // WHY LRM AT THE FRONT?
1473
+ // Prevents the bidi algorithm from treating the numeric content as RTL
1474
+ // text and mirroring digit order or separator placement.
1475
+ // The LRM is stripped before the value reaches the model.
1476
+ const fullDisplayValue = this.isRtl
1477
+ ? LRM + this.suffix + formattedContent + this.prefix
1478
+ : this.prefix + formattedContent + this.suffix;
1479
+ this.renderer.setProperty(this.el.nativeElement, 'value', fullDisplayValue);
1480
+ // ── Step 3: restore cursor position ───────────────────────────────────
1481
+ if (cursorBefore !== undefined && lengthBefore !== undefined) {
1482
+ const lengthDelta = fullDisplayValue.length - lengthBefore;
1483
+ // Clamp cursor between the editable start and end offsets,
1484
+ // both of which account for the RTL/LTR decoration layout.
1485
+ const newCursorPos = Math.max(this.editableStartOffset, Math.min(cursorBefore + lengthDelta, fullDisplayValue.length - this.editableEndPadding));
1486
+ // Defer to next animation frame so the browser has committed the new value
1487
+ requestAnimationFrame(() => {
1488
+ this.el.nativeElement.setSelectionRange(newCursorPos, newCursorPos);
1489
+ });
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Apply the display format pattern to the current raw digit string.
1494
+ *
1495
+ * Algorithm:
1496
+ * Walk through the pattern character by character.
1497
+ * On '0': consume the next raw digit and add it to the result.
1498
+ * On anything else: this is a separator — add it ONLY if more digits follow.
1499
+ * (The "more digits follow" check prevents trailing separators mid-type.)
1500
+ *
1501
+ * Examples:
1502
+ * pattern '0000 0000 0000 0000', raw '41111111' → '4111 1111'
1503
+ * pattern '(000) 000-0000', raw '5558675' → '(555) 867'
1504
+ * pattern '00/00', raw '122' → '12/2'
1505
+ */
1506
+ applyPatternFormatting(raw) {
1507
+ if (!this.hasPattern || !raw) {
1508
+ return raw;
1509
+ }
1510
+ const digits = raw.replace(REGEX.ONLY_DIGITS, ''); // isolated digit string
1511
+ let digitIndex = 0;
1512
+ let displayResult = '';
1513
+ for (const patternChar of this._resolvedPattern) {
1514
+ // Stop when we run out of digits to place
1515
+ if (digitIndex >= digits.length) {
1516
+ break;
1517
+ }
1518
+ if (patternChar === '0') {
1519
+ // Digit slot: consume the next digit
1520
+ displayResult += digits[digitIndex];
1521
+ digitIndex++;
1522
+ }
1523
+ else {
1524
+ // Separator character: insert it only when more digits are still coming
1525
+ // so we never show a trailing space/dash after the last typed digit
1526
+ const moreDigitsRemain = digitIndex < digits.length;
1527
+ if (moreDigitsRemain) {
1528
+ displayResult += patternChar;
1529
+ }
1530
+ }
1531
+ }
1532
+ return displayResult;
1533
+ }
1534
+ /**
1535
+ * Apply thousands separator grouping and decimal character to a raw number string.
1536
+ * Only used in number output mode (not pattern, not string mode).
1537
+ *
1538
+ * Examples (separator=',', decimalChar='.'):
1539
+ * '1234567' → '1,234,567'
1540
+ * '-9876.50' → '-9,876.50'
1541
+ * '100' → '100'
1542
+ */
1543
+ applyThousandsFormatting(raw) {
1544
+ if (raw === '' || raw === '-') {
1545
+ return raw;
1546
+ }
1547
+ const isNegative = raw.startsWith('-');
1548
+ const absValue = isNegative ? raw.slice(1) : raw;
1549
+ const [integerPart, decimalPart] = absValue.split('.');
1550
+ // Insert the thousands separator every 3 digits from the right
1551
+ const formattedInteger = this.thousandSeparator
1552
+ ? integerPart.replace(REGEX.THOUSANDS_SEPARATOR_POSITIONS, this.thousandSeparator)
1553
+ : integerPart;
1554
+ const formattedDecimal = decimalPart !== undefined
1555
+ ? this.displayDecimalChar + decimalPart
1556
+ : '';
1557
+ return (isNegative ? '-' : '') + formattedInteger + formattedDecimal;
1558
+ }
1559
+ /**
1560
+ * Remove all visual decoration from the displayed input value and return
1561
+ * the bare digit/numeric string ready for internal storage.
1562
+ *
1563
+ * Steps:
1564
+ * 1. Strip prefix from the front
1565
+ * 2. Strip suffix from the back
1566
+ * 3. Remove pattern separators OR thousands separators (depending on mode)
1567
+ * 4. Normalize non-ASCII decimal separator back to '.'
1568
+ * 5. Remove any remaining non-numeric characters
1569
+ */
1570
+ stripDecorationsFrom(displayValue) {
1571
+ let raw = displayValue;
1572
+ // ── Remove the LRM marker if present ──────────────────────────────────
1573
+ // In RTL mode every display string starts with LRM (U+200E).
1574
+ // Strip it first so subsequent prefix/suffix checks work correctly.
1575
+ if (raw.startsWith(LRM)) {
1576
+ raw = raw.slice(LRM.length);
1577
+ }
1578
+ if (this.isRtl) {
1579
+ // RTL layout: [suffix][formattedNumber][prefix]
1580
+ // Remove suffix from the LEFT (start) and prefix from the RIGHT (end).
1581
+ if (this.suffix && raw.startsWith(this.suffix)) {
1582
+ raw = raw.slice(this.suffix.length);
1583
+ }
1584
+ if (this.prefix && raw.endsWith(this.prefix)) {
1585
+ raw = raw.slice(0, -this.prefix.length);
1586
+ }
1587
+ }
1588
+ else {
1589
+ // LTR layout: [prefix][formattedNumber][suffix]
1590
+ // Remove prefix from the LEFT and suffix from the RIGHT.
1591
+ if (this.prefix && raw.startsWith(this.prefix)) {
1592
+ raw = raw.slice(this.prefix.length);
1593
+ }
1594
+ if (this.suffix && raw.endsWith(this.suffix)) {
1595
+ raw = raw.slice(0, -this.suffix.length);
1596
+ }
1597
+ }
1598
+ if (this.hasPattern) {
1599
+ // Pattern mode: remove the separator characters defined by the pattern
1600
+ raw = this.stripPatternSeparatorsFrom(raw);
1601
+ }
1602
+ else if (this.thousandSeparator) {
1603
+ // Number mode with thousands grouping: remove the grouping character
1604
+ const escapedSeparator = this.thousandSeparator.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&' // prepend \ before any regex-special character
1605
+ );
1606
+ raw = raw.replace(new RegExp(escapedSeparator, 'g'), '');
1607
+ }
1608
+ // If we're using a non-standard decimal char (e.g. ',' in European mode),
1609
+ // normalize it back to '.' for internal storage
1610
+ if (!this.hasPattern && this.displayDecimalChar !== '.') {
1611
+ raw = raw.replace(this.displayDecimalChar, '.');
1612
+ }
1613
+ // Final sweep: remove anything that still isn't a digit, '.', or '-'
1614
+ raw = raw.replace(REGEX.ONLY_DIGITS_DOT_AND_MINUS, '');
1615
+ return raw;
1616
+ }
1617
+ /**
1618
+ * Remove every separator character that the current pattern defines.
1619
+ * For example, pattern '(000) 000-0000' defines '(', ')', ' ', '-' as separators.
1620
+ * This strips all four from the input string, leaving only digits.
1621
+ */
1622
+ stripPatternSeparatorsFrom(value) {
1623
+ let result = value;
1624
+ for (const separatorChar of this.patternSeparatorChars) {
1625
+ const escapedChar = separatorChar.replace(REGEX.REGEX_SPECIAL_CHARACTERS, '\\$&');
1626
+ result = result.replace(new RegExp(escapedChar, 'g'), '');
1627
+ }
1628
+ return result;
1629
+ }
1630
+ /**
1631
+ * Ensure a raw numeric string has at most one decimal point
1632
+ * and at most [decimalPlaces] digits after it.
1633
+ *
1634
+ * Examples (decimalPlaces=2):
1635
+ * '12.345' → '12.34'
1636
+ * '1.2.3' → '1.23'
1637
+ * '100' → '100'
1638
+ */
1639
+ enforceDecimalRules(raw) {
1640
+ const parts = raw.split('.');
1641
+ // More than one dot? Collapse everything after the first dot into one decimal part
1642
+ if (parts.length > 2) {
1643
+ raw = parts[0] + '.' + parts.slice(1).join('');
1644
+ }
1645
+ // Too many decimal digits? Truncate to the allowed count
1646
+ const [intPart, decPart] = raw.split('.');
1647
+ if (decPart !== undefined && decPart.length > this.decimalPlaces) {
1648
+ raw = intPart + '.' + decPart.slice(0, this.decimalPlaces);
1649
+ }
1650
+ return raw;
1651
+ }
1652
+ /**
1653
+ * Remove unnecessary leading zeros from the integer part of a numeric string.
1654
+ * Always preserves at least one digit and never touches the decimal part.
1655
+ *
1656
+ * Examples:
1657
+ * '007' → '7'
1658
+ * '0042' → '42'
1659
+ * '0.5' → '0.5' (leading zero before decimal is correct, don't strip)
1660
+ * '100' → '100' (no leading zero)
1661
+ * '-007' → '-7' (handles negative values)
1662
+ */
1663
+ stripLeadingZeros(raw) {
1664
+ if (!raw) {
1665
+ return raw;
1666
+ }
1667
+ const isNegative = raw.startsWith('-');
1668
+ const absValue = isNegative ? raw.slice(1) : raw;
1669
+ const dotPosition = absValue.indexOf('.');
1670
+ const integerPart = dotPosition >= 0 ? absValue.slice(0, dotPosition) : absValue;
1671
+ const decimalPart = dotPosition >= 0 ? absValue.slice(dotPosition) : '';
1672
+ // Replace leading zeros but always keep at least one digit
1673
+ const cleanedInteger = integerPart.replace(REGEX.LEADING_ZEROS, '$1') || integerPart;
1674
+ return (isNegative ? '-' : '') + cleanedInteger + decimalPart;
1675
+ }
1676
+ /**
1677
+ * Sanitize a pasted string for non-pattern mode.
1678
+ * The characters we keep depend on the current mode:
1679
+ *
1680
+ * string mode → digits only (no dot, no minus; identifiers can't be negative)
1681
+ * integer number → digits + optional leading minus
1682
+ * decimal number → digits + optional dot + optional leading minus
1683
+ */
1684
+ sanitizePastedText(text) {
1685
+ // Normalize decimal comma first (e.g. from a European spreadsheet paste)
1686
+ let clean = text.replace(',', '.');
1687
+ if (this.effectiveOutputType === 'string') {
1688
+ clean = clean.replace(REGEX.ONLY_DIGITS, ''); // string: digits only
1689
+ }
1690
+ else if (this.decimalPlaces === 0) {
1691
+ clean = clean.replace(REGEX.ONLY_DIGITS_AND_MINUS, ''); // integer: digits + minus
1692
+ }
1693
+ else {
1694
+ clean = clean.replace(REGEX.ONLY_DIGITS_DOT_AND_MINUS, ''); // decimal: digits + . + minus
1695
+ }
1696
+ // Remove minus signs unless negative values are permitted
1697
+ if (!this.allowNegative || this.effectiveOutputType === 'string') {
1698
+ clean = clean.replace(REGEX.ALL_MINUS_SIGNS, '');
1699
+ }
1700
+ return clean;
1701
+ }
1702
+ /**
1703
+ * Map a cursor position in the formatted DISPLAY string back to an index
1704
+ * in the raw digit string.
1705
+ *
1706
+ * This is needed during paste operations in pattern mode so we know
1707
+ * exactly which digit position the cursor is sitting at.
1708
+ *
1709
+ * Example:
1710
+ * pattern '(000) 000-0000'
1711
+ * display '(555) 867' cursor at position 8 (after '(555) 86')
1712
+ *
1713
+ * Walking the pattern:
1714
+ * '(' pos 0 → sep (adjusted pos 0, raw index still 0)
1715
+ * '0' pos 1 → digit (raw index 0) digit consumed → raw index 1
1716
+ * '0' pos 2 → digit (raw index 1) digit consumed → raw index 2
1717
+ * '0' pos 3 → digit (raw index 2) digit consumed → raw index 3
1718
+ * ')' pos 4 → sep
1719
+ * ' ' pos 5 → sep
1720
+ * '0' pos 6 → digit (raw index 3) digit consumed → raw index 4
1721
+ * '0' pos 7 → digit (raw index 4) digit consumed → raw index 5
1722
+ * We reached position 8, so raw index = 5 ✔
1723
+ */
1724
+ mapDisplayCursorToRawIndex(displayCursorPos) {
1725
+ if (!this.hasPattern) {
1726
+ return displayCursorPos;
1727
+ }
1728
+ // Adjust for prefix length so we walk the pattern relative to its own start
1729
+ const positionInPattern = displayCursorPos - this.prefix.length;
1730
+ let rawDigitIndex = 0;
1731
+ let patternPosition = 0;
1732
+ for (const patternChar of this._resolvedPattern) {
1733
+ if (patternPosition >= positionInPattern) {
1734
+ break;
1735
+ }
1736
+ if (patternChar === '0') {
1737
+ rawDigitIndex++; // this position consumed a digit
1738
+ }
1739
+ patternPosition++;
1740
+ }
1741
+ return rawDigitIndex;
1742
+ }
1743
+ /**
1744
+ * Decide whether a keyboard event is a "control" key that should always
1745
+ * be allowed through without interception.
1746
+ *
1747
+ * Includes:
1748
+ * - Modifier keys held (Ctrl, Meta/Cmd, Alt) — so Ctrl+C, Ctrl+V, etc. work
1749
+ * - Navigation keys (arrows, Home, End)
1750
+ * - Editing keys (Backspace, Delete, Tab, Escape, Enter)
1751
+ */
1752
+ isControlKey(event) {
1753
+ const isModifierHeld = event.ctrlKey || event.metaKey || event.altKey;
1754
+ const isNavigationOrEditKey = [
1755
+ 'Backspace', 'Delete',
1756
+ 'Tab', 'Escape', 'Enter',
1757
+ 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
1758
+ 'Home', 'End',
1759
+ ].includes(event.key);
1760
+ return isModifierHeld || isNavigationOrEditKey;
1761
+ }
1762
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: digitsOnlyDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Directive });
1763
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: digitsOnlyDirective, isStandalone: true, selector: "[digitsOnly]", inputs: { decimalPlaces: "decimalPlaces", thousandSeparator: "thousandSeparator", prefix: "prefix", suffix: "suffix", allowNegative: "allowNegative", leadingZeros: "leadingZeros", maxLength: "maxLength", min: "min", max: "max", outputType: "outputType", convertEasternNumerals: "convertEasternNumerals", pattern: "pattern" }, host: { listeners: { "keydown": "onKeyDown($event)", "input": "onInput()", "blur": "onBlur()", "paste": "onPaste($event)", "drop": "onDrop($event)" } }, providers: [
1764
+ {
1765
+ provide: NG_VALUE_ACCESSOR,
1766
+ useExisting: forwardRef(() => digitsOnlyDirective),
1767
+ multi: true,
1768
+ },
1769
+ {
1770
+ provide: NG_VALIDATORS,
1771
+ useExisting: forwardRef(() => digitsOnlyDirective),
1772
+ multi: true,
1773
+ },
1774
+ ], usesOnChanges: true, ngImport: i0 });
1775
+ }
1776
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: digitsOnlyDirective, decorators: [{
1777
+ type: Directive,
1778
+ args: [{
1779
+ selector: '[digitsOnly]',
1780
+ standalone: true,
1781
+ providers: [
1782
+ {
1783
+ provide: NG_VALUE_ACCESSOR,
1784
+ useExisting: forwardRef(() => digitsOnlyDirective),
1785
+ multi: true,
1786
+ },
1787
+ {
1788
+ provide: NG_VALIDATORS,
1789
+ useExisting: forwardRef(() => digitsOnlyDirective),
1790
+ multi: true,
1791
+ },
1792
+ ],
1793
+ }]
1794
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: Document, decorators: [{
1795
+ type: Inject,
1796
+ args: [DOCUMENT]
1797
+ }] }], propDecorators: { decimalPlaces: [{
1798
+ type: Input
1799
+ }], thousandSeparator: [{
1800
+ type: Input
1801
+ }], prefix: [{
1802
+ type: Input
1803
+ }], suffix: [{
1804
+ type: Input
1805
+ }], allowNegative: [{
1806
+ type: Input
1807
+ }], leadingZeros: [{
1808
+ type: Input
1809
+ }], maxLength: [{
1810
+ type: Input
1811
+ }], min: [{
1812
+ type: Input
1813
+ }], max: [{
1814
+ type: Input
1815
+ }], outputType: [{
1816
+ type: Input
1817
+ }], convertEasternNumerals: [{
1818
+ type: Input
1819
+ }], pattern: [{
1820
+ type: Input
1821
+ }], onKeyDown: [{
1822
+ type: HostListener,
1823
+ args: ['keydown', ['$event']]
1824
+ }], onInput: [{
1825
+ type: HostListener,
1826
+ args: ['input']
1827
+ }], onBlur: [{
1828
+ type: HostListener,
1829
+ args: ['blur']
1830
+ }], onPaste: [{
1831
+ type: HostListener,
1832
+ args: ['paste', ['$event']]
1833
+ }], onDrop: [{
1834
+ type: HostListener,
1835
+ args: ['drop', ['$event']]
1836
+ }] } });
1837
+
1838
+ /*
1839
+ * Public API Surface of digits-only
1840
+ */
1841
+
1842
+ /**
1843
+ * Generated bundle index. Do not edit.
1844
+ */
1845
+
1846
+ export { EASTERN_DIGIT_MAP, EASTERN_NUMERAL_REGEX, convertEasternDigits, digitsOnlyDirective, getDigitScript, hasEasternDigits, toWesternNumber };
1847
+ //# sourceMappingURL=ngx-digits-only.mjs.map