|
| 1 | +/** |
| 2 | + * Use of this source code is governed by an MIT-style license that can be |
| 3 | + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE |
| 4 | + */ |
| 5 | + |
| 6 | +import { BACKSPACE } from '@angular/cdk/keycodes'; |
| 7 | +import { |
| 8 | + booleanAttribute, |
| 9 | + ChangeDetectionStrategy, |
| 10 | + Component, |
| 11 | + ElementRef, |
| 12 | + forwardRef, |
| 13 | + Input, |
| 14 | + numberAttribute, |
| 15 | + OnChanges, |
| 16 | + QueryList, |
| 17 | + SimpleChanges, |
| 18 | + ViewChildren, |
| 19 | + ViewEncapsulation |
| 20 | +} from '@angular/core'; |
| 21 | +import { |
| 22 | + ControlValueAccessor, |
| 23 | + FormArray, |
| 24 | + FormBuilder, |
| 25 | + FormControl, |
| 26 | + NG_VALUE_ACCESSOR, |
| 27 | + ReactiveFormsModule, |
| 28 | + Validators |
| 29 | +} from '@angular/forms'; |
| 30 | +import { takeUntil, tap } from 'rxjs/operators'; |
| 31 | + |
| 32 | +import { NzDestroyService } from 'ng-zorro-antd/core/services'; |
| 33 | +import { NzSafeAny, NzSizeLDSType, NzStatus, OnTouchedType } from 'ng-zorro-antd/core/types'; |
| 34 | + |
| 35 | +import { NzInputDirective } from './input.directive'; |
| 36 | + |
| 37 | +@Component({ |
| 38 | + selector: 'nz-input-otp', |
| 39 | + exportAs: 'nzInputOtp', |
| 40 | + preserveWhitespaces: false, |
| 41 | + encapsulation: ViewEncapsulation.None, |
| 42 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 43 | + template: ` |
| 44 | + @for (item of otpArray.controls; track $index) { |
| 45 | + <input |
| 46 | + nz-input |
| 47 | + class="ant-otp-input" |
| 48 | + type="text" |
| 49 | + maxlength="1" |
| 50 | + size="1" |
| 51 | + [nzSize]="nzSize" |
| 52 | + [formControl]="item" |
| 53 | + [nzStatus]="nzStatus" |
| 54 | + (input)="onInput($index, $event)" |
| 55 | + (focus)="onFocus($event)" |
| 56 | + (keydown)="onKeyDown($index, $event)" |
| 57 | + (paste)="onPaste($index, $event)" |
| 58 | + #otpInput |
| 59 | + /> |
| 60 | + } |
| 61 | + `, |
| 62 | + host: { |
| 63 | + class: 'ant-otp' |
| 64 | + }, |
| 65 | + providers: [ |
| 66 | + { |
| 67 | + provide: NG_VALUE_ACCESSOR, |
| 68 | + useExisting: forwardRef(() => NzInputOtpComponent), |
| 69 | + multi: true |
| 70 | + }, |
| 71 | + NzDestroyService |
| 72 | + ], |
| 73 | + imports: [NzInputDirective, ReactiveFormsModule], |
| 74 | + standalone: true |
| 75 | +}) |
| 76 | +export class NzInputOtpComponent implements ControlValueAccessor, OnChanges { |
| 77 | + @ViewChildren('otpInput') otpInputs!: QueryList<ElementRef>; |
| 78 | + |
| 79 | + @Input({ transform: numberAttribute }) nzLength: number = 6; |
| 80 | + @Input() nzSize: NzSizeLDSType = 'default'; |
| 81 | + @Input({ transform: booleanAttribute }) disabled = false; |
| 82 | + @Input() nzStatus: NzStatus = ''; |
| 83 | + @Input() nzFormatter: (value: string) => string = value => value; |
| 84 | + @Input() nzMask: string | null = null; |
| 85 | + |
| 86 | + protected otpArray!: FormArray<FormControl<string>>; |
| 87 | + private internalValue: string[] = []; |
| 88 | + private onChangeCallback?: (_: NzSafeAny) => void; |
| 89 | + onTouched: OnTouchedType = () => {}; |
| 90 | + |
| 91 | + constructor( |
| 92 | + private readonly formBuilder: FormBuilder, |
| 93 | + private readonly nzDestroyService: NzDestroyService |
| 94 | + ) { |
| 95 | + this.createFormArray(); |
| 96 | + } |
| 97 | + |
| 98 | + ngOnChanges(changes: SimpleChanges): void { |
| 99 | + if (changes['nzLength']?.currentValue) { |
| 100 | + this.createFormArray(); |
| 101 | + } |
| 102 | + |
| 103 | + if (changes['disabled']) { |
| 104 | + this.setDisabledState(this.disabled); |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + onInput(index: number, event: Event): void { |
| 109 | + const inputElement = event.target as HTMLInputElement; |
| 110 | + const nextInput = this.otpInputs.toArray()[index + 1]; |
| 111 | + |
| 112 | + if (inputElement.value && nextInput) { |
| 113 | + nextInput.nativeElement.focus(); |
| 114 | + } else if (!nextInput) { |
| 115 | + this.selectInputBox(index); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + onFocus(event: FocusEvent): void { |
| 120 | + const inputElement = event.target as HTMLInputElement; |
| 121 | + inputElement.select(); |
| 122 | + } |
| 123 | + |
| 124 | + onKeyDown(index: number, event: KeyboardEvent): void { |
| 125 | + const previousInput = this.otpInputs.toArray()[index - 1]; |
| 126 | + |
| 127 | + if (event.keyCode === BACKSPACE) { |
| 128 | + event.preventDefault(); |
| 129 | + |
| 130 | + this.internalValue[index] = ''; |
| 131 | + this.otpArray.at(index).setValue('', { emitEvent: false }); |
| 132 | + |
| 133 | + if (previousInput) { |
| 134 | + this.selectInputBox(index - 1); |
| 135 | + } |
| 136 | + |
| 137 | + this.emitValue(); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + writeValue(value: string): void { |
| 142 | + if (!value) { |
| 143 | + this.otpArray.reset(); |
| 144 | + return; |
| 145 | + } |
| 146 | + |
| 147 | + const controlValues = value.split(''); |
| 148 | + this.internalValue = controlValues; |
| 149 | + |
| 150 | + controlValues.forEach((val, i) => { |
| 151 | + const formattedValue = this.nzFormatter(val); |
| 152 | + const value = this.nzMask ? this.nzMask : formattedValue; |
| 153 | + this.otpArray.at(i).setValue(value, { emitEvent: false }); |
| 154 | + }); |
| 155 | + } |
| 156 | + |
| 157 | + registerOnChange(fn: (value: string) => void): void { |
| 158 | + this.onChangeCallback = fn; |
| 159 | + } |
| 160 | + |
| 161 | + registerOnTouched(fn: () => {}): void { |
| 162 | + this.onTouched = fn; |
| 163 | + } |
| 164 | + |
| 165 | + setDisabledState(isDisabled: boolean): void { |
| 166 | + if (isDisabled) { |
| 167 | + this.otpArray.disable(); |
| 168 | + } else { |
| 169 | + this.otpArray.enable(); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + onPaste(index: number, event: ClipboardEvent): void { |
| 174 | + const pastedText = event.clipboardData?.getData('text') || ''; |
| 175 | + if (!pastedText) return; |
| 176 | + |
| 177 | + let currentIndex = index; |
| 178 | + for (const char of pastedText.split('')) { |
| 179 | + if (currentIndex < this.nzLength) { |
| 180 | + const formattedChar = this.nzFormatter(char); |
| 181 | + this.internalValue[currentIndex] = char; |
| 182 | + const maskedValue = this.nzMask ? this.nzMask : formattedChar; |
| 183 | + this.otpArray.at(currentIndex).setValue(maskedValue, { emitEvent: false }); |
| 184 | + currentIndex++; |
| 185 | + } else { |
| 186 | + break; |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + event.preventDefault(); // this line is needed, otherwise the last index that is going to be selected will also be filled (in the next line). |
| 191 | + this.selectInputBox(currentIndex); |
| 192 | + this.emitValue(); |
| 193 | + } |
| 194 | + |
| 195 | + private createFormArray(): void { |
| 196 | + this.otpArray = this.formBuilder.array<FormControl<string>>([]); |
| 197 | + this.internalValue = new Array(this.nzLength).fill(''); |
| 198 | + |
| 199 | + for (let i = 0; i < this.nzLength; i++) { |
| 200 | + const control = this.formBuilder.nonNullable.control('', [Validators.required]); |
| 201 | + |
| 202 | + control.valueChanges |
| 203 | + .pipe( |
| 204 | + tap(value => { |
| 205 | + const unmaskedValue = this.nzFormatter(value); |
| 206 | + this.internalValue[i] = unmaskedValue; |
| 207 | + |
| 208 | + control.setValue(this.nzMask ?? unmaskedValue, { emitEvent: false }); |
| 209 | + |
| 210 | + this.emitValue(); |
| 211 | + }), |
| 212 | + takeUntil(this.nzDestroyService) |
| 213 | + ) |
| 214 | + .subscribe(); |
| 215 | + |
| 216 | + this.otpArray.push(control); |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + private emitValue(): void { |
| 221 | + const result = this.internalValue.join(''); |
| 222 | + if (this.onChangeCallback) { |
| 223 | + this.onChangeCallback(result); |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + private selectInputBox(index: number): void { |
| 228 | + const otpInputArray = this.otpInputs.toArray(); |
| 229 | + if (index >= otpInputArray.length) index = otpInputArray.length - 1; |
| 230 | + |
| 231 | + otpInputArray[index].nativeElement.select(); |
| 232 | + } |
| 233 | +} |
0 commit comments