UNPKG

@uiowa/uiowa-mfk

Version:

mfk, uiowa-mfk, uiowa-favorite-mfk, mfk-string

501 lines (490 loc) 24.1 kB
import * as i0 from '@angular/core'; import { output, input, viewChildren, effect, Input, ChangeDetectionStrategy, Component, Pipe } from '@angular/core'; import * as i1 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { DigitOnlyDirective } from '@uiowa/digit-only'; class MfkFieldName { static FUND = 'fund'; static ORG = 'org'; static DEPT = 'dept'; static SUBDEPT = 'subdept'; static GRANTPGM = 'grantpgm'; static IACT = 'iact'; static OACT = 'oact'; static DACT = 'dact'; static FN = 'fn'; static CCTR = 'cctr'; static BRF = 'brf'; } /** * Options for MFK field. * * allows to set field default value, set readonly attribute, set validation regex pattern. Example usage: * * ```typescript * new MfkFieldOption(MfkFieldName.IACT, '6218') * new MfkFieldOption(MfkFieldName.IACT, '6218', true) * new MfkFieldOption(MfkFieldName.BRF) * ``` */ class MfkFieldOption { name; defaultValue; readonly; valuePattern; label; width; length; numericRegex = '^[0-9]+$'; /** * Options for MFK field. * * allows to set field default value, set readonly attribute, set validation regex pattern. Example usage: * * ```typescript * new MfkFieldOption(MfkFieldName.IACT, '6218') * new MfkFieldOption(MfkFieldName.IACT, '6218', true) * new MfkFieldOption(MfkFieldName.BRF) * ``` * * @param name (Required) the input field name. Use MfkFieldName type to get a proper value. * @param defaultValue (Optional) set a default value for this field. Default: ''. * @param readonly (Optional) set to true if the input field is readonly. Default: false. * @param valuePattern (Optional) set a regex for this field. Default: '^[0-9]+$'. */ constructor(name, defaultValue = '', readonly = false, valuePattern = '^[0-9]+$') { this.name = name; this.defaultValue = defaultValue; this.readonly = readonly; this.valuePattern = valuePattern; switch (name) { case MfkFieldName.FUND: this.label = 'Fund'; this.length = 3; break; case MfkFieldName.ORG: this.label = 'Org'; this.length = 2; break; case MfkFieldName.DEPT: this.label = 'Dept'; this.length = 4; break; case MfkFieldName.SUBDEPT: this.label = 'Subdept'; this.length = 5; break; case MfkFieldName.GRANTPGM: this.label = `Grant/Pgm`; this.length = 8; break; case MfkFieldName.IACT: this.label = 'Iact'; this.length = 4; break; case MfkFieldName.OACT: this.label = 'Oact'; this.length = 3; break; case MfkFieldName.DACT: this.label = 'Dact'; this.length = 5; break; case MfkFieldName.FN: this.label = 'Fn'; this.length = 2; break; case MfkFieldName.CCTR: this.label = 'Cctr'; this.length = 4; break; case MfkFieldName.BRF: this.label = 'Brf'; this.length = 2; break; default: throw new Error(`MFK field name [${name.toUpperCase()}] is invalid.`); } this.width = this.length * 0.65 + 0.75; if (!valuePattern) { valuePattern = this.numericRegex; } if (this.readonly) { if (!this.defaultValue) { throw new Error(`Default value for readonly field [${name.toUpperCase()}] is required.`); } return; // if readonly, then don't validate default value } if (defaultValue) { if (defaultValue.length !== this.length) { throw new Error(`The default value [${defaultValue}] for ${name.toUpperCase()} is not ${this.length} digits long.`); } let reg = new RegExp(this.numericRegex); if (!reg.test(defaultValue)) { throw new Error(`The default value [${defaultValue}] for ${name.toUpperCase()} is not a number.`); } if (valuePattern !== this.numericRegex) { reg = new RegExp(valuePattern); if (!reg.test(defaultValue)) { throw new Error(`The default value [${defaultValue}] for ${name.toUpperCase()} doesn't match RegEx "${valuePattern}".`); } } } } } /** * checks the equality of two Mfk objects * @param mfk1 an Mfk object * @param mfk2 an Mfk object */ function areEqual(mfk1, mfk2) { if (!mfk1 || !mfk2) { return false; } if (Object.keys(mfk1).length !== Object.keys(mfk2).length) { return false; } for (const k of Object.keys(mfk1)) { if (mfk1[k] !== mfk2[k]) { return false; } } return true; } /** * converts an Mfk object to a string by joining 10 fields with '-' symbol. * @param mfk An Mfk object */ function stringify(mfk) { if (!mfk) { return ''; } const s = Object.keys(mfk) .filter((k) => k !== MfkFieldName.BRF) .map((k) => mfk[k]) .join('-'); return s; } /** * creates an empty Mfk object with all 10 fields being empty string */ function emptyMfk() { return { fund: '', org: '', dept: '', subdept: '', grantpgm: '', iact: '', oact: '', dact: '', fn: '', cctr: '', }; } /** * checks if an Mfk object is in a valid format */ function validFormat(mfk) { var mfkString = stringify(mfk); if (mfkString.length !== 49) { return false; } if (mfk.brf && mfk.brf.length !== 2) { return false; } return true; } /** * checks if an Mfk string is in a valid format */ function validMfkString(mfkString) { const s = mfkString.replace(/\D/g, ''); return s.length >= 40; } /** * checks if each field of an Mfk object is in a valid format * returns an array of error messages * If the format is valid, then the returning array is empty. */ function validateStructure(mfk) { const result = []; if (mfk.fund?.length !== 3) { result.push(`Invalid Fund: Length is incorrect (must be 3 digits)`); } if (mfk.org?.length !== 2) { result.push(`Invalid Org: Length is incorrect (must be 2 digits)`); } if (mfk.dept?.length !== 4) { result.push(`Invalid Dept: Length is incorrect (must be 4 digits)`); } if (mfk.subdept?.length !== 5) { result.push(`Invalid Subdept: Length is incorrect (must be 5 digits)`); } if (mfk.grantpgm?.length !== 8) { result.push(`Invalid Grant/Pgm: Length is incorrect (must be 8 digits)`); } if (mfk.iact?.length !== 4) { result.push(`Invalid Iact: Length is incorrect (must be 4 digits)`); } if (mfk.oact?.length !== 3) { result.push(`Invalid Oact: Length is incorrect (must be 3 digits)`); } if (mfk.dact?.length !== 5) { result.push(`Invalid Dact: Length is incorrect (must be 5 digits)`); } if (mfk.fn?.length !== 2) { result.push(`Invalid Fn: Length is incorrect (must be 2 digits)`); } if (mfk.cctr?.length !== 4) { result.push(`Invalid Cctr: Length is incorrect (must be 4 digits)`); } if (mfk.brf && mfk.brf.length !== 2) { result.push(`Invalid Brf: Length is incorrect (must be 2 digits)`); } return result; } /** * Convert a string to an MFK object * @param mfkString MFK string * @returns MFK object */ function toMfk(mfkString) { const mfk = {}; const s = mfkString.replace(/\D/g, ''); if (s && s.length >= 40) { mfk.fund = s.substring(0, 3); mfk.org = s.substring(3, 5); mfk.dept = s.substring(5, 9); mfk.subdept = s.substring(9, 14); mfk.grantpgm = s.substring(14, 22); mfk.iact = s.substring(22, 26); mfk.oact = s.substring(26, 29); mfk.dact = s.substring(29, 34); mfk.fn = s.substring(34, 36); mfk.cctr = s.substring(36, 40); if (s.length >= 42) { mfk.brf = s.substring(40, 42); } } return mfk; } class MfkInput { _mfk = emptyMfk(); set mfk(mfk) { const m = Object.assign(emptyMfk(), mfk); this._options .filter((o) => o.defaultValue) .forEach((o) => (m[o.name] = m[o.name] || o.defaultValue)); this._options.filter((o) => o.readonly).forEach((o) => (m[o.name] = o.defaultValue)); this._mfk = m; } get mfk() { return this._mfk; } mfkChange = output(); options = input([], { ...(ngDevMode ? { debugName: "options" } : {}) }); _options = []; mfkInputFields = viewChildren(DigitOnlyDirective, { ...(ngDevMode ? { debugName: "mfkInputFields" } : {}) }); elementId = 'mfk-container_'; elementName = 'mfk-container_'; constructor(el) { const rand = Math.random().toString(36).substring(2); this.elementId += el.nativeElement.getAttribute('id') || el.nativeElement.getAttribute('name') || rand; this.elementName += el.nativeElement.getAttribute('name') || el.nativeElement.getAttribute('id') || rand; effect(() => { this._options = this.mergeOptions(this.options()); this.mfk = this.mfk; this.mfkChange.emit(this.mfk); }); } ngOnChanges(changes) { if (changes['mfk'] && changes['mfk'].currentValue) { if (!areEqual(changes['mfk'].previousValue, changes['mfk'].currentValue)) { this.mfk = changes['mfk'].currentValue; this.mfkChange.emit(this.mfk); } } } paste(e) { const pastedInput = e.clipboardData?.getData('text/plain').replace(/\D/g, '') ?? ''; // get a digit-only string if (!pastedInput) { return; } if (pastedInput.length >= 40) { const mfk_temp = toMfk(pastedInput); if (!areEqual(this.mfk, mfk_temp)) { this.mfk = mfk_temp; this.originalMfk = stringify(this.mfk); this.mfkChange.emit(this.mfk); } } } originalMfk = ''; onKeydown(e) { this.originalMfk = stringify(this.mfk); const target = e.target; const fieldName = target.name; // handle "tab" key --> auto fill '0's if the input field has not completed if (e.key === 'Tab') { if (target.readOnly) { return; } while (this.mfk[fieldName].length < target.maxLength) { this.mfk[fieldName] = this.mfk[fieldName].concat('0'); } } // handle "backspace" key if ((e.key === 'Backspace' || e.code === 'Backspace') && this.mfk[fieldName]?.length === 0) { const currentInputFieldIndex = this._options.findIndex((o) => o.name === fieldName); // auto jump to the previous input field when current field is empty for (let i = currentInputFieldIndex - 1; i >= 0; i--) { if (this._options[i].readonly) { continue; } const prevInputField = this.mfkInputFields().find((v) => v.el.nativeElement['name'] === this._options[i].name); prevInputField?.el.nativeElement.focus(); break; } } } onKeyup(e) { if (this.originalMfk !== stringify(this.mfk)) { this.mfkChange.emit(this.mfk); } if (isNaN(Number(e.key))) { return; // only numbers can trigger auto jump feature. } const target = e.target; const fieldName = target.name; if (this.mfk[fieldName]?.length === target.maxLength) { // auto jump to the next input field when current field is full const currentInputFieldIndex = this._options.findIndex((o) => o.name === fieldName); for (let i = currentInputFieldIndex + 1; i < this._options.length; i++) { if (this._options[i].readonly) { continue; } const nextInputField = this.mfkInputFields().find((v) => v.el.nativeElement['name'] === this._options[i].name); nextInputField?.el.nativeElement.focus(); break; } } } mergeOptions(options = []) { const result = [ new MfkFieldOption(MfkFieldName.FUND), new MfkFieldOption(MfkFieldName.ORG), new MfkFieldOption(MfkFieldName.DEPT), new MfkFieldOption(MfkFieldName.SUBDEPT), new MfkFieldOption(MfkFieldName.GRANTPGM), new MfkFieldOption(MfkFieldName.IACT), new MfkFieldOption(MfkFieldName.OACT), new MfkFieldOption(MfkFieldName.DACT), new MfkFieldOption(MfkFieldName.FN), new MfkFieldOption(MfkFieldName.CCTR), ]; if (options && options.length > 0) { for (const option of options) { const fieldOption = result.find((o) => o.name === option.name); if (fieldOption) { const index = result.indexOf(fieldOption); result[index] = option; } else { result.push(option); } } } return result; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkInput, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.1", type: MfkInput, isStandalone: true, selector: "uiowa-mfk-input", inputs: { mfk: { classPropertyName: "mfk", publicName: "mfk", isSignal: false, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mfkChange: "mfkChange" }, viewQueries: [{ propertyName: "mfkInputFields", predicate: DigitOnlyDirective, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<form id=\"{{ elementId }}\" name=\"{{ elementName }}\" [attr.name]=\"elementName\" class=\"mfk-container\">\r\n @for (op of _options; track $index) {\r\n <div class=\"mfk-field\">\r\n <label for=\"{{ elementId + op.name }}\">\r\n {{ op.label }}\r\n </label>\r\n <input\r\n type=\"text\"\r\n id=\"{{ elementId + op.name }}\"\r\n name=\"{{ op.name }}\"\r\n [attr.name]=\"op.name\"\r\n [style.width.rem]=\"op.width\"\r\n [attr.aria-label]=\"op.name\"\r\n [attr.maxlength]=\"op.length\"\r\n [readOnly]=\"op.readonly\"\r\n [(ngModel)]=\"mfk[op.name]\"\r\n (paste)=\"paste($event)\"\r\n (keyup)=\"onKeyup($event)\"\r\n (keydown)=\"onKeydown($event)\"\r\n inputmode=\"numeric\"\r\n pattern=\"[0-9]*\"\r\n digitOnly\r\n />\r\n </div>\r\n }\r\n</form>\r\n", styles: [":host{display:inline-flex}.mfk-field{display:inline-flex;flex-direction:column;vertical-align:middle;text-align:center;margin-right:.25rem}.mfk-field label{font-size:.625rem;margin-bottom:0;white-space:nowrap}.mfk-field input{display:block;padding:.375rem!important;font-size:1rem;font-weight:400;font-style:normal;font-variant:normal;text-align:center;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.mfk-field input:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.mfk-field input:disabled,.mfk-field input[readonly]{background-color:#e9ecef;opacity:1}\n"], dependencies: [{ kind: "directive", type: DigitOnlyDirective, selector: "[digitOnly]", inputs: ["decimal", "decimalSeparator", "allowNegatives", "allowPaste", "negativeSign", "min", "max", "pattern"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkInput, decorators: [{ type: Component, args: [{ selector: 'uiowa-mfk-input', imports: [DigitOnlyDirective, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form id=\"{{ elementId }}\" name=\"{{ elementName }}\" [attr.name]=\"elementName\" class=\"mfk-container\">\r\n @for (op of _options; track $index) {\r\n <div class=\"mfk-field\">\r\n <label for=\"{{ elementId + op.name }}\">\r\n {{ op.label }}\r\n </label>\r\n <input\r\n type=\"text\"\r\n id=\"{{ elementId + op.name }}\"\r\n name=\"{{ op.name }}\"\r\n [attr.name]=\"op.name\"\r\n [style.width.rem]=\"op.width\"\r\n [attr.aria-label]=\"op.name\"\r\n [attr.maxlength]=\"op.length\"\r\n [readOnly]=\"op.readonly\"\r\n [(ngModel)]=\"mfk[op.name]\"\r\n (paste)=\"paste($event)\"\r\n (keyup)=\"onKeyup($event)\"\r\n (keydown)=\"onKeydown($event)\"\r\n inputmode=\"numeric\"\r\n pattern=\"[0-9]*\"\r\n digitOnly\r\n />\r\n </div>\r\n }\r\n</form>\r\n", styles: [":host{display:inline-flex}.mfk-field{display:inline-flex;flex-direction:column;vertical-align:middle;text-align:center;margin-right:.25rem}.mfk-field label{font-size:.625rem;margin-bottom:0;white-space:nowrap}.mfk-field input{display:block;padding:.375rem!important;font-size:1rem;font-weight:400;font-style:normal;font-variant:normal;text-align:center;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.mfk-field input:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem #0d6efd40}.mfk-field input:disabled,.mfk-field input[readonly]{background-color:#e9ecef;opacity:1}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { mfk: [{ type: Input }], mfkChange: [{ type: i0.Output, args: ["mfkChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], mfkInputFields: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => DigitOnlyDirective), { isSignal: true }] }] } }); class MfkString { mfk = input(undefined, { ...(ngDevMode ? { debugName: "mfk" } : {}) }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkString, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.1", type: MfkString, isStandalone: true, selector: "uiowa-mfk-string", inputs: { mfk: { classPropertyName: "mfk", publicName: "mfk", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ` @if(mfk(); as mfk){ <span> <span>{{ mfk.fund }}</span> <span>-{{ mfk.org }}</span> <span>-{{ mfk.dept }}</span> <span>-{{ mfk.subdept }}</span> <span>-{{ mfk.grantpgm }}</span> <span>-{{ mfk.iact }}</span> <span>-{{ mfk.oact }}</span> <span>-{{ mfk.dact }}</span> <span>-{{ mfk.fn }}</span> <span>-{{ mfk.cctr }}</span> @if(mfk.brf){ <span>-{{ mfk.brf }}</span> } </span> }`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkString, decorators: [{ type: Component, args: [{ selector: 'uiowa-mfk-string', imports: [], template: ` @if(mfk(); as mfk){ <span> <span>{{ mfk.fund }}</span> <span>-{{ mfk.org }}</span> <span>-{{ mfk.dept }}</span> <span>-{{ mfk.subdept }}</span> <span>-{{ mfk.grantpgm }}</span> <span>-{{ mfk.iact }}</span> <span>-{{ mfk.oact }}</span> <span>-{{ mfk.dact }}</span> <span>-{{ mfk.fn }}</span> <span>-{{ mfk.cctr }}</span> @if(mfk.brf){ <span>-{{ mfk.brf }}</span> } </span> }`, changeDetection: ChangeDetectionStrategy.OnPush }] }], propDecorators: { mfk: [{ type: i0.Input, args: [{ isSignal: true, alias: "mfk", required: false }] }] } }); class MfkStringPipe { transform(value, ...args) { return stringify(value); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkStringPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.1", ngImport: i0, type: MfkStringPipe, isStandalone: true, name: "mfkString" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: MfkStringPipe, decorators: [{ type: Pipe, args: [{ name: 'mfkString' }] }] }); class WhoKeyStringPipe { transform(value, ...args) { if (!value) { return ''; } return (value.fund + '-' + value.org + '-' + value.dept + '-' + value.subdept + '-' + value.grantpgm + '-' + value.fn); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: WhoKeyStringPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.0.1", ngImport: i0, type: WhoKeyStringPipe, isStandalone: true, name: "whoKeyString" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.1", ngImport: i0, type: WhoKeyStringPipe, decorators: [{ type: Pipe, args: [{ name: 'whoKeyString' }] }] }); /* * Public API Surface of uiowa-mfk */ /** * Generated bundle index. Do not edit. */ export { MfkFieldName, MfkFieldOption, MfkInput, MfkString, MfkStringPipe, WhoKeyStringPipe, areEqual, emptyMfk, stringify, toMfk, validFormat, validMfkString, validateStructure }; //# sourceMappingURL=uiowa-uiowa-mfk.mjs.map