@stilljs/core
Version:
<div style="display:flex; justify-content: center"> <img src="@still/img/logo-no-bg.png" style="width: 5em;"/> </div>
439 lines (360 loc) • 17.4 kB
JavaScript
import { $stillconst } from "../../setup/constants.js";
export const validationPatterns = {
'number': /^\d{0,}$/,
'alphanumeric': /^[a-zA-Z0-9\s]{0,}$/,
'text': /^(.){0,}$/,
'email': /^[a-z0-9]{0,}(\@){1}[a-z0-9]{2,}(\.){1}[a-z0-9]{2,}$/,
'phone': /^[\+]{0,1}[\d \s]{8,}$/,
'date': /(\d){2}\-(\d){2}\-(\d){4}/,
'dateUS': /(\d){4}\-(\d){2}\-(\d){2}/,
}
const validationTriggers = {
typing: 'onkeyup',
losefocus: 'onblur',
focus: 'onfocus'
}
const validatorMinMaxTypes = ['number', 'date'];
export class BehaviorComponent {
$stillClassLvlSubscribers = [];
static currentFormsValidators = {};
#triggetSet = $stillconst.VALID_TRIGGER_SET;
#triggetOnType = $stillconst.VALID_TRIGGER_ONTYPE;
_const = $stillconst;
lang = 'PT';
behaviorEvtSubscriptions = {};
onChange(callback = (newState) => { }) {
this.$stillClassLvlSubscribers.push(callback);
}
notifySubscribers(state) {
this.$stillClassLvlSubscribers.forEach(
subscriber => subscriber(state)
);
}
static ignrKeys = [
'arrowleft', 'arrowright', 'arrowdown', 'arrowup', 'tab', 'space', 'meta',
'control', 'alt', 'shift', 'escape', 'end', 'home', 'insert', 'capslock'
]
/**
* @param {*} field
* @param {{ value: string, required: Blob, pattern: RegExp }} inpt
*/
onValueInput(e, field, inpt, formRef, cmp = null, reset = false) {
const fieldType = inpt?.type?.toLowerCase();
const fieldSrc = this.constructor.name == 'BehaviorComponent' ? cmp : this;
if(reset){
fieldSrc[field] = (fieldType == 'checkbox' || inpt.multiple) ? [] : '';
return;
}
const isOptList = ['radio','checkbox'].includes(fieldType);
if (e && !isOptList)
if (BehaviorComponent.ignrKeys.includes(e.key.toString().toLowerCase())) return;
const pattern = inpt.getAttribute('(validator)');
let required = inpt.getAttribute('(required)');
let validationTrigger = inpt.getAttribute('(validator-trigger)');
required = required == 'false' ? false : required;
let isTriggerSet = inpt.getAttribute(this.#triggetSet);
let isOntypeTrigger = inpt.getAttribute(this.#triggetOnType);
isTriggerSet = isTriggerSet == "false" ? false : isTriggerSet;
isOntypeTrigger = isOntypeTrigger == "false" ? false : isOntypeTrigger;
let isOptListValid = true;
if(isOptList || inpt.multiple){
if(fieldType == 'checkbox'){
let currentValues = ['',undefined].includes(fieldSrc[field].value) ? [] : fieldSrc[field].value;
if(inpt?.checked) currentValues.push(inpt.value);
else {
currentValues = currentValues.filter(v => v != inpt.value);
fieldSrc['st'+field+'cbRem'] = inpt.value;
}
fieldSrc['st'+field+'cbClk'] = true;
fieldSrc[field] = currentValues;
isOptListValid = currentValues.length > 0 ? true : false;
}
else {
//this is shared with both scenarios (entering/selecting a value and form validation)
//the assignment will not happen in case it's in the form validation flow
if(cmp == null) fieldSrc[field] = inpt.value;
}
} else
fieldSrc[field] = inpt.value;
/* If the validation trigger was set and it and it should not trigger
* when typing then its get stopped by return statement */
if (isTriggerSet && !isOntypeTrigger) return;
if (validationTrigger && !isOntypeTrigger) {
if (validationTriggers[validationTrigger] != validationTriggers.typing) {
const actualTrigger = validationTriggers[validationTrigger];
if (!inpt[actualTrigger] && (validationTrigger in validationTriggers)) {
/* Bellow line will pich the trigger and add as event to the input
* e.g input.onkeyup = () => {}; */
inpt[actualTrigger] = () => {
this.#handleInputValidation(inpt, field, formRef, pattern, required, isOptListValid);
}
inpt.setAttribute(this.#triggetSet, true);
inpt.setAttribute(this.#triggetOnType, false);
return;
} else {
inpt.setAttribute(this.#triggetSet, true);
inpt.setAttribute(this.#triggetOnType, true);
}
} else {
inpt.setAttribute(this.#triggetSet, true);
inpt.setAttribute(this.#triggetOnType, true);
}
}
/** In case no validation trigger was not set of set to typing (ontype/onkeyup)
* then validation function will be called everytime new character is entered */
return this.#handleInputValidation(inpt, field, formRef, pattern, required, isOptListValid, cmp);
}
#handleInputValidation(inpt, field, formRef, pattern, required, isOptListValid = null, cmp = null) {
let { value } = inpt;
// To address radio and checkbox edge case when running form validator
const isOptList = ['checkbox','radio'].includes(inpt.type);
const isCBInValidator = this.constructor.name == 'BehaviorComponent' && isOptList;
if(isCBInValidator) value = cmp[field].value.length > 0 ? cmp[field].value : '';
if('radio' == inpt.type && value == '') value = cmp[field].value;
let isValid = true, validation;
const fieldPath = `${this.constructor.name}${formRef && formRef != 'null' ? `-${formRef}` : ''}`;
if(!isCBInValidator && isOptList){
const numOutRangeMsg = this.#handleMinMaxValidation(inpt, pattern, value, fieldPath);
/** Only apply out of range validation in case there is a value in the input */
if (numOutRangeMsg && value != '')
this.#handleValidationWarning('add', inpt, fieldPath, numOutRangeMsg, 'range');
}
/** Remove Out of range warning if added before */
this.#handleValidationWarning('remove', inpt, fieldPath, 'to-remove', 'range');
if (pattern && value.trim() != '' || ('radio' == inpt.type)) {
let regex = pattern;
if (pattern in validationPatterns) {
regex = validationPatterns[pattern];
} else {
if(pattern){
const datePattern = this.#checkDatePattern(pattern);
if (datePattern) regex = datePattern;
if (!datePattern) regex = String.raw`${regex}`;
}
}
if (typeof regex == 'function') {
validation = regex(value);
if (!validation) isValid = false;
} else {
if(pattern){
pattern = validationPatterns[pattern];
validation = value.match(new RegExp(regex));
if (!validation || !validation[0]?.length) isValid = false;
}
else if(!isNaN(value) && 'radio' == inpt.type && value != '') isValid = true;
else if (value?.trim() == '' && required) isValid = false;
}
}
else if (isOptList && value.length > 0) isValid = true;
else if (value?.trim() == '' && required) isValid = false;
if(!isOptListValid) this.#handleValidationWarning('add', inpt, fieldPath);
else if (!isValid && !isOptList || (!isValid && 'radio' == inpt.type))
this.#handleValidationWarning('add', inpt, fieldPath);
else this.#handleValidationWarning('remove', inpt, fieldPath);
BehaviorComponent.setValidatorForField(fieldPath, field);
BehaviorComponent.currentFormsValidators[fieldPath][field]['isValid'] = isValid;
return isValid;
}
#handleMinMaxValidation(inpt, pattern, value, fieldPath) {
this.#handleValidationWarning('remove', inpt, fieldPath, 'to-remove', 'range');
if (validatorMinMaxTypes.includes(pattern)) {
let min = this.#parseLikelyNumber(inpt.getAttribute('(validator-min)'));
let max = this.#parseLikelyNumber(inpt.getAttribute('(validator-max)'));
/** If not min and max restriction stated then
* any value will be allowed and get passed to
* the next validation on the pipeline */
if (!min && !max) return false;
const isValidNum = this.#parseLikelyNumber(value);
let msg = null;
if (min) {
if (isValidNum < min) {
msg = inpt.getAttribute('(validator-min-warn)');
msg = msg || this._const.value['MIN_VALID_MSG_' + this.lang.value].replace('{{}}', min);
}
}
if (max) {
if (isValidNum > max) {
msg = inpt.getAttribute('(validator-man-warn)');
msg = msg || this._const.value['MAX_VALID_MSG_' + this.lang.value].replace('{{}}', max);
}
}
if (msg) return msg;
}
return false;
}
/**
* @param {string} pattern
*/
#checkDatePattern(pattern) {
const value = pattern.trim().toLowerCase();
const isDay = value.indexOf('dd');
const isMon = value.indexOf('dd');
const isYear = value.indexOf('dd');
const dashSep = value.match(/\-/g) || [];
const slashSep = value.match(/\//g) || [];
if (
(isDay >= 0 && isMon >= 0 && isYear >= 0)
&& (value.length == 8 || value.length == 10)
&& (dashSep.length == 2 || slashSep.length == 2)
) {
const sep = dashSep.length == 2 ? '-' : '/';
const [first, sec, third] = value.split(sep);
return new RegExp(`\\d{${first.length}}${sep}\\d{${sec.length}}${sep}\\d{${third.length}}`);
}
return null;
}
/**
* @param {'add' | 'remove'} opt
* @param {HTMLElement} inpt
* @param {string} message
* @param {string} fieldPath
*/
#handleValidationWarning(opt, inpt, fieldPath, msg = null, msgSuffix = '') {
let validationWarning = msg, optListParent = inpt.parentElement;
if (!validationWarning) validationWarning = inpt.getAttribute('(validator-warn)');
if(!validationWarning && ['checkbox','radio'].includes(inpt.type)) validationWarning = 'This is s mandatory field';
if (validationWarning) {
const id = `still-validation-warning${fieldPath}${msgSuffix ? msgSuffix : ''}`;
const classList = `still-validation-warning`;
if (opt == 'add') {
if(['checkbox','radio'].includes(inpt.type))
optListParent = BehaviorComponent.getOptListContainert(optListParent);
const content = `<div id="${id}" class="${classList}">${validationWarning}</div>`;
inpt.classList.add('still-validation-failed-style');
if(optListParent.querySelector('.still-validation-warning')) return;
//const inputField = document.getElementById(id);
if (content) optListParent.insertAdjacentHTML('beforeend', content);
}
if (opt == 'remove') {
let content = document.getElementById(id);
if(['checkbox','radio'].includes(inpt.type))
optListParent = BehaviorComponent.getOptListContainert(optListParent);
content = optListParent.querySelector('.still-validation-warning');
inpt.classList.remove('still-validation-failed-style');
if (content) try { optListParent.removeChild(content); } catch (error) {}
}
} else {
if (opt == 'add') inpt.classList.add('still-validation-failed-style');
if (opt == 'remove') inpt.classList.remove('still-validation-failed-style');
}
}
static getOptListContainert(optListParent){
let foundTopParent = null, treeLvl = 0;
while(treeLvl < 5 && foundTopParent == null) {
if(optListParent.parentElement.tagName != 'OUTPUT') foundTopParent = true;
optListParent = optListParent.parentElement, treeLvl += 1;
}
return optListParent;
}
static setValidatorForField(fieldPath, field) {
if (!(fieldPath in BehaviorComponent.currentFormsValidators))
BehaviorComponent.currentFormsValidators[fieldPath] = {};
if (!(field in BehaviorComponent.currentFormsValidators[fieldPath]))
BehaviorComponent.currentFormsValidators[fieldPath][field] = {};
}
/**
* @param {string} mt
* @param {Object} cmp
* @param {string} field
*/
static setOnValueInput(mt, cmp, field, formRef) {
const fieldPath = `${cmp.cmpInternalId}${formRef ? `-${formRef}` : ''}`;
BehaviorComponent.setValidatorForField(fieldPath, field);
let isValid = true;
if (
mt.indexOf(' (required)="true" ') >= 0
|| mt.indexOf('\n(required)="true"\n') >= 0
|| mt.indexOf('\n(required)="true"') >= 0
|| mt.indexOf('(required)="true"\n') >= 0
/* || mt.indexOf('pattern="') >= 0
|| mt.indexOf('\npattern="') >= 0 */
) {
isValid = false;
}
else
isValid = true;
BehaviorComponent.currentFormsValidators[fieldPath][field]['isValid'] = isValid;
if (!isValid) {
let validatorClass = 'still-validation-class';
const specificValidatorClass = `still-validation-class-${fieldPath}-${field}`;
validatorClass = `${validatorClass} ${specificValidatorClass}`;
BehaviorComponent.currentFormsValidators[fieldPath][field]['inputClass'] = specificValidatorClass;
return validatorClass;
}
return '';
}
static validateForm(fieldPath, cmp, formRefObj = {}, reset = false) {
const formFields = BehaviorComponent.currentFormsValidators[fieldPath];
let valid = true;
const intValidators = Object.entries(formFields);
const behaviorInstance = new BehaviorComponent();
const formRef = String(fieldPath)?.split('-')?.slice(-1)[0];
fieldPath = fieldPath.slice(0, -(formRef.length + 1));
const validators = Object
.entries(intValidators)
.map(
([_, stngs]) => {
const field = stngs[0];
const inpt = document.querySelector(`.${fieldPath}-${field}`);
return [
field, {
isValid: behaviorInstance.onValueInput(null, field, inpt, formRef, cmp, reset),
inputClass: stngs[1].inputClass
}
];
});
if(formRefObj) formRefObj.errorCount = 0;
for (let [field, validator] of validators) {
if (!validator.isValid) {
if(formRefObj) formRefObj.errorCount++;
valid = false;
}
if (validator.inputClass) {
const obj = new BehaviorComponent();
const inpt = document.querySelector('.' + validator.inputClass);
if (!validator.isValid && !['checkbox','radio'].includes(inpt.type)) {
obj.#handleValidationWarning('add', inpt, fieldPath);
} else {
if(!['checkbox','radio'].includes(inpt.type))
obj.#handleValidationWarning('remove', inpt, fieldPath);
}
}
}
return valid;
}
#parseLikelyNumber(value) {
if (!value) return null;
if (!isNaN(value)) return parseFloat(value);
return null;
}
/** @param {'load'} evt */
on(evt, action) {
if (evt in this.behaviorEvtSubscriptions) {
if (this.behaviorEvtSubscriptions[evt].status == $stillconst.A_STATUS.DONE) {
setTimeout(() => {
action(this);
}, 200);
return;
}
}
if (!(evt in this.behaviorEvtSubscriptions)) {
const status = $stillconst.A_STATUS.PENDING
this.behaviorEvtSubscriptions[evt] = { status, actions: [], count: 0 };
}
const count = this.behaviorEvtSubscriptions[evt].count;
this.behaviorEvtSubscriptions[evt].actions.push(action);
this.behaviorEvtSubscriptions[evt].count = count + 1;
}
/** @param {'load'} evt */
emit(evt) {
if (!(evt in this.behaviorEvtSubscriptions)) {
const status = $stillconst.A_STATUS.DONE;
this.behaviorEvtSubscriptions[evt] = { status, actions: [], count: 0 };
return;
}
setTimeout(() => {
this.behaviorEvtSubscriptions[evt].actions.forEach(action => action(this));
}, 200);
this.behaviorEvtSubscriptions[evt].status = $stillconst.A_STATUS.DONE;
}
}