script/FormDataValidator.js   B
last analyzed

Complexity

Total Complexity 47
Complexity/F 5.22

Size

Lines of Code 319
Function Count 9

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 47
eloc 175
mnd 38
bc 38
fnc 9
dl 0
loc 319
rs 8.64
bpm 4.2222
cpm 5.2222
noi 0
c 0
b 0
f 0

9 Functions

Rating   Name   Duplication   Size   Complexity  
A FormDataValidator.setError 0 12 2
A FormDataValidator.getValidationElements 0 13 3
A FormDataValidator.isValidInteger 0 12 3
C FormDataValidator.validate 0 60 10
A FormDataValidator.setFocusItem 0 6 2
A FormDataValidator.constructor 0 7 1
C FormDataValidator.isValidDate 0 59 9
C FormDataValidator.isValidTime 0 51 11
B FormDataValidator.isValidFloat 0 34 6

How to fix   Complexity   

Complexity

Complex classes like script/FormDataValidator.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * This class can validate the marked input fields of the form with given id.
3
 * Each field that is marked with the custom 'data-validation' attribute is 
4
 * validated according to the information this attribute contains.
5
 */
6
class FormDataValidator 
7
{
8
	/**
9
	 * pass the id of the form element to the constructor.
10
	 * @param string id
11
	 */
12
    constructor(config) 
13
	{
14
        this.config = config;
15
        this.focusItem = null;
16
        this.focusTabIndex = null;
17
        this.errors = 0;
18
    }
19
20
	/**
21
	 * Perform the validation.
22
	 * @returns bool true if all elements contains valid data, false on error
23
	 */
24
    validate() 
25
	{
26
        var i;
27
        var aValidationElements = this.getValidationElements();
28
        var length = aValidationElements.length;
29
        for (i = 0; i < length; i++) {
30
			// since getValidationElements() only return existing items with 'data-validation' attribute
31
			// there's no need to check this values against null
32
			let item = aValidationElements[i];
33
            let validate = item.getAttribute('data-validation');
34
            // we can't use the split function because the param may contain the ':' itself (time-separator...)
35
			// let [type, param] = validate.split(':');
36
            let pos = validate.indexOf(':');
37
            let type = (pos >= 0) ? validate.substring(0, pos) : validate;
38
            let param = (pos >= 0) ? validate.substring(pos + 1) : '';
39
			let valid = false;
40
            switch (type) {
41
                case 'integer':
42
		            valid = this.isValidInteger(item.value, param);
43
                    break;
44
                case 'float':
45
		            valid = this.isValidFloat(item.value, param);
46
                    break;
47
                case 'date':
48
		            valid = this.isValidDate(item.value, param);
49
                    break;
50
                case 'time':
51
		            valid = this.isValidTime(item.value, param);
52
                    break;
53
                default:
54
                    break;
55
            }
56
            if (valid !== false) {
57
                item.value = valid;
58
                this.setError(false, item);
59
            } else {
60
                this.setError(true, item);
61
            }
62
        }
63
        // if errors found, dispplay message and set focus to last input
64
        if (this.errors > 0) {
65
            var strMsg;
66
            if (this.config.errorMsg !== undefined) {
67
                strMsg = this.config.errorMsg;
68
            } else {
69
                strMsg = "The form contains invalid information that is marked in red!<br/><br/>Please correct or complete your entries. ";
70
            }
71
72
            var popup = document.getElementById('errorPopup');
73
            if (popup) {
74
                popup.innerHTML = strMsg;
75
                popup.style.display = 'block';
76
            }
77
            if (this.focusItem !== null) {
78
                this.focusItem.focus();
79
            }
80
            return false;
81
        }
82
        return true;
83
    }
84
85
	/**
86
	 * Get all elements inside the form with the attribute 'data-validation' set.
87
	 * @returns array
88
	 */
89
    getValidationElements() 
90
	{
91
        let validationElements = [];
92
        let form = document.getElementById(this.config.formID);
93
        let formElements = form.getElementsByTagName('*');
94
        let length = formElements.length;
95
        for (let i = 0; i < length; i++) {
96
            if (formElements[i].getAttribute('data-validation') !== null) {
97
                validationElements.push(formElements[i]);
98
            }
99
        }
100
        return validationElements;
101
    }
102
103
	/**
104
	 * Check, if input is a valid date.
105
	 * strParam[0]: separator
106
	 * strParam[1..]: YMD, DMY, MDY for the order of year, month and day 
107
     * @param string strDate value to checked
108
     * @param string
109
     * @returns false|string false if invalid, otherwise formated value
110
	 */
111
    isValidDate(strDate, strParam) 
112
	{
113
		let strSep = strParam.charAt(0);
114
		let strYMD = strParam.substring(1);
115
		
116
        // remove all whitespace
117
        strDate = strDate.toString().trim();
118
		if (strDate == '') {
119
			return '';
120
		}
121
122
		let iY = 0, iM = 0, iD = 0;
123
		switch (strYMD) {
124
			case 'YMD':
125
		        [iY, iM, iD] = strDate.split(strSep);
126
				break;
127
			case 'DMY':
128
		        [iD, iM, iY] = strDate.split(strSep);
129
				break;
130
			case 'MDY':
131
		        [iM, iD, iY] = strDate.split(strSep);
132
				break;
133
			default:
134
				// console.log('invalid format specification for date validation [' + strParam + ']!');
135
				return false;
136
		}
137
        if (isNaN(iY) || isNaN(iM) || isNaN(iD)) {
138
            return false;
139
        }
140
        // values < 33 are 21'st century and 33...99 20'st Century!
141
        if (iY < 100) {
142
            if (iY < 33) {
143
                iY += 2000;
144
            } else {
145
                iY += 1900;
146
            }
147
        }
148
        if (iY < 1900) {
149
            return false;
150
        }
151
        // simply initialize a new Date-Object and compare all parts... (Note: JS Date works with month 0...11 !!)
152
        let date = new Date(iY, --iM, iD);
153
        if (iD != date.getDate() || iM++ != date.getMonth() || iY != date.getFullYear()) {
154
            return false;
155
        }
156
		// all fine - let's format pretty...
157
		switch (strYMD) {
158
			case 'YMD':
159
				strDate = iY + strSep + ("00" + iM).slice(-2) + strSep + ("00" + iD).slice(-2); 
160
				break;
161
			case 'DMY':
162
				strDate = ("00" + iD).slice(-2) + strSep + ("00" + iM).slice(-2) + strSep + iY; 
163
				break;
164
			case 'MDY':
165
				strDate = ("00" + iM).slice(-2) + strSep + ("00" + iD).slice(-2) + strSep + iY; 
166
				break;
167
		}
168
        return strDate;
169
    }
170
171
	/**
172
	 * Check, if input is a valid time
173
	 * strParam[0]: separator
174
	 * strParam[1]: 1 if with seconds, 0 without 
175
	 * strParam[2]: m if allowed to input minutes only, not allowed all other value
176
     * @param string strTime value to checked
177
     * @param string
178
     * @returns false|string false if invalid, otherwise formated value
179
	 */
180
    isValidTime(strTime, strParam)
181
	{
182
		if (strParam.length != 3) {
183
			// console.log('invalid format specification for time validation [' + strParam + ']!');
184
			return false;
185
		}
186
		let strSep = strParam.charAt(0);
187
		let strSec = strParam.charAt(1);
188
		let strMO = strParam.charAt(2);
189
        
190
        // remove all whitespace
191
        strTime = strTime.toString().trim();
192
		if (strTime == '') {
193
			return '';
194
		}
195
196
		let [iH, iM, iS] = strTime.split(strSep);
197
        if (iM === undefined) {
198
            if (strMO != 'm') {
199
                return false;
200
            }
201
            // if only a number is typed in, we interpret it as minutes...
202
            iM = iH;
203
            iH = 0;
204
        }
205
        iS = (iS === undefined ? 0 : iS);
206
        if (isNaN(iM) || isNaN(iH) || isNaN(iS)) {
207
            return false;
208
        }
209
        
210
        if (iM > 59) {
211
            iH += (iM - (iM % 60)) / 60;
212
            iM = iM % 60;
213
        }
214
        // ... 23:59 is the end
215
        if (iH > 23) {
216
            return false;
217
        }
218
219
        // simply initialize a new Date-Object and compare all parts...
220
        let date = new Date(0, 0, 0, iH, iM, iS);
221
        if (iH != date.getHours() || iM != date.getMinutes() || iS != date.getSeconds()) {
222
            return false;
223
        }
224
		// all fine - let's format pretty...
225
        strTime = ("00" + iH).slice(-2) + strSep + ("00" + iM).slice(-2);
226
        if (strSec != '0') {
227
            strTime += strSep + ("00" + iS).slice(-2); 
228
        }
229
        return strTime;
230
    }
231
232
	/**
233
     * Check for valid integer.
234
	 * strParam[0]: 'e' if empty value allowed, all other empty value is set to '0'
235
     * @param string strInt value to checked
236
     * @param string
237
     * @returns false|string false if invalid, otherwise formated value
238
	 */
239
    isValidInteger(strInt, strParam) 
240
	{
241
        // remove all whitespace
242
        strInt = strInt.toString().trim();
243
        if (isNaN(strInt) || strInt.indexOf('.') !== -1) {
244
            return false;
245
        }
246
        if (strInt == '' && strParam != 'e') {
247
            strInt = '0';
248
        }
249
        return strInt;
250
    }
251
252
	/**
253
	 * Check, if input is a valid float.
254
	 * strParam[1]: 'e' if empty value allowed, all other empty value is set to '0'
255
	 * strParam[2]: decimal digits 
256
	 * strParam[3]: decimal point
257
	 * strParam[4]: thousands separator
258
     * @param string strCur value to checked
259
     * @param string
260
     * @returns false|string false if invalid, otherwise formated value
261
	 */
262
    isValidFloat(strCur, strParam) 
263
	{
264
        let iLength = strParam.length
265
		if (iLength != 3 && iLength != 4) {
266
			// console.log('invalid format specification for float validation [' + strParam + ']!');
267
			return false;
268
		}
269
		let strEmpty = strParam.charAt(0);
270
		let strDD = strParam.charAt(1);
271
		let strDP = strParam.charAt(2);
272
		let strTS = (iLength == 3) ? '' : strParam.charAt(3);
273
		
274
        // remove all whitespace
275
        strCur = strCur.toString().trim();
276
277
        if (strCur == '') {
278
            // empty vlues are allowed
279
            if (strEmpty == 'e') {
280
                return '';
281
            }
282
            strCur = '0';
283
        } else {
284
	        // remove thousands separator and replace decimal point with '.'
285
	        strCur = strCur.replace(strTS, "");
286
	        strCur = strCur.replace(strDP, ".");
287
		}
288
        if (isNaN(strCur)) {
289
            return false;
290
        }
291
        strCur = Number.parseFloat(strCur).toFixed(strDD);
292
        strCur = strCur.replace(".", strDP);
293
        strCur = strCur.replace(/\B(?=(\d{3})+(?!\d))/g, strTS);
294
        return strCur;
295
    }
296
297
	/**
298
     * Mark/unmark element as error.
299
     * @param bool set true marks error
300
     * @param element item to mark
301
	 */
302
    setError(set, item) 
303
	{
304
        if (set) {
305
            item.className = item.className.replace(/Mand/, 'MError');
306
            item.className = item.className.replace(/OK/, 'Error');
307
            this.setFocusItem(item);
308
            this.errors++;
309
        } else {
310
            item.className = item.className.replace(/MError/, 'Mand');
311
            item.className = item.className.replace(/Error/, 'OK');
312
        }
313
    }
314
315
    /**
316
     * Save invalid item with lowest tabindex to set focus after error message.
317
     */
318
    setFocusItem(item) {
319
        if (!this.focusTabIndex || this.focusTabIndex > item.tabIndex) {
320
            this.focusTabIndex = item.tabIndex;
321
            this.focusItem = item;
322
        }
323
    }
324
}
325