-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathInputValidator.js
386 lines (369 loc) · 17.1 KB
/
InputValidator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
export class Validator {
constructor(options = {}) {
// set class options properties
// if true, carry out validation right after the input field lost focus
this.validate_on_lost_focus = options.validate_on_lost_focus ? options.validate_on_lost_focus : true;
// if true, validate all input fields in the form when submitting the form; if false, no validation is done on submitting the form
this.validate_form_on_submit = options.validate_form_on_submit ? options.validate_form_on_submit : true;
// scroll screen to the input field which failed validation
this.scroll_to_input = options.scroll_to_input ? options.scroll_to_input : false;
this.scroll_behavior = options.scroll_behavior ? options.scroll_behavior : 'smooth';
// the classname of the closest input element where the error div should be rendered
this.error_message_display_class = options.error_message_place_class ? options.error_message_place_class : 'default';
// where within the found element with the specified class the error div should be displayed (the optional values are according to insertAdjacentHTML() specification)
this.error_message_display_where = options.error_message_place_where ? options.error_message_place_where : 'beforebegin';
// should the error message div contain some attribute or class or something specified by the user
this.error_message_div_contains = options.error_message_div_contains ? options.error_message_div_contains : '';
this.error_message_display = options.error_message_display ? options.error_message_display : false,
this.error_message_styles = options.error_message_styles ? options.error_message_styles : {
color: 'red',
fontSize: '1rem',
fontWidth: '700'
}
this.custom_styles_change = options.custom_styles_change ? options.custom_styles_change : {
borderColor: 'red',
borderStyle: 'solid'
}
this.custom_styles_initial = options.custom_styles_initial ? options.custom_styles_initial : {
borderColor: 'rgb(118,118,118)',
borderStyle: 'solid'
}
this.error_messages = options.error_messages ? options.error_messages : {
message0 : 'The field cannot be empty'
}
this.callbacks = options.callbacks ? options.callbacks : {},
this.password_regex = options.password_regex ? options.password_regex : /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/;
// start validator for each input field if validate_on_lost_focus === true
if(this.validate_on_lost_focus === true) {
this.fetchInputs();
}
}
// apply styles (pass the second argument exactly as either 'custom_styles_change' or 'custom_styles_initial')
applyStyles(input_element, style) {
// change the styles, either the style to change or the initial style
for(const [key, value] of Object.entries(this[style])) {
input_element.style[key] = value;
}
}
// read the attributes
readAttributes(input_element) {
const attributes = input_element.getAttribute('data-inputvalidator').split('&');
// new object with preset values
const attrib = {
type: '',
message_id: '',
message_password_no_match: '',
callback: '',
callback_message_id: null
}
// if password attribute contains two messages (first to validate password, second to display no match with second password), but first check if there are at least 3 values in array
if((attributes.length>2) && (attributes[0]==='password1' || attributes[0]==='password2') && attributes[1].substr(0,4)==='mess' && attributes[2].substr(0,4)==='mess') {
attrib.type = attributes[0];
attrib.message_id = attributes[1];
attrib.message_password_no_match = attributes[2];
attrib.callback = attributes[3];
attrib.callback_message_id = attributes[4];
}
else {
attrib.type = attributes[0];
attrib.message_id = attributes[1];
attrib.message_password_no_match = '';
attrib.callback = attributes[2];
attrib.callback_message_id = attributes[3];
}
return attrib;
}
// if callback required, call it and display message
async runCallback(input_element) {
// check if callback is required on this input element
const attributes = this.readAttributes(input_element);
const callback_id = attributes.callback;
const callback_message = attributes.callback_message_id;
// get the callback function from options
const callback_func = this.callbacks[callback_id];
if(callback_id !== undefined || callback_id === '') {
const callback_result = await callback_func(input_element.value);
// if callback result is ok
if(callback_result===true) {
// return true as valid
return true;
}
else {
// apply style change
this.applyStyles(input_element, 'custom_styles_change');
// check if there is any callback message to display, if so, display it
if(this.error_message_display && callback_message!=='' && callback_message !== 'undefined') {
this.renderErrorDiv(input_element, callback_message);
}
// return false as not valid
return Promise.resolve(false);
}
}
else {
// return true as valid
return Promise.resolve(true);
}
}
// test for value emptiness, if so, apply required styles
testIfNotEmpty(input_element) {
if(input_element.value === '') {
this.applyStyles(input_element, 'custom_styles_change');
return false;
}
else {
// set initial styles
this.applyStyles(input_element, 'custom_styles_initial');
// remove error message
this.removeErrorDiv(input_element);
return true;
}
}
// test email format match
testEmail(input_value) {
const mailformat = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
if(input_value.match(mailformat) === null) {
return true;
}
else {
return false;
}
}
// test digit format
testDigit(input_value) {
if(isNaN(input_value)) {
return true;
}
else {
return false;
}
}
// test if two passwords match one another
testPasswordsMatch(input_element) {
// find out if this find the other password input, if exists (can be either password1 or password2)
const this_password = input_element.getAttribute('data-inputvalidator').substr(0,9);
const second_password = this_password === 'password1' ? 'password2' : 'password1';
// loop through all inputs and search for the second password element
let second_password_element = null;
for (let el of this.inputs_to_validate) {
if(el.getAttribute('data-inputvalidator').includes(second_password)) { second_password_element = el; }
}
// if second password element exists and is filled out
if(second_password_element !== null && second_password_element.value !== '') {
// compare values of the two passwords
if(input_element.value !== second_password_element.value) {
return true;
}
else {
return false;
}
}
else {
// second password element doesn't exist, return false as the control was ok
return false;
}
}
// test if password is valid
testPass(input_element) {
// test the match against the regex passed in options (or the default one)
if(input_element.value.match(this.password_regex) === null) {
// password doesn't match (doesn't meet criterions) so return true
return true;
}
else {
// match against regex alright
return false;
}
}
// if required in options, scroll to input / display error message
scrollShowMessage(input_element, empty_value_message=false, passwords_match_message=false) {
// find out message id
const message_id = this.readAttributes(input_element).message_id;
// if scrolling was required
if(this.scroll_to_input) {
input_element.scrollIntoView({behavior: this.scroll_behavior, block: "end", inline: "nearest"});
}
// render error message with its style if required
if(this.error_message_display) {
// if the message should be empty value, then I set the message to 0 which is a default message for empty value
if(empty_value_message===true) {
this.renderErrorDiv(input_element, 'message0');
}
else if(passwords_match_message === true) {
// if the message is no match of passwords, then find out the message and use it
const message_no_match = this.readAttributes(input_element).message_password_no_match;
this.renderErrorDiv(input_element, message_no_match);
}
else {
this.renderErrorDiv(input_element, message_id);
}
}
}
// check particular input
async checkInput(input_element) {
// find out validation type and message id
const validation_type = this.readAttributes(input_element).type;
// check if the value is not empty, if returned true, execute other check according to the validation_type
if(this.testIfNotEmpty(input_element)===true) {
if(validation_type === 'text') {
// run callback function, if callback is true or there is no callback, return true as valid to the caller function, else return false as not valid result
const returned_value = await this.runCallback(input_element);
return returned_value === true ? Promise.resolve(true) : Promise.resolve(false);
}
if(validation_type === 'email') {
// run email check function
if(this.testEmail(input_element.value)) {
this.applyStyles(input_element, 'custom_styles_change');
this.scrollShowMessage(input_element);
// return false to the caller function as not valid
return Promise.resolve(false);
}
else {
// set the initial styles and remove error message
this.applyStyles(input_element, 'custom_styles_initial');
this.removeErrorDiv(input_element);
// run callback function, if callback is true or there is no callback, return true as valid to the caller function, else return false as not valid result
const returned_value = await this.runCallback(input_element);
return returned_value === true ? Promise.resolve(true) : Promise.resolve(false);
}
}
if(validation_type === 'digit') {
// run digit check function
if(this.testDigit(input_element.value)) {
this.applyStyles(input_element, 'custom_styles_change');
this.scrollShowMessage(input_element);
// return false to the caller function as not valid
return Promise.resolve(false);
}
else {
// set the initial styles and remove error message
this.applyStyles(input_element, 'custom_styles_initial');
this.removeErrorDiv(input_element);
// run callback function, if callback is true or there is no callback, return true as valid to the caller function, else return false as not valid result
const returned_value = await this.runCallback(input_element);
return returned_value === true ? Promise.resolve(true) : Promise.resolve(false);
}
}
// first check of password is on valid criteria
if(validation_type === 'password1' || validation_type === 'password2') {
this.first_check_of_password = 'not valid';
// run password check function
if(this.testPass(input_element)) {
this.applyStyles(input_element, 'custom_styles_change');
this.scrollShowMessage(input_element);
// return false to the caller function as not valid
return Promise.resolve(false);
}
else {
// set the initial styles and remove error message
this.applyStyles(input_element, 'custom_styles_initial');
this.removeErrorDiv(input_element);
// set first_check_of_password to valid so that the check of the passwords match against each other can be executed
this.first_check_of_password = 'valid';
// not returning anything so that it doesn't quit this function and testPasswordMatch can be now executed
}
}
// second check of password is on the match against the other password
if((validation_type === 'password1' || validation_type === 'password2') && this.first_check_of_password === 'valid') {
// run password check function
if(this.testPasswordsMatch(input_element)) {
this.applyStyles(input_element, 'custom_styles_change');
this.scrollShowMessage(input_element, false, true);
// return false to the caller function as not valid
return Promise.resolve(false);
}
else {
// set the initial styles and remove error message
this.applyStyles(input_element, 'custom_styles_initial');
this.removeErrorDiv(input_element);
// return true as valid
return Promise.resolve(true);
}
}
}
else {
// scroll and display message if required (second argument 'true' is passed only because this function is being run based on empty field so I have to set fixed message_id)
this.scrollShowMessage(input_element, true);
// return false as not valid
return Promise.resolve(false);
}
}
// fetch all input fields with attribute data-inputvalidator, add blur event listener on each of them
fetchInputs() {
this.inputs_to_validate = document.querySelectorAll('[data-inputvalidator]');
for (let el of this.inputs_to_validate) {
el.addEventListener('blur', (evt)=>{
this.checkInput(evt.target);
});
}
}
// get the message text according to the required message id
getMessageText(message_id) {
return this.error_messages[message_id];
}
/* create error div above the input field */
// width of the message div must be the width of the input field width
// input_element = element passed based on the current data-inputvalidator attribute, message_id = id of the message passed into the constructor options
renderErrorDiv(input_element, message_id) {
// first remove any previous error message if there exists any
this.removeErrorDiv(input_element);
const div_width = window.getComputedStyle(input_element).getPropertyValue('width');
const message = this.getMessageText(message_id);
const random_number = Math.floor(Math.random() * 90000) + 10000;
const unique_id = "unique_id_" + random_number;
const div = `<div ${this.error_message_div_contains} errormessage="true" id='${unique_id}' style="display:block;width:${div_width};">${message}</div>`;
// check if error_message_display_class is defined, if so, then find the closest element with this class, else place it above the input_element
let place_into_element_with_class;
if(this.error_message_display_class === 'default') {
place_into_element_with_class = input_element;
}
else {
place_into_element_with_class = input_element.closest(`.${this.error_message_display_class}`);
}
// insert div above the input field
place_into_element_with_class.insertAdjacentHTML(this.error_message_display_where, div);
// select this inserted error message div
let inserted_error_div = document.getElementById(unique_id);
// apply error message style
for(const [key, value] of Object.entries(this.error_message_styles)) {
inserted_error_div.style[key] = value;
}
}
// remove the error message div
removeErrorDiv(input_element) {
// if there was not specified any error_message_place_class, so remove the previous siblings where the error div is defaulty placed
if(this.error_message_display_class === 'default') {
// remove first div in line above the input_element
const error_div = input_element.previousSibling;
// remove error message if exists (check if the previousSibling has attribute errormessage)
if(error_div.hasAttribute('errormessage')) error_div.remove();
}
// else as I don't know where the user wanted to place the div error (within which class he specified the div), I am removing all error divs
const all_error_divs = document.querySelectorAll('[errormessage]');
for(const el of all_error_divs) {
el.remove();
}
}
// check all input fields if not empty
async validateForm(parent_element) {
// if validate_form_on_submit is set to false, resolve true right away (which means ignore the correctness of form fields on submitting the form)
if(this.validate_form_on_submit === false) {
return Promise.resolve(true);
}
let all_inputs_ok = true;
this.inputs_to_validate = document.querySelectorAll('[data-inputvalidator]');
for (let el of this.inputs_to_validate) {
// validate each input element, if any of them fails, set all_inputs_filled to false
let res = await this.checkInput(el);
//console.log(el, `returned value is: ${res}`);
if(res === false) all_inputs_ok = false;
}
// if all field are filled and valid, return true to the external on submit function
if(all_inputs_ok === true) {
return Promise.resolve(true);
}
else {
// return false to the external on submit function
return Promise.resolve(false);
}
}
}