@uiowa/uiowa-mfk
Version:
mfk, uiowa-mfk, uiowa-favorite-mfk, mfk-string
501 lines (490 loc) • 24.1 kB
JavaScript
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