ngxsmk-tel-input 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngxsmk-tel-input",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Angular international telephone input (intl-tel-input UI + libphonenumber-js validation). ControlValueAccessor. SSR-safe.",
5
5
  "license": "MIT",
6
6
  "scripts": { "build": "ng build ngxsmk-tel-input" },
@@ -55,5 +55,24 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "tslib": "^2.3.0"
58
- }
58
+ },
59
+ "typings": "./ngxsmk-tel-input.d.ts",
60
+ "module": "./fesm2022/ngxsmk-tel-input.mjs",
61
+ "exports": {
62
+ ".": {
63
+ "types": "./ngxsmk-tel-input.d.ts",
64
+ "esm2022": "./esm2022/ngxsmk-tel-input.mjs",
65
+ "default": "./fesm2022/ngxsmk-tel-input.mjs"
66
+ },
67
+ "./src/*": null
68
+ },
69
+ "files": [
70
+ "bundles/",
71
+ "fesm2022/",
72
+ "esm2022/",
73
+ "ngxsmk-tel-input.d.ts",
74
+ "ngxsmk-tel-input.d.ts.map",
75
+ "README.md",
76
+ "LICENSE"
77
+ ]
59
78
  }
@@ -1,8 +0,0 @@
1
- # Changesets
2
-
3
- Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
- with multi-package repos, or single-package repos to help you version and publish your code. You can
5
- find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
-
7
- We have a quick list of common questions to get you started engaging with this project in
8
- [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3
- "changelog": "@changesets/cli/changelog",
4
- "commit": false,
5
- "fixed": [],
6
- "linked": [],
7
- "access": "restricted",
8
- "baseBranch": "main",
9
- "updateInternalDependencies": "patch",
10
- "ignore": []
11
- }
package/docs/invalid.png DELETED
Binary file
package/docs/kr.png DELETED
Binary file
package/docs/valid.png DELETED
Binary file
package/ng-package.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
- "dest": "../../dist/ngxsmk-tel-input",
4
- "lib": {
5
- "entryFile": "src/public-api.ts"
6
- },
7
- "allowedNonPeerDependencies": [
8
- "intl-tel-input",
9
- "libphonenumber-js"
10
- ],
11
- "assets": [
12
- "README.md",
13
- "LICENSE",
14
- "docs"
15
- ]
16
- }
@@ -1,220 +0,0 @@
1
- /* ---------- Theme tokens ---------- */
2
- :host {
3
- --tel-bg: #fff;
4
- --tel-fg: #0f172a;
5
- --tel-border: #c0c0c0;
6
- --tel-border-hover: #9aa0a6;
7
- --tel-ring: #2563eb;
8
- --tel-placeholder: #9ca3af;
9
- --tel-error: #ef4444;
10
- --tel-radius: 12px;
11
- --tel-focus-shadow: 0 0 0 3px rgba(37, 99, 235, .25);
12
-
13
- --tel-dd-bg: var(--tel-bg);
14
- --tel-dd-border: var(--tel-border);
15
- --tel-dd-shadow: 0 24px 60px rgba(0, 0, 0, .18);
16
- --tel-dd-radius: 12px;
17
- --tel-dd-item-hover: rgba(37, 99, 235, .08);
18
- --tel-dd-z: 2000;
19
- --tel-dd-search-bg: rgba(148, 163, 184, .08);
20
-
21
- display: block;
22
- }
23
-
24
- :host-context(.dark) {
25
- --tel-bg: #0b0f17;
26
- --tel-fg: #e5e7eb;
27
- --tel-border: #334155;
28
- --tel-border-hover: #475569;
29
- --tel-ring: #60a5fa;
30
- --tel-placeholder: #94a3b8;
31
-
32
- --tel-dd-bg: #0f1521;
33
- --tel-dd-border: #324056;
34
- --tel-dd-search-bg: rgba(148, 163, 184, .12);
35
- }
36
-
37
- /* ---------- Structure ---------- */
38
- .ngxsmk-tel {
39
- width: 100%;
40
- color: var(--tel-fg);
41
- }
42
-
43
- .ngxsmk-tel.disabled {
44
- opacity: .7;
45
- cursor: not-allowed;
46
- }
47
-
48
- .ngxsmk-tel__label {
49
- display: inline-block;
50
- margin-bottom: 6px;
51
- font-size: .875rem;
52
- font-weight: 500;
53
- }
54
-
55
- .ngxsmk-tel__wrap {
56
- position: relative;
57
- }
58
-
59
- .ngxsmk-tel-input__wrapper,
60
- :host ::ng-deep .iti {
61
- width: 100%;
62
- }
63
-
64
- .ngxsmk-tel-input__control {
65
- width: 100%;
66
- height: 40px;
67
- font: inherit;
68
- color: var(--tel-fg);
69
- background: var(--tel-bg);
70
- border: 1px solid var(--tel-border);
71
- border-radius: var(--tel-radius);
72
- padding: 10px 40px 10px 12px;
73
- outline: none;
74
- transition: border-color .15s, box-shadow .15s, background .15s;
75
- }
76
-
77
- .ngxsmk-tel-input__control::placeholder {
78
- color: var(--tel-placeholder);
79
- }
80
-
81
- .ngxsmk-tel-input__control:hover {
82
- border-color: var(--tel-border-hover);
83
- }
84
-
85
- .ngxsmk-tel-input__control:focus {
86
- border-color: var(--tel-ring);
87
- box-shadow: var(--tel-focus-shadow);
88
- }
89
-
90
- /* Size presets */
91
- [data-size="sm"] .ngxsmk-tel-input__control {
92
- height: 34px;
93
- font-size: 13px;
94
- padding: 6px 36px 6px 10px;
95
- border-radius: 10px;
96
- }
97
-
98
- [data-size="lg"] .ngxsmk-tel-input__control {
99
- height: 46px;
100
- font-size: 16px;
101
- padding: 12px 44px 12px 14px;
102
- border-radius: 14px;
103
- }
104
-
105
- /* Variants */
106
- [data-variant="filled"] .ngxsmk-tel-input__control {
107
- background: rgba(148, 163, 184, .08);
108
- }
109
-
110
- [data-variant="underline"] .ngxsmk-tel-input__control {
111
- border: 0;
112
- border-bottom: 2px solid var(--tel-border);
113
- border-radius: 0;
114
- padding-left: 0;
115
- padding-right: 34px;
116
- }
117
-
118
- [data-variant="underline"] .ngxsmk-tel-input__control:focus {
119
- border-bottom-color: var(--tel-ring);
120
- box-shadow: none;
121
- }
122
-
123
- /* ---------- intl-tel-input dropdown (deep selectors) ---------- */
124
- :host ::ng-deep .iti__flag-container {
125
- border-top-left-radius: var(--tel-radius);
126
- border-bottom-left-radius: var(--tel-radius);
127
- border: 1px solid var(--tel-border);
128
- border-right: none;
129
- background: var(--tel-bg);
130
- }
131
-
132
- :host ::ng-deep .iti__selected-flag {
133
- height: 100%;
134
- padding: 0 10px;
135
- display: inline-flex;
136
- align-items: center;
137
- }
138
-
139
- :host ::ng-deep .iti__country-list {
140
- background: var(--tel-dd-bg);
141
- border: 1px solid var(--tel-dd-border);
142
- border-radius: var(--tel-dd-radius);
143
- box-shadow: var(--tel-dd-shadow);
144
- max-height: min(50vh, 360px);
145
- overflow: auto;
146
- padding: 6px 0;
147
- width: max(280px, 100%);
148
- z-index: var(--tel-dd-z);
149
- }
150
-
151
- :host ::ng-deep .iti--container .iti__country-list {
152
- z-index: var(--tel-dd-z);
153
- }
154
-
155
- :host ::ng-deep .iti__search-input {
156
- position: sticky;
157
- top: 0;
158
- margin: 0;
159
- padding: 10px 12px;
160
- width: 100%;
161
- border: 0;
162
- border-bottom: 1px solid var(--tel-dd-border);
163
- outline: none;
164
- background: var(--tel-dd-search-bg);
165
- color: var(--tel-fg);
166
- }
167
-
168
- :host ::ng-deep .iti__country {
169
- display: grid;
170
- grid-template-columns: 28px 1fr auto;
171
- align-items: center;
172
- column-gap: .5rem;
173
- padding: 10px 12px;
174
- cursor: pointer;
175
- }
176
-
177
- :host ::ng-deep .iti__dial-code {
178
- color: var(--tel-placeholder);
179
- font-weight: 600;
180
- margin-left: 10px;
181
- }
182
-
183
- /* Clear button */
184
- .ngxsmk-tel__clear {
185
- position: absolute;
186
- right: 8px;
187
- top: 50%;
188
- transform: translateY(-50%);
189
- border: 0;
190
- background: transparent;
191
- font-size: 18px;
192
- line-height: 1;
193
- width: 28px;
194
- height: 28px;
195
- border-radius: 50%;
196
- cursor: pointer;
197
- color: var(--tel-placeholder);
198
- }
199
-
200
- .ngxsmk-tel__clear:hover {
201
- background: rgba(148, 163, 184, .15);
202
- }
203
-
204
- /* Hint & Error */
205
- .ngxsmk-tel__hint {
206
- margin-top: 6px;
207
- font-size: 12px;
208
- color: var(--tel-placeholder);
209
- }
210
-
211
- .ngxsmk-tel__error {
212
- margin-top: 6px;
213
- font-size: 12px;
214
- color: var(--tel-error);
215
- }
216
-
217
- .ngxsmk-tel__wrap.has-error .ngxsmk-tel-input__control {
218
- border-color: var(--tel-error);
219
- box-shadow: 0 0 0 3px rgba(239, 68, 68, .15);
220
- }
@@ -1,24 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { PLATFORM_ID } from '@angular/core';
3
- import { NgxsmkTelInputComponent } from './ngxsmk-tel-input.component';
4
-
5
- describe('NgxsmkTelInputComponent', () => {
6
- let component: NgxsmkTelInputComponent;
7
- let fixture: ComponentFixture<NgxsmkTelInputComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [NgxsmkTelInputComponent],
12
- // Pretend we're on the server so ngAfterViewInit skips intl-tel-input init
13
- providers: [{ provide: PLATFORM_ID, useValue: 'server' }],
14
- }).compileComponents();
15
-
16
- fixture = TestBed.createComponent(NgxsmkTelInputComponent);
17
- component = fixture.componentInstance;
18
- fixture.detectChanges();
19
- });
20
-
21
- it('should create', () => {
22
- expect(component).toBeTruthy();
23
- });
24
- });
@@ -1,458 +0,0 @@
1
- import {
2
- AfterViewInit,
3
- Component,
4
- ElementRef,
5
- EventEmitter,
6
- forwardRef,
7
- inject,
8
- Input,
9
- NgZone,
10
- OnChanges,
11
- OnDestroy,
12
- Output,
13
- PLATFORM_ID,
14
- SimpleChanges,
15
- ViewChild
16
- } from '@angular/core';
17
- import {isPlatformBrowser} from '@angular/common';
18
- import {
19
- AbstractControl,
20
- ControlValueAccessor,
21
- NG_VALIDATORS,
22
- NG_VALUE_ACCESSOR,
23
- ValidationErrors,
24
- Validator
25
- } from '@angular/forms';
26
- import type {CountryCode} from 'libphonenumber-js';
27
- import {NgxsmkTelInputService} from './ngxsmk-tel-input.service';
28
- import {CountryMap, IntlTelI18n} from './types';
29
-
30
- type IntlTelInstance = any;
31
-
32
- @Component({
33
- selector: 'ngxsmk-tel-input',
34
- standalone: true,
35
- imports: [],
36
- template: `
37
- <div class="ngxsmk-tel"
38
- [class.disabled]="disabled"
39
- [attr.data-size]="size"
40
- [attr.data-variant]="variant"
41
- [attr.dir]="dir">
42
- @if (label) {
43
- <label class="ngxsmk-tel__label" [for]="resolvedId">{{ label }}</label>
44
- }
45
-
46
- <div class="ngxsmk-tel__wrap" [class.has-error]="showError">
47
- <div class="ngxsmk-tel-input__wrapper">
48
- <input
49
- #telInput
50
- type="tel"
51
- class="ngxsmk-tel-input__control"
52
- [id]="resolvedId"
53
- [attr.name]="name || null"
54
- [attr.placeholder]="placeholder || null"
55
- [attr.autocomplete]="autocomplete"
56
- [attr.inputmode]="digitsOnly ? 'numeric' : 'tel'"
57
- [attr.pattern]="digitsOnly ? (allowLeadingPlus ? '\\\\+?[0-9]*' : '[0-9]*') : null"
58
- [disabled]="disabled"
59
- [attr.aria-invalid]="showError ? 'true' : 'false'"
60
- (blur)="onBlur()"
61
- (focus)="onFocus()"
62
- />
63
- </div>
64
-
65
- @if (showClear && currentRaw()) {
66
- <button type="button"
67
- class="ngxsmk-tel__clear"
68
- (click)="clearInput()"
69
- [attr.aria-label]="clearAriaLabel">
70
- ×
71
- </button>
72
- }
73
- </div>
74
-
75
- @if (hint && !showError) {
76
- <div class="ngxsmk-tel__hint">{{ hint }}</div>
77
- }
78
-
79
- @if (showError) {
80
- <div class="ngxsmk-tel__error">{{ errorText || 'Please enter a valid phone number.' }}</div>
81
- }
82
- </div>
83
- `,
84
- styleUrls: ['./ngxsmk-tel-input.component.scss'],
85
- providers: [
86
- {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true},
87
- {provide: NG_VALIDATORS, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true}
88
- ]
89
- })
90
- export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
91
-
92
- @ViewChild('telInput', {static: true}) inputRef!: ElementRef<HTMLInputElement>;
93
-
94
- /* Core config */
95
- @Input() initialCountry: CountryCode | 'auto' = 'US';
96
- @Input() preferredCountries: CountryCode[] = ['US', 'GB'];
97
- @Input() onlyCountries?: CountryCode[];
98
- @Input() nationalMode: boolean = false;
99
- @Input() separateDialCode: boolean = false;
100
- @Input() allowDropdown: boolean = true;
101
-
102
- /* UX */
103
- @Input() placeholder?: string;
104
- @Input() autocomplete = 'tel';
105
- @Input() name?: string;
106
- @Input() inputId?: string;
107
- @Input() disabled: boolean = false;
108
-
109
- @Input() label?: string;
110
- @Input() hint?: string;
111
- @Input() errorText?: string;
112
- @Input() size: 'sm' | 'md' | 'lg' = 'md';
113
- @Input() variant: 'outline' | 'filled' | 'underline' = 'outline';
114
- @Input() showClear: boolean = true;
115
- @Input() autoFocus: boolean = false;
116
- @Input() selectOnFocus: boolean = false;
117
- @Input() formatOnBlur: boolean = true;
118
- @Input() showErrorWhenTouched: boolean = true;
119
-
120
- /* Dropdown plumbing */
121
- @Input() dropdownAttachToBody: boolean = true;
122
- @Input() dropdownZIndex: number = 2000;
123
-
124
- /* Localization + RTL */
125
- @Input('i18n') i18n?: IntlTelI18n;
126
-
127
- @Input('telI18n') set telI18n(v: IntlTelI18n | undefined) {
128
- this.i18n = v;
129
- }
130
-
131
- @Input('localizedCountries') localizedCountries?: CountryMap;
132
-
133
- @Input('telLocalizedCountries') set telLocalizedCountries(v: CountryMap | undefined) {
134
- this.localizedCountries = v;
135
- }
136
-
137
- @Input() clearAriaLabel: string = 'Clear phone number';
138
- @Input() dir: 'ltr' | 'rtl' = 'ltr';
139
-
140
- /* Placeholders (intl-tel-input) */
141
- @Input() autoPlaceholder: 'off' | 'polite' | 'aggressive' = 'off'; // default OFF since no utils fallback
142
- @Input() utilsScript?: string;
143
- @Input() customPlaceholder?: (example: string, country: any) => string;
144
-
145
- @Input() formatWhenValid: 'off' | 'blur' | 'typing' = 'blur';
146
-
147
- /* Digits-only controls */
148
- @Input() digitsOnly: boolean = true;
149
- @Input() allowLeadingPlus: boolean = true;
150
-
151
- /* Outputs */
152
- @Output() countryChange = new EventEmitter<{ iso2: CountryCode }>();
153
- @Output() validityChange = new EventEmitter<boolean>();
154
- @Output() inputChange = new EventEmitter<{ raw: string; e164: string | null; iso2: CountryCode }>();
155
-
156
- /* Internal */
157
- private iti: IntlTelInstance | null = null;
158
- private onChange: (val: string | null) => void = () => {
159
- };
160
- private onTouchedCb: () => void = () => {
161
- };
162
- private validatorChange?: () => void;
163
- private lastEmittedValid = false;
164
- private pendingWrite: string | null = null;
165
- private touched: boolean = false;
166
-
167
- readonly resolvedId: string = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
168
-
169
- private readonly platformId = inject(PLATFORM_ID);
170
-
171
- constructor(
172
- private readonly zone: NgZone,
173
- private readonly tel: NgxsmkTelInputService
174
- ) {
175
- }
176
-
177
- ngAfterViewInit(): void {
178
- if (!isPlatformBrowser(this.platformId)) return;
179
- void this.initAndWire();
180
- }
181
-
182
- private async initAndWire(): Promise<void> {
183
- await this.initIntlTelInput();
184
- this.bindDomListeners();
185
-
186
- if (this.pendingWrite !== null) {
187
- this.setInputValue(this.pendingWrite);
188
- this.handleInput();
189
- this.pendingWrite = null;
190
- }
191
- if (this.autoFocus) setTimeout(() => this.focus(), 0);
192
- }
193
-
194
- ngOnChanges(changes: SimpleChanges): void {
195
- if (!isPlatformBrowser(this.platformId)) return;
196
- const configChanged = [
197
- 'initialCountry', 'preferredCountries', 'onlyCountries',
198
- 'separateDialCode', 'allowDropdown', 'nationalMode',
199
- 'i18n', 'localizedCountries', 'dir',
200
- 'autoPlaceholder', 'utilsScript', 'customPlaceholder',
201
- 'digitsOnly', 'allowLeadingPlus'
202
- ].some(k => k in changes && !changes[k]?.firstChange);
203
- if (configChanged && this.iti) {
204
- this.reinitPlugin();
205
- this.validatorChange?.();
206
- }
207
- }
208
-
209
- ngOnDestroy(): void {
210
- this.destroyPlugin();
211
- }
212
-
213
- // ----- CVA -----
214
- writeValue(val: string | null): void {
215
- if (!this.inputRef) return;
216
- if (!this.iti) {
217
- this.pendingWrite = val ?? '';
218
- return;
219
- }
220
- this.setInputValue(val ?? '');
221
- }
222
-
223
- registerOnChange(fn: any): void {
224
- this.onChange = fn;
225
- }
226
-
227
- registerOnTouched(fn: any): void {
228
- this.onTouchedCb = fn;
229
- }
230
-
231
- setDisabledState(isDisabled: boolean): void {
232
- this.disabled = isDisabled;
233
- if (this.inputRef) this.inputRef.nativeElement.disabled = isDisabled;
234
- }
235
-
236
- // ----- Validator -----
237
- validate(_: AbstractControl): ValidationErrors | null {
238
- const raw = this.currentRaw();
239
- if (!raw) return null;
240
- const valid = this.tel.isValid(raw, this.currentIso2());
241
- if (valid !== this.lastEmittedValid) {
242
- this.lastEmittedValid = valid;
243
- this.validityChange.emit(valid);
244
- }
245
- return valid ? null : {phoneInvalid: true};
246
- }
247
-
248
- registerOnValidatorChange(fn: () => void): void {
249
- this.validatorChange = fn;
250
- }
251
-
252
- // ----- Public helpers -----
253
- focus(): void {
254
- this.inputRef?.nativeElement.focus();
255
- if (this.selectOnFocus) {
256
- const el = this.inputRef.nativeElement;
257
- queueMicrotask(() => el.setSelectionRange(0, el.value.length));
258
- }
259
- }
260
-
261
- selectCountry(iso2: CountryCode): void {
262
- if (this.iti) {
263
- this.iti.setCountry(iso2.toLowerCase());
264
- this.handleInput();
265
- }
266
- }
267
-
268
- clearInput() {
269
- this.setInputValue('');
270
- this.handleInput();
271
- this.inputRef.nativeElement.focus();
272
- }
273
-
274
- // ----- Plugin wiring -----
275
- private async initIntlTelInput() {
276
- const [{default: intlTelInput}] = await Promise.all([import('intl-tel-input')]);
277
-
278
- const toLowerKeys = (m?: CountryMap) => {
279
- if (!m) return undefined;
280
- const out: Record<string, string> = {};
281
- for (const k in m) {
282
- if (Object.prototype.hasOwnProperty.call(m, k)) {
283
- const v = (m as Record<string, string | undefined>)[k];
284
- if (v != null) out[k.toLowerCase()] = v;
285
- }
286
- }
287
- return out;
288
- };
289
-
290
- const config: any = {
291
- initialCountry: this.initialCountry === 'auto' ? 'auto' : (this.initialCountry?.toLowerCase() || 'us'),
292
- preferredCountries: (this.preferredCountries ?? []).map(c => c.toLowerCase()),
293
- onlyCountries: (this.onlyCountries ?? []).map(c => c.toLowerCase()),
294
- nationalMode: this.nationalMode,
295
- allowDropdown: this.allowDropdown,
296
- separateDialCode: this.separateDialCode,
297
- geoIpLookup: (cb: (iso2: string) => void) => cb('us'),
298
-
299
- // placeholders
300
- autoPlaceholder: this.autoPlaceholder,
301
- utilsScript: this.utilsScript,
302
- customPlaceholder: this.customPlaceholder,
303
-
304
- // localization
305
- i18n: this.i18n,
306
- localizedCountries: toLowerKeys(this.localizedCountries),
307
-
308
- // dropdown container
309
- dropdownContainer: this.dropdownAttachToBody && typeof document !== 'undefined' ? document.body : undefined
310
- };
311
-
312
- this.zone.runOutsideAngular(() => {
313
- this.iti = intlTelInput(this.inputRef.nativeElement, config);
314
- });
315
-
316
- (this.inputRef.nativeElement as HTMLElement).style.setProperty('--tel-dd-z', String(this.dropdownZIndex));
317
- }
318
-
319
- private reinitPlugin() {
320
- const current = this.currentRaw();
321
- this.destroyPlugin();
322
- this.initIntlTelInput().then(() => {
323
- if (current) {
324
- this.setInputValue(current);
325
- this.handleInput();
326
- }
327
- });
328
- }
329
-
330
- private destroyPlugin() {
331
- if (this.iti) {
332
- this.iti.destroy();
333
- this.iti = null;
334
- }
335
- if (this.inputRef?.nativeElement) {
336
- const el = this.inputRef.nativeElement;
337
- const clone = el.cloneNode(true) as HTMLInputElement;
338
- el.parentNode?.replaceChild(clone, el);
339
- (this.inputRef as any).nativeElement = clone;
340
- }
341
- }
342
-
343
- // ----- Input filtering (digits-only) -----
344
- private sanitizeDigits(value: string): string {
345
- if (!this.digitsOnly) return value;
346
- let v = value.replace(/[^\d+]/g, '');
347
- if (this.allowLeadingPlus) {
348
- const hasLeadingPlus = v.startsWith('+');
349
- v = (hasLeadingPlus ? '+' : '') + v.replace(/\+/g, '');
350
- } else {
351
- v = v.replace(/\+/g, '');
352
- }
353
- return v;
354
- }
355
-
356
- private bindDomListeners() {
357
- const el = this.inputRef.nativeElement;
358
-
359
- this.zone.runOutsideAngular(() => {
360
- el.addEventListener('beforeinput', (ev: InputEvent) => {
361
- if (!this.digitsOnly) return;
362
- const data = (ev as any).data as string | null;
363
- if (!data || ev.inputType !== 'insertText') return;
364
-
365
- const pos = el.selectionStart ?? 0;
366
- const isDigit = data >= '0' && data <= '9';
367
- const isPlusAtStart = this.allowLeadingPlus && data === '+' && pos === 0 && !el.value.includes('+');
368
-
369
- if (!isDigit && !isPlusAtStart) ev.preventDefault();
370
- });
371
-
372
- el.addEventListener('paste', (e: ClipboardEvent) => {
373
- if (!this.digitsOnly) return;
374
- e.preventDefault();
375
- const text = (e.clipboardData || (window as any).clipboardData).getData('text');
376
- const sanitized = this.sanitizeDigits(text);
377
- const start = el.selectionStart ?? el.value.length;
378
- const end = el.selectionEnd ?? el.value.length;
379
- el.setRangeText(sanitized, start, end, 'end');
380
- queueMicrotask(() => this.handleInput());
381
- });
382
-
383
- el.addEventListener('input', () => {
384
- if (this.digitsOnly) {
385
- const val = el.value;
386
- const sanitized = this.sanitizeDigits(val);
387
- if (val !== sanitized) {
388
- const caret = el.selectionStart ?? sanitized.length;
389
- el.value = sanitized;
390
- el.setSelectionRange(caret, caret);
391
- }
392
- }
393
- this.handleInput();
394
- });
395
-
396
- el.addEventListener('countrychange', () => {
397
- const iso2 = this.currentIso2();
398
- this.zone.run(() => {
399
- this.countryChange.emit({iso2});
400
- this.validatorChange?.();
401
- });
402
- this.handleInput();
403
- });
404
-
405
- el.addEventListener('blur', () => this.onBlur());
406
- });
407
- }
408
-
409
- onBlur() {
410
- this.touched = true;
411
- this.zone.run(() => this.onTouchedCb());
412
- if (!this.formatOnBlur) return;
413
- const raw = this.currentRaw();
414
- if (!raw) return;
415
- const parsed = this.tel.parse(raw, this.currentIso2());
416
- if (this.nationalMode && parsed.national) {
417
- this.setInputValue((parsed.national || '').replace(/\s{2,}/g, ' '));
418
- }
419
- }
420
-
421
- onFocus() {
422
- if (this.selectOnFocus) {
423
- const el = this.inputRef.nativeElement;
424
- queueMicrotask(() => el.setSelectionRange(0, el.value.length));
425
- }
426
- }
427
-
428
- private handleInput() {
429
- const raw = this.currentRaw();
430
- const iso2 = this.currentIso2();
431
- const parsed = this.tel.parse(raw, iso2);
432
- this.zone.run(() => this.onChange(parsed.e164)); // E.164 or null
433
- this.zone.run(() => this.inputChange.emit({raw, e164: parsed.e164, iso2}));
434
- if (raw && this.nationalMode && parsed.national) {
435
- const normalized = parsed.national.replace(/\s{2,}/g, ' ');
436
- if (normalized !== raw) this.setInputValue(normalized);
437
- }
438
- }
439
-
440
- currentRaw(): string {
441
- return (this.inputRef?.nativeElement.value ?? '').trim();
442
- }
443
-
444
- private currentIso2(): CountryCode {
445
- const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US')
446
- .toString().toUpperCase();
447
- return iso2 as CountryCode;
448
- }
449
-
450
- private setInputValue(v: string) {
451
- this.inputRef.nativeElement.value = v ?? '';
452
- }
453
-
454
- get showError(): boolean {
455
- const invalid = !!this.validate({} as AbstractControl);
456
- return this.showErrorWhenTouched ? (this.touched && invalid) : invalid;
457
- }
458
- }
@@ -1,15 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { NgxsmkTelInputService } from './ngxsmk-tel-input.service';
3
-
4
- describe('NgxsmkTelInputService', () => {
5
- let service: NgxsmkTelInputService;
6
-
7
- beforeEach(() => {
8
- TestBed.configureTestingModule({});
9
- service = TestBed.inject(NgxsmkTelInputService);
10
- });
11
-
12
- it('should be created', () => {
13
- expect(service).toBeTruthy();
14
- });
15
- });
@@ -1,17 +0,0 @@
1
- import { Injectable } from '@angular/core';
2
- import { parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js';
3
-
4
- @Injectable({ providedIn: 'root' })
5
- export class NgxsmkTelInputService {
6
- parse(input: string, iso2: CountryCode): { e164: string | null; national: string | null; isValid: boolean } {
7
- const phone = parsePhoneNumberFromString(input || '', iso2);
8
- if (!phone) return { e164: null, national: null, isValid: false };
9
- const isValid = phone.isValid();
10
- return { e164: isValid ? phone.number : null, national: phone.formatNational(), isValid };
11
- }
12
-
13
- isValid(input: string, iso2: CountryCode): boolean {
14
- const phone = parsePhoneNumberFromString(input || '', iso2);
15
- return !!phone && phone.isValid();
16
- }
17
- }
package/src/lib/types.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { CountryCode } from 'libphonenumber-js';
2
-
3
- export type CountryMap = Partial<Record<CountryCode, string>>;
4
-
5
- export interface IntlTelI18n {
6
- selectedCountryAriaLabel?: string;
7
- countryListAriaLabel?: string;
8
- searchPlaceholder?: string;
9
- zeroSearchResults?: string;
10
- noCountrySelected?: string;
11
- }
package/src/public-api.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './lib/ngxsmk-tel-input.component';
2
- export * from './lib/ngxsmk-tel-input.service';
3
- export type { CountryMap, IntlTelI18n } from './lib/types';
package/tsconfig.lib.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "../../out-tsc/lib",
5
- "declaration": true,
6
- "declarationMap": true,
7
- "inlineSources": true,
8
- "types": []
9
- },
10
- "exclude": [
11
- "**/*.spec.ts"
12
- ]
13
- }
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "./tsconfig.lib.json",
3
- "compilerOptions": {
4
- "declarationMap": false
5
- },
6
- "angularCompilerOptions": {
7
- "compilationMode": "partial"
8
- }
9
- }
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "../../out-tsc/spec",
5
- "types": [
6
- "jasmine"
7
- ]
8
- },
9
- "include": [
10
- "**/*.spec.ts",
11
- "**/*.d.ts"
12
- ]
13
- }