@tapsellorg/angular-material-library
Version:
Angular library for Tapsell
347 lines (342 loc) • 18 kB
JavaScript
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