updates
This commit is contained in:
@@ -1,223 +0,0 @@
|
||||
import './validator.css';
|
||||
|
||||
import { Ut } from '../Ut';
|
||||
import { El } from '../dom';
|
||||
import { Boot } from '../../core/Boot';
|
||||
import { Validator } from './Validator';
|
||||
|
||||
////////////////////////////
|
||||
// Class Form Validator
|
||||
////////////////////////////
|
||||
|
||||
export class FormValidator extends Validator {
|
||||
|
||||
constructor(target, opts) {
|
||||
|
||||
super(target);
|
||||
|
||||
this.target = target;
|
||||
|
||||
if (this.target.dxData('form')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.target.dxData('form', this);
|
||||
|
||||
this.opts = { ... {
|
||||
submit : false,
|
||||
display_err: true,
|
||||
reset_form : false
|
||||
}, ... opts };
|
||||
|
||||
Ut.trigger(this.opts.onInit, this.target.elements, this);
|
||||
|
||||
this.setEvents();
|
||||
}
|
||||
|
||||
setEvents() {
|
||||
|
||||
// Click on error UI
|
||||
|
||||
this.target.dxOn('click.ui', '.sp-has-error', (_, t) => this.target.dxFind('input[name="' + t.dxFind('input').name + '"', item => item.parentNode?.classList.remove('sp-has-error')) );
|
||||
|
||||
// Input Focus - Handler
|
||||
|
||||
this.target.dxOn('click.form', 'input, select, textarea', (_, t) => {
|
||||
|
||||
if (this.opts.display_err) {
|
||||
|
||||
let fieldWrapper = t.dxParents('.form-field');
|
||||
|
||||
fieldWrapper?.classList.remove('form-field-error');
|
||||
|
||||
fieldWrapper?.dxFind('.sp-label-error')?.remove();
|
||||
}
|
||||
|
||||
Ut.trigger(this.opts.onClick);
|
||||
});
|
||||
|
||||
// Submit - Handler
|
||||
this.target.dxOn('submit.form', e => {
|
||||
|
||||
if (!this.opts.submit) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.entry = this.getData();
|
||||
|
||||
if (Ut.isFn(this.opts.valid) && !this.valid()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ut.isFn(this.opts.onSubmit) && !this.opts.onSubmit(this.entry, this)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opts.submit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ut.isFn(this.opts.onSubmit)) {
|
||||
this.opts.onSubmit.call(this);
|
||||
}
|
||||
|
||||
if (this.opts.reset_form) {
|
||||
this.target.dxResetForm();
|
||||
}
|
||||
});
|
||||
|
||||
// Fix keydown enter bug
|
||||
|
||||
let delayed = false;
|
||||
|
||||
this.target.dxOn('keydown.form', e => {
|
||||
|
||||
if (e.key == 'Enter' && e.target.tagName.toLowerCase() != 'textarea') {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.target.readOnly && !this.pendingRequest && !delayed) {
|
||||
|
||||
delayed = true;
|
||||
|
||||
setTimeout(() => delayed = false, 200);
|
||||
this.target.dxTrigger('submit');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
valid () {
|
||||
|
||||
let pass = true;
|
||||
|
||||
this.target.dxFind('.form-field-error', item => item.classList.remove('form-field-error', 'sp-has-error'));
|
||||
this.target.dxFind('.sp-label-error', item => item.remove());
|
||||
|
||||
let rules = this.opts.valid(this.entry);
|
||||
|
||||
for (let name in rules) {
|
||||
|
||||
if (!(name in this.target.elements)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let el = this.target.elements[ name ];
|
||||
|
||||
if (!('length' in el ) || el.nodeName.toLowerCase() == 'select') {
|
||||
el = [ el ];
|
||||
}
|
||||
|
||||
for (let input of el) {
|
||||
|
||||
let val = (input.type && input.type == 'checkbox')
|
||||
? (input.checked ? input.value : '')
|
||||
: El('p', {}, input.value).textContent.trim();
|
||||
|
||||
let ruleList = rules[ name ].split('|');
|
||||
|
||||
for (let tokenRule of ruleList) {
|
||||
|
||||
let chunks = tokenRule.split(':');
|
||||
|
||||
let args = chunks[1] ?? null;
|
||||
|
||||
let rule = chunks[0];
|
||||
|
||||
if (!this.handlers[rule]) {
|
||||
throw new Error(`The rule "{${rule}}" does not exist`);
|
||||
}
|
||||
|
||||
let format = { value: Validator.INPUT_STR };
|
||||
|
||||
let result = this.handlers[ rule ] (val ?? null, !!input, args, format);
|
||||
|
||||
if (result === false) {
|
||||
|
||||
pass = false;
|
||||
|
||||
let emsg = this.renderRuleMsg(format, rule, args);
|
||||
|
||||
if (this.opts.display_err) {
|
||||
this.inputError(input, emsg);
|
||||
}
|
||||
|
||||
Ut.trigger(this.opts.onError, emsg);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pass;
|
||||
}
|
||||
|
||||
renderRuleMsg(format, rule, args) {
|
||||
|
||||
args = args ? args.split(',') : null;
|
||||
|
||||
return Boot.intData.error[ this.formatName(format.value) + '_' + rule ].replace(/\$(\d+)/g, (_, i) => args[i - 1] ?? '')
|
||||
}
|
||||
|
||||
formatName(val) {
|
||||
|
||||
switch (val) {
|
||||
case Validator.INPUT_STR: return 'str';
|
||||
case Validator.INPUT_NUM: return 'num';
|
||||
case Validator.INPUT_FILE: return 'file';
|
||||
default: return 'str';
|
||||
}
|
||||
}
|
||||
|
||||
inputError(input, emsg) {
|
||||
|
||||
if (input.type == 'checkbox' || input.type == 'radio') {
|
||||
input.parentNode.classList.add('sp-has-error');
|
||||
}
|
||||
else {
|
||||
|
||||
let fieldWrapper = input.dxParents('.form-field');
|
||||
|
||||
if (!fieldWrapper) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fieldWrapper.classList.add('form-field-error');
|
||||
|
||||
fieldWrapper.append( El('div', { className: 'sp-label-error' }, emsg ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.target.dxOff('submit.form click.form keydown.form click.ui');
|
||||
this.target.dxRemoveData('form');
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import './validator.css';
|
||||
|
||||
//import { Ut } from ".";
|
||||
import { El } from '../dom';
|
||||
|
||||
export class InputError {
|
||||
|
||||
/**
|
||||
* InputError, Constructor
|
||||
*/
|
||||
constructor(target, emsg) {
|
||||
|
||||
this.target = target;
|
||||
this.emsg = emsg;
|
||||
|
||||
this.fieldWrapper = target.dxParents('.form-field');
|
||||
|
||||
if (!this.fieldWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.fieldWrapper.classList.contains('form-field-error')) {
|
||||
this.create();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InputError, Create
|
||||
*/
|
||||
create() {
|
||||
this.fieldWrapper.classList.add('form-field-error');
|
||||
this.fieldWrapper.append( El('div', { className: 'sp-label-error' }, this.emsg) );
|
||||
}
|
||||
|
||||
/**
|
||||
* InputError, Destroy
|
||||
*/
|
||||
destroy() {
|
||||
|
||||
this.fieldWrapper.classList.remove('form-field-error');
|
||||
this.fieldWrapper.dxFind('.sp-label-error')?.remove();
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
////////////////////
|
||||
// Class Validator
|
||||
////////////////////
|
||||
|
||||
export class Validator {
|
||||
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
static INPUT_STR = 1;
|
||||
static INPUT_NUM = 2;
|
||||
static INPUT_FILE = 3;
|
||||
|
||||
handlers = {
|
||||
|
||||
required: (val, present) => present && val !== '' && val !== undefined && val !== null,
|
||||
|
||||
not_empty: (val, present) => present && val !== '' && val !== undefined && val !== null && val !== 0 && val !== '0',
|
||||
|
||||
sometimes: (_, present) => !present ? null : true,
|
||||
|
||||
filled: (val, present) => !present ? null : val !== '' && val !== undefined && val !== null,
|
||||
|
||||
present: (_, present) => present,
|
||||
|
||||
missing: (_, present) => !present,
|
||||
|
||||
nullable: (val, present) => (!present || val === '' || val === undefined || val === null) ? null: true,
|
||||
|
||||
numeric: (val, present, arg, format) => {
|
||||
format.value = Validator.INPUT_NUM;
|
||||
return !present ? null : !isNaN(val);
|
||||
},
|
||||
|
||||
integer: (val, present, arg, format) => {
|
||||
format.value = Validator.INPUT_NUM;
|
||||
return !present ? null : Number.isInteger(Number(val));
|
||||
},
|
||||
|
||||
boolean: (val, present, arg, format) => {
|
||||
format.value = Validator.INPUT_NUM;
|
||||
return !present ? null : [true, false, 0, 1, '0', '1'].includes(val);
|
||||
},
|
||||
|
||||
string: (val, present, arg, format) => {
|
||||
format.value = Validator.INPUT_STR;
|
||||
return !present ? null : typeof val === 'string';
|
||||
},
|
||||
file: (val, present, arg, format) => {
|
||||
|
||||
format.value = Validator.INPUT_FILE;
|
||||
|
||||
return !present ? null : (
|
||||
typeof val === 'object' &&
|
||||
'error' in val &&
|
||||
'tmp_name' in val &&
|
||||
val.error === 0
|
||||
);
|
||||
},
|
||||
|
||||
image: (val, present, arg, format) => {
|
||||
|
||||
format.value = Validator.INPUT_FILE;
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
if (
|
||||
typeof val !== 'object' ||
|
||||
!('error' in val) ||
|
||||
!('tmp_name' in val) ||
|
||||
val.error !== 0 ||
|
||||
typeof val.name !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ext = val.name.split('.').pop().toLowerCase();
|
||||
|
||||
return ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'svg', 'webp'].includes(ext);
|
||||
},
|
||||
|
||||
mimes: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
if (
|
||||
typeof val !== 'object' ||
|
||||
!('error' in val) ||
|
||||
!('tmp_name' in val) ||
|
||||
val.error !== 0 ||
|
||||
typeof val.name !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowed = arg.split(',').map(ext => ext.trim().toLowerCase());
|
||||
const ext = val.name.split('.').pop().toLowerCase();
|
||||
|
||||
return allowed.includes(ext);
|
||||
},
|
||||
|
||||
min: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length >= arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) >= arg)
|
||||
);
|
||||
},
|
||||
|
||||
max: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length <= arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) <= arg)
|
||||
);
|
||||
},
|
||||
|
||||
size: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length == arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) == arg)
|
||||
);
|
||||
},
|
||||
|
||||
gt: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length > arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) > arg)
|
||||
);
|
||||
},
|
||||
|
||||
gte: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length >= arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) >= arg)
|
||||
);
|
||||
},
|
||||
|
||||
lt: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length < arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) < arg)
|
||||
);
|
||||
},
|
||||
|
||||
lte: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return (
|
||||
(format.value === Validator.INPUT_STR && val.length <= arg) ||
|
||||
(format.value === Validator.INPUT_NUM && Number(val) <= arg)
|
||||
);
|
||||
},
|
||||
|
||||
between: (val, present, arg, format) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
const [min, max] = arg.split(',').map(Number);
|
||||
|
||||
if (isNaN(min) || isNaN(max)) return false;
|
||||
|
||||
if (format.value === Validator.INPUT_STR) {
|
||||
const len = val.length;
|
||||
return len >= min && len <= max;
|
||||
} else if (format.value === Validator.INPUT_NUM) {
|
||||
return Number(val) >= min && Number(val) <= max;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
accepted: (val, present) => !present ? null : ['yes', 'on', 1, '1', true, 'true'].includes(val),
|
||||
|
||||
declined: (val, present) => !present ? null : ['no', 'off', 0, '0', false, 'false'].includes(val),
|
||||
|
||||
starts_with: (val, present, arg) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return arg.split(',').some(prefix => val.startsWith(prefix));
|
||||
},
|
||||
|
||||
ends_with: (val, present, arg) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
return arg.split(',').some(suffix => val.endsWith(suffix));
|
||||
},
|
||||
|
||||
alpha: (val, present) => !present ? null : /^[a-zA-Z]+$/.test(val),
|
||||
|
||||
alpha_num: (val, present) => !present ? null : /^[a-zA-Z0-9]+$/.test(val),
|
||||
|
||||
uppercase: (val, present) => !present ? null : val === val.toUpperCase(),
|
||||
|
||||
lowercase: (val, present) => !present ? null : val === val.toLowerCase(),
|
||||
|
||||
domain: (val, present) => !present ? null : /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(val),
|
||||
|
||||
email: (val, present) => !present ? null : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
|
||||
|
||||
url: (val, present) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
try {
|
||||
new URL(val);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
ip: (val, present) => !present ? null : (this.handlers.ipv4(val, present) || this.handlers.ipv6(val, present) ),
|
||||
|
||||
ipv4: (val, present) => !present ? null : /^(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)$/.test(val),
|
||||
|
||||
ipv6: (val, present) => !present ? null : /^([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})$/.test(val),
|
||||
|
||||
hash: (val, present) => !present ? null : /^[a-f0-9]+$/i.test(val),
|
||||
|
||||
in: (val, present, arg) => !present ? null : arg.split(',').includes(val),
|
||||
|
||||
same: (val, present, arg) => !present ? null : val === this.target.elements[arg].value,
|
||||
|
||||
json: (val, present) => {
|
||||
|
||||
if (!present) return null;
|
||||
|
||||
try {
|
||||
JSON.parse(val);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
date: (val, present, arg, format) => !present ? null : format.value === Validator.INPUT_STR && !isNaN(Date.parse(val)),
|
||||
|
||||
cnp: (val, present, arg, format) => !present ? null : this.validCNP(val)
|
||||
};
|
||||
|
||||
getData() {
|
||||
|
||||
let fdata = new FormData();
|
||||
|
||||
[...this.target.elements].forEach(el => {
|
||||
|
||||
if (!el.name || el.type == 'button' || el.type == 'submit') {
|
||||
return true;
|
||||
}
|
||||
|
||||
let value;
|
||||
let displayedVal = null;
|
||||
|
||||
const widget = el.dxWidget();
|
||||
|
||||
if (widget) {
|
||||
value = widget.getValue();
|
||||
displayedVal = el.value.trim();
|
||||
}
|
||||
else {
|
||||
value = el.value.trim();
|
||||
}
|
||||
|
||||
switch (el.type) {
|
||||
|
||||
case 'file':
|
||||
|
||||
let uploader = el.dxData('uploader');
|
||||
|
||||
if (uploader) {
|
||||
uploader.getFiles().forEach(filename => fdata.append('file__' + el.name + '[]', filename));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'radio':
|
||||
|
||||
if (!fdata.has(el.name)) {
|
||||
fdata.set(el.name, '');
|
||||
}
|
||||
|
||||
if (el.checked) {
|
||||
fdata.set(el.name, value);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'checkbox':
|
||||
|
||||
if (el.checked) {
|
||||
fdata.append(el.name + '[]', value );
|
||||
}
|
||||
|
||||
break;
|
||||
case 'select-multiple':
|
||||
|
||||
for (let j = 0; j < el.options.length; j++) {
|
||||
if (el.options[j].selected && el.options[j].value != 0) {
|
||||
fdata.append(el.name + '[]', /^\d+$/.test(el.options[j].value)
|
||||
? parseInt(el.options[j].value)
|
||||
: el.options[j].value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
if (fdata.has(el.name)) {
|
||||
|
||||
fdata.append(el.name + '[]', fdata.get(el.name) );
|
||||
fdata.delete(el.name);
|
||||
fdata.append(el.name + '[]', value);
|
||||
}
|
||||
else if (fdata.has(el.name + '[]')) {
|
||||
fdata.append(el.name + '[]', value);
|
||||
}
|
||||
else {
|
||||
|
||||
fdata.set(el.name, value);
|
||||
|
||||
if (displayedVal) {
|
||||
fdata.set(el.name + '_value', displayedVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return fdata;
|
||||
}
|
||||
|
||||
validCNP(value) {
|
||||
|
||||
let i = 0 , year = 0 , hashResult = 0 , cnp = [] , hashTable = [ 2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9 ];
|
||||
|
||||
if ( value.length !== 13 ) { return false; }
|
||||
|
||||
for ( i = 0 ; i < 13 ; i++ ) {
|
||||
|
||||
cnp[i] = parseInt( value.charAt(i) , 10 );
|
||||
|
||||
if( isNaN( cnp[i] ) ) { return false; }
|
||||
|
||||
if( i < 12 ) { hashResult = hashResult + ( cnp[i] * hashTable[i] ); }
|
||||
}
|
||||
|
||||
hashResult = hashResult % 11;
|
||||
|
||||
if( hashResult === 10 ) { hashResult = 1; }
|
||||
|
||||
year = (cnp[1]*10)+cnp[2];
|
||||
|
||||
switch( cnp[0] ) {
|
||||
case 1 : case 2 : { year += 1900; } break;
|
||||
case 3 : case 4 : { year += 1800; } break;
|
||||
case 5 : case 6 : { year += 2000; } break;
|
||||
case 7 : case 8 : case 9 : { year += 2000; if( year > ( parseInt( new Date().getFullYear() , 10 ) - 14 ) ) { year -= 100; } } break;
|
||||
default : { return false; }
|
||||
}
|
||||
|
||||
if( year < 1800 || year > 2099 ) { return false; }
|
||||
|
||||
return ( cnp[12] === hashResult );
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/* Validator */
|
||||
|
||||
.sp-has-error{
|
||||
color: red;
|
||||
}
|
||||
|
||||
.sp-label-error {
|
||||
position: absolute;
|
||||
background: #fb434a;
|
||||
font-size: 12px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
bottom: -30px;
|
||||
}
|
||||
|
||||
.sp-label-error:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 5px solid #fb434a;
|
||||
top: -5px;
|
||||
left: 10px;
|
||||
margin: auto;
|
||||
}
|
||||
Reference in New Issue
Block a user