UNPKG

@tapsellorg/angular-material-library

Version:

Angular library for Tapsell

347 lines (342 loc) 18 kB
import * as i0 from '@angular/core'; import { InjectionToken, input, output, effect, ViewChildren, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core'; import { CommonModule } from '@angular/common'; const CodeInputComponentConfigToken = new InjectionToken('CodeInputComponentConfig'); const defaultComponentConfig = { codeLength: 4, inputType: 'tel', inputMode: 'numeric', initialFocusField: 0, isCharsCode: false, isCodeHidden: false, isPrevFocusableAfterClearing: true, isFocusingOnLastByClickIfFilled: false, code: '', disabled: false, autocapitalize: '', }; var InputState; (function (InputState) { InputState[InputState["ready"] = 0] = "ready"; InputState[InputState["reset"] = 1] = "reset"; })(InputState || (InputState = {})); class PghPinInputComponent { constructor() { this.codeLength = input(defaultComponentConfig.codeLength); this.inputType = input(defaultComponentConfig.inputType); this.inputMode = input(defaultComponentConfig.inputMode); this.initialFocusField = input(defaultComponentConfig.initialFocusField); this.isCharsCode = input(defaultComponentConfig.isCharsCode); this.isCodeHidden = input(defaultComponentConfig.isCodeHidden); this.isPrevFocusableAfterClearing = input(defaultComponentConfig.isPrevFocusableAfterClearing); this.isFocusingOnLastByClickIfFilled = input(defaultComponentConfig.isFocusingOnLastByClickIfFilled); this.disabled = input(defaultComponentConfig.disabled); this.autocapitalize = input(defaultComponentConfig.autocapitalize); this.code = input(defaultComponentConfig.code); this.codeChanged = output(); this.codeCompleted = output(); this.codeLengthEffect = effect(() => this.onCodeLengthChanges(this.codeLength())); this.codeEffect = effect(() => this.onInputCodeChanges(this.code())); this.placeholders = []; this.inputs = []; this.inputsStates = []; this.state = { isFocusingAfterAppearingCompleted: false, isInitialFocusFieldEnabled: false, }; } ngOnInit() { // defining the state this.state.isInitialFocusFieldEnabled = !this.isEmpty(this.initialFocusField()); // initiating the code this.onCodeLengthChanges(this.codeLength()); } ngAfterViewInit() { // initiation of the inputs this.inputsListSubscription = this.inputsList.changes.subscribe(this.onInputsListChanges.bind(this)); this.onInputsListChanges(this.inputsList); } ngAfterViewChecked() { this.focusOnInputAfterAppearing(); } ngOnDestroy() { if (this.inputsListSubscription) { this.inputsListSubscription.unsubscribe(); } } reset(isChangesEmitting = false) { // resetting the code to its initial value or to an empty value this.onInputCodeChanges(this.code()); if (this.state.isInitialFocusFieldEnabled) { // tslint:disable-next-line:no-non-null-assertion this.focusOnField(this.initialFocusField()); } if (isChangesEmitting) { this.emitChanges(); } } focusOnField(index) { if (index >= this.codeLength()) { throw new Error('The index of the focusing input box should be less than the codeLength.'); } this.inputs[index]?.focus(); } onClick(e) { // handle click events only if the the prop is enabled if (!this.isFocusingOnLastByClickIfFilled()) { return; } const { target } = e; const last = this.inputs[this.codeLength() - 1]; // already focused if (target === last) { return; } // check filling const isFilled = this.getCurrentFilledCode().length >= this.codeLength(); if (!isFilled) { return; } // focusing on the last input if is filled setTimeout(() => last?.focus()); } onInput(e, i) { const { target } = e; const value = e.data || target.value; if (this.isEmpty(value)) { return; } // only digits are allowed if isCharsCode flag is absent/false if (!this.canInputValue(value)) { e.preventDefault(); e.stopPropagation(); this.setInputValue(target, null); this.setStateForInput(target, InputState.reset); return; } const values = value.toString().trim().split(''); for (let j = 0; j < values.length; j++) { const index = j + i; if (index > this.codeLength() - 1) { break; } this.setInputValue(this.inputs[index], values[j]); } this.emitChanges(); const next = i + values.length; if (next > this.codeLength() - 1) { target.blur(); return; } this.inputs[next]?.focus(); } onPaste(e, i) { e.preventDefault(); e.stopPropagation(); const data = e.clipboardData ? e.clipboardData.getData('text').trim() : undefined; if (this.isEmpty(data)) { return; } // Convert paste text into iterable // tslint:disable-next-line:no-non-null-assertion const values = data.split(''); let valIndex = 0; for (let j = i; j < this.inputs.length; j++) { // The values end is reached. Loop exit if (valIndex === values.length) { break; } const input = this.inputs[j]; const val = values[valIndex]; // Cancel the loop when a value cannot be used if (!this.canInputValue(val)) { this.setInputValue(input, null); this.setStateForInput(input, InputState.reset); return; } this.setInputValue(input, val.toString()); valIndex++; } this.inputs[i].blur(); this.emitChanges(); } async onKeydown(e, i) { const { target } = e; const isTargetEmpty = this.isEmpty(target.value); const prev = i - 1; // processing only the backspace and delete key events const isBackspaceKey = await this.isBackspaceKey(e); const isDeleteKey = this.isDeleteKey(e); if (!isBackspaceKey && !isDeleteKey) { return; } e.preventDefault(); this.setInputValue(target, null); if (!isTargetEmpty) { this.emitChanges(); } // preventing to focusing on the previous field if it does not exist or the delete key has been pressed if (prev < 0 || isDeleteKey) { return; } if (isTargetEmpty || this.isPrevFocusableAfterClearing()) { this.inputs[prev]?.focus(); } } onInputCodeChanges(code) { if (!this.inputs.length) { return; } if (this.isEmpty(code)) { this.inputs.forEach((input) => { this.setInputValue(input, null); }); return; } const chars = (code?.toString().trim() ?? '').split(''); // checking if all the values are correct let isAllCharsAreAllowed = true; for (const char of chars) { if (!this.canInputValue(char)) { isAllCharsAreAllowed = false; break; } } this.inputs.forEach((input, index) => { const value = isAllCharsAreAllowed ? chars[index] : null; this.setInputValue(input, value); }); } onCodeLengthChanges(codeLength) { if (!codeLength) { return; } if (codeLength > this.placeholders.length) { const numbers = Array(codeLength - this.placeholders.length).fill(1); this.placeholders.splice(this.placeholders.length - 1, 0, ...numbers); } else if (codeLength < this.placeholders.length) { this.placeholders.splice(codeLength); } } onInputsListChanges(list) { if (list.length > this.inputs.length) { const inputsToAdd = list.filter((item, index) => index > this.inputs.length - 1); this.inputs.splice(this.inputs.length, 0, ...inputsToAdd.map(item => item.nativeElement)); const states = Array(inputsToAdd.length).fill(InputState.ready); this.inputsStates.splice(this.inputsStates.length, 0, ...states); } else if (list.length < this.inputs.length) { this.inputs.splice(list.length); this.inputsStates.splice(list.length); } // filling the inputs after changing of their count this.onInputCodeChanges(this.code()); } focusOnInputAfterAppearing() { if (!this.state.isInitialFocusFieldEnabled) { return; } if (this.state.isFocusingAfterAppearingCompleted) { return; } this.focusOnField(this.initialFocusField()); this.state.isFocusingAfterAppearingCompleted = this.initialFocusField() !== undefined && this.initialFocusField() !== null && document.activeElement === this.inputs[this.initialFocusField()]; } emitChanges() { setTimeout(() => this.emitCode(), 50); } emitCode() { const code = this.getCurrentFilledCode(); this.codeChanged.emit(code); if (code.length >= this.codeLength()) { this.codeCompleted.emit(code); } } getCurrentFilledCode() { let code = ''; for (const input of this.inputs) { if (!this.isEmpty(input.value)) { code += input.value; } } return code; } isBackspaceKey(e) { const isBackspace = (e.key && e.key.toLowerCase() === 'backspace') || (e.keyCode && e.keyCode === 8); if (isBackspace) { return Promise.resolve(true); } // process only key with placeholder keycode on android devices if (!e.keyCode || e.keyCode !== 229) { return Promise.resolve(false); } return new Promise(resolve => { setTimeout(() => { const input = e.target; const isReset = this.getStateForInput(input) === InputState.reset; if (isReset) { this.setStateForInput(input, InputState.ready); } // if backspace key pressed the caret will have position 0 (for single value field) resolve(input.selectionStart === 0 && !isReset); }); }); } isDeleteKey(e) { return (e.key && e.key.toLowerCase() === 'delete') || (e.keyCode && e.keyCode === 46); } setInputValue(input, value) { const isEmpty = this.isEmpty(value); const valueClassCSS = 'has-value'; const emptyClassCSS = 'empty'; if (isEmpty) { input.value = ''; input.classList.remove(valueClassCSS); input.parentElement.classList.add(emptyClassCSS); } else { input.value = value; input.classList.add(valueClassCSS); input.parentElement.classList.remove(emptyClassCSS); } } canInputValue(value) { if (this.isEmpty(value)) { return false; } const isDigitsValue = /^[0-9]+$/.test(value.toString()); return isDigitsValue || this.isCharsCode(); } setStateForInput(input, state) { const index = this.inputs.indexOf(input); if (index < 0) { return; } this.inputsStates[index] = state; } getStateForInput(input) { const index = this.inputs.indexOf(input); return this.inputsStates[index]; } isEmpty(value) { return value === null || value === undefined || !value.toString().length; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: PghPinInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", type: PghPinInputComponent, isStandalone: true, selector: "pgh-pin-input", inputs: { codeLength: { classPropertyName: "codeLength", publicName: "codeLength", isSignal: true, isRequired: false, transformFunction: null }, inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, initialFocusField: { classPropertyName: "initialFocusField", publicName: "initialFocusField", isSignal: true, isRequired: false, transformFunction: null }, isCharsCode: { classPropertyName: "isCharsCode", publicName: "isCharsCode", isSignal: true, isRequired: false, transformFunction: null }, isCodeHidden: { classPropertyName: "isCodeHidden", publicName: "isCodeHidden", isSignal: true, isRequired: false, transformFunction: null }, isPrevFocusableAfterClearing: { classPropertyName: "isPrevFocusableAfterClearing", publicName: "isPrevFocusableAfterClearing", isSignal: true, isRequired: false, transformFunction: null }, isFocusingOnLastByClickIfFilled: { classPropertyName: "isFocusingOnLastByClickIfFilled", publicName: "isFocusingOnLastByClickIfFilled", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autocapitalize: { classPropertyName: "autocapitalize", publicName: "autocapitalize", isSignal: true, isRequired: false, transformFunction: null }, code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { codeChanged: "codeChanged", codeCompleted: "codeCompleted" }, viewQueries: [{ propertyName: "inputsList", predicate: ["input"], descendants: true }], ngImport: i0, template: "<div class=\"wrapper-input\">\n @for (holder of placeholders; track $index) {\n <span [class.code-hidden]=\"isCodeHidden()\">\n <input\n #input\n (click)=\"onClick($event)\"\n (paste)=\"onPaste($event, $index)\"\n (input)=\"onInput($event, $index)\"\n (keydown)=\"onKeydown($event, $index)\"\n [type]=\"inputType()\"\n [disabled]=\"disabled()\"\n [attr.inputmode]=\"inputMode()\"\n [attr.autocapitalize]=\"autocapitalize()\"\n autocomplete=\"one-time-code\"\n />\n </span>\n }\n</div>\n", styles: [".wrapper-input{display:flex;transform:translateZ(0);font-size:inherit}.wrapper-input span{display:block;flex:1;padding-right:4px}.wrapper-input span:first-child{padding-left:0}.wrapper-input span:last-child{padding-right:0}.wrapper-input span.code-hidden input{-webkit-text-security:disc;-moz-text-security:disc}.wrapper-input input{width:100%;height:4.375em;color:inherit;background:transparent;text-align:center;font-size:inherit;font-weight:300;border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300);border-radius:5px;transform:translateZ(0);-webkit-transform:translate3d(0,0,0);outline:none}.wrapper-input input.has-value{border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300)}.wrapper-input input:focus{background:transparent;border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300);box-shadow:0 1px 5px var(--shadow)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: PghPinInputComponent, decorators: [{ type: Component, args: [{ selector: 'pgh-pin-input', imports: [CommonModule], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"wrapper-input\">\n @for (holder of placeholders; track $index) {\n <span [class.code-hidden]=\"isCodeHidden()\">\n <input\n #input\n (click)=\"onClick($event)\"\n (paste)=\"onPaste($event, $index)\"\n (input)=\"onInput($event, $index)\"\n (keydown)=\"onKeydown($event, $index)\"\n [type]=\"inputType()\"\n [disabled]=\"disabled()\"\n [attr.inputmode]=\"inputMode()\"\n [attr.autocapitalize]=\"autocapitalize()\"\n autocomplete=\"one-time-code\"\n />\n </span>\n }\n</div>\n", styles: [".wrapper-input{display:flex;transform:translateZ(0);font-size:inherit}.wrapper-input span{display:block;flex:1;padding-right:4px}.wrapper-input span:first-child{padding-left:0}.wrapper-input span:last-child{padding-right:0}.wrapper-input span.code-hidden input{-webkit-text-security:disc;-moz-text-security:disc}.wrapper-input input{width:100%;height:4.375em;color:inherit;background:transparent;text-align:center;font-size:inherit;font-weight:300;border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300);border-radius:5px;transform:translateZ(0);-webkit-transform:translate3d(0,0,0);outline:none}.wrapper-input input.has-value{border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300)}.wrapper-input input:focus{background:transparent;border:1px solid var(--gray-300);border-bottom:1px solid var(--gray-300);box-shadow:0 1px 5px var(--shadow)}\n"] }] }], propDecorators: { inputsList: [{ type: ViewChildren, args: ['input'] }] } }); /** * Generated bundle index. Do not edit. */ export { PghPinInputComponent }; //# sourceMappingURL=tapsellorg-angular-material-library-src-lib-pin-input.mjs.map