1 /**
  2  * The NornixFormHandler class makes it easy to do form input validation and other
  3  * common form tasks.
  4  *
  5  * Invalid fields will generate a dialog box with an error message,
  6  * and the corresponding form fields will get the class "invalid".
  7  *
  8  * It is easy to add your own field validators.
  9  *
 10  * Usage instructions at http://forms.nornix.com/info/usage
 11  *
 12  * Warning: some strings used in the scripts will only function properly
 13  * if it is served as UTF-8. If your HTML document is encoded using UTF-8
 14  * this will happen automagically. See the usage instructions.
 15  *
 16  * @author      Anders Nawroth <http://www.anders.nawroth.com/>
 17  * @author      Eric Jexén <http://eric.jexen.se/>
 18  * @license     LGPL <http://forms.nornix.com/license>
 19  * @copyright   2006--2007
 20  * @version     @version@
 21  * @constructor
 22  */
 23 Nornix.FormHandler = function ()
 24 {
 25 	/**
 26 	 * Variable to work around ECMAScript problems with the "this" keyword inside functions.
 27 	 * @type Object
 28 	 * @private
 29 	 */
 30 	var thiz = this;
 31 	/**
 32 	 * Simple RegExp to find validator class names.
 33 	 * Error checking on the names is executed in the checkForms method.
 34 	 * @type RegExp
 35 	 * @private
 36 	 */
 37 	var searchValidate = /validate(\S)+/g;
 38 	/**
 39 	 * Simple RegExp to find "confirm" in class names.
 40 	 * @type RegExp
 41 	 * @private
 42 	 */
 43 	var searchConfirm = /(^| )confirm( |$)/;
 44 	/**
 45 	 * Simple RegExp to find "okMessage" in class names.
 46 	 * @type RegExp
 47 	 * @private
 48 	 */
 49 	var searchOkMessage = /(^| )okMessage( |$)/;
 50 	/**
 51 	 * Objects to keep track of the focused form element, if any.
 52 	 * The lastFocuesElement won't get cleared.
 53 	 * @type Object
 54 	 * @private
 55 	 */
 56 	var focusedElement = null;
 57 	/**
 58 	 * Pattern to recognize "message" class.
 59 	 * @type RegExp
 60 	 * @private
 61 	 */
 62 	var searchMessage = /(^| )message( |$)/;
 63 	/**
 64 	 * Pattern to recognize "-cancel" in name attribute of submit button.
 65 	 * @type RegExp
 66 	 * @private
 67 	 */
 68 	var searchCancel = /.*-cancel$/;
 69 
 70 	// initialize when page is loaded
 71 	Nornix.events.add(window, 'load', init);
 72 	
 73 	/**
 74 	 * Initializes the NornixFormHandler object.
 75 	 * Searches thru the document, and adds event handlers for the submit and reset events.
 76 	 * It also adds event handlers for focus and blur events, to keep track of the focused element.
 77 	 * Most work is done when the submit is fired, not earlier.
 78 	 * @member NornixFormHandler
 79 	 */
 80 	function init()
 81 	{
 82 		if (!document.getElementsByTagName) return;
 83 		// search for messages
 84 		var pElement, pElements = document.getElementsByTagName("p"), i = 0;
 85 		while (pElement = pElements[i++])
 86 		{
 87 			if (searchMessage.test(pElement.className))
 88 			{
 89 				Nornix.fader(pElement, "fader", 20);
 90 			}
 91 		}
 92 		var form, formElements = document.getElementsByTagName("form");
 93 		i = 0;
 94 		while (form = formElements[i++])
 95 		{
 96 			Nornix.events.add(form, 'submit', checkForm);
 97 			Nornix.events.add(form, 'reset', resetForm);
 98 			var element, elements = form.elements, j = 0;
 99 			while (element = elements[j++])
100 			{
101 				if (!Nornix.dom.eqNodeName(element, 'fieldset'))
102 				{
103 					Nornix.events.add(element, 'focus', setFocus);
104 					Nornix.events.add(element, 'blur', clearFocus);
105 				}
106 			}
107 		}
108 	}
109 	
110 	/**
111 	 * Main method to validate form.
112 	 * 
113 	 * Runs the apropriate validators and writes error messages if needed.
114 	 * If there is a function assigned to the chainedSubmit property of
115 	 * the form, it will be called when the form validates.
116 	 * 
117 	 * Form cancelling buttons:
118 	 * If the focused element on submit has a name ending in "-cancel", the
119 	 * form is sent without any validiation or other functions.
120 	 * 
121 	 * @param {Event} event the event object
122 	 * @private
123 	 */
124 	var checkForm = function (event)
125 	{
126 		if (!this.elements) return; // nothing to work with, quit
127 		if (focusedElement && focusedElement.name && searchCancel.test(focusedElement.name))
128 		{
129 			return; // this is a cancel button, don't mess it up!
130 		}
131 		var errMsg = [];
132 		var confirmSubmit = null;
133 		var firstError = null;
134 		var validator;
135 		// store the focused element beforing using any alerts (IE!)
136 		var lastFocused = focusedElement;
137 		// loop form elements ("this" points to the form)
138 		var e, elements = this.elements, i = 0;
139 		while (e = elements[i++])
140 		{
141 			var classString = e.className;
142 			// loop thru validator functions
143 			while (validator = searchValidate.exec(classString))
144 			{
145 				var validate = thiz.validators[validator[0]]; // get validator function
146 				if (validate === undefined) continue; // skip validator that doesn't exist :TODO: warning message?
147 				var result = validate(e.value);
148 				if (result)
149 				{
150 					setValid(e);
151 				}
152 				else
153 				{
154 					if (!firstError)
155 					{
156 						firstError = e;
157 					}
158 					setInvalid(e);
159 					// find field label & title to use as error message
160 					var msg = findLabelText(e);
161 					msg += ": " + e.title;
162 					errMsg.push(msg);
163 					classString = ""; // no need to look further into this element (and: this is *not* a submit element!)
164 					continue;
165 				}
166 			}
167 			// submitbutton with focus and "confirm" class?
168 			if (e === lastFocused && searchConfirm.test(classString))
169 			{
170 				confirmSubmit = e; // save for later use
171 			}
172 		}
173 		// check if there are errors
174 		if (errMsg.length !== 0)
175 		{
176 			var legends = this.getElementsByTagName("legend");
177 			// we simply pick the first legend, supposing it's for the whole form
178 			var heading = legends.length > 0 ? Nornix.dom.getTextContent(legends[0]) : thiz.texts.errHeading;
179 			if (firstError)
180 			{
181 				firstError.focus();
182 			}
183 			alert("===  " + heading + "  ===\n\n"+errMsg.join("\n"));
184 			Nornix.events.cancel(event);
185 			return;
186 		}
187 		// check if the form submit should be confirmed
188 		if (confirmSubmit)
189 		{
190 			// get text for confirm question
191 			if (!confirmSubmit.title || confirmSubmit.title === "")
192 			{
193 				confirmSubmit.title = thiz.texts.confirmText;
194 			}
195 			// ask user to confirm
196 			if (!confirm(confirmSubmit.title + '?'))
197 			{
198 				// don't send the form
199 				Nornix.events.cancel(event);
200 				return;
201 			}
202 		}
203 		// show message if the form is OK
204 		if (searchOkMessage.test(this.className))
205 		{
206 			alert(thiz.texts.okForm);
207 		}
208 		if (thiz.settings.ieFixButtons && Nornix.util.isIe)
209 		{
210 			// disable not pushed submitting buttons, to make IE6 play nice
211 			i = 0;
212 			while (e = elements[i++])
213 			{
214 				if (Nornix.dom.eqNodeName(e, 'button') && e !== lastFocused)
215 				{
216 					e.disabled = true;
217 				}
218 			}
219 		}
220 		// check for more functions to run before submitting the form
221 		if (this.chainedSubmit) this.chainedSubmit(event);
222 	};
223 
224 	/**
225 	 * Cleanup method on form reset.
226 	 * Sets all elements to "valid" state.
227 	 * If there is a function assigned to the {@link chainedReset} property of
228 	 * the form, it will be called when the form is cleaned.
229 	 * @private
230 	 */
231 	var resetForm = function (event)
232 	{
233 		if (!this.elements) return; // nothing to work with, quit
234 		var i=0, e;
235 		while (e = this.elements[i++])
236 		{
237 			setValid(e);
238 		}
239 		if (this.chainedReset) this.chainedReset(event);
240 	};
241 
242 	/**
243 	 * Find the label text for a form field.
244 	 * Uses [@link findLabelElement) to find the label element.
245 	 * @private
246 	 * @param {HTMLElement} el form element to find the label text for
247 	 * @return label text of element
248 	 * @type String
249 	 */
250 	function findLabelText (el)
251 	{
252 		var label = findLabelElement(el);
253 		if (label) return Nornix.dom.getTextContent(label);
254 		return "";
255 	}
256 
257 	/**
258 	 * Find the label element for a form field.
259 	 * @private
260 	 * @param {HTMLElement} el form element to find the label for
261 	 * @return label element for element or null
262 	 * @type HTMLLabelElement
263 	 */
264 	function findLabelElement (el)
265 	{
266 		if (el.parentNode.nodeName.toLowerCase() === 'label')
267 		{
268 			// el(ement) is nested inside label tag, no "for" attribute is needed
269 			return el.parentNode;
270 		}
271 		// get id of element
272 		var id = el.name ? el.name : (el.id ? el.id : null);
273 		if (id === null) return null;
274 		// loop thru all labels in this form
275 		var labels = el.form.getElementsByTagName("label"), i=0, labl;
276 		while (labl = labels[i++])
277 		{
278 			// check "for" attribute of label
279 			if (labl.htmlFor == id)
280 			{
281 				return labl;
282 			}
283 		}
284 		return null;
285 	}
286 
287 	/**
288 	 * Sets "focus memory", called as an event handler.
289 	 * @private
290 	 */
291 	function setFocus ()
292 	{
293 		focusedElement = this;
294 	}
295 
296 	/**
297 	 * Empties the "focus memory", called as an event handler.
298 	 * @private
299 	 */
300 	function clearFocus ()
301 	{
302 		focusedElement = null;
303 	}
304 
305 	/**
306 	 * Set a form field to look valid.
307 	 * @private
308 	 * @param {HTMLElement} obj form element to set to "valid"
309 	 */
310 	function setValid (obj)
311 	{
312 		setFieldState(obj, Nornix.css.remove);
313 	}
314 
315 	/**
316 	 * Set a form field to look invalid.
317 	 * @private
318 	 * @param {HTMLElement} obj form element to set to "invalid"
319 	 */
320 	function setInvalid (obj)
321 	{
322 		setFieldState(obj, Nornix.css.add);
323 	}
324 
325 	/**
326 	 * Set a form field and it's label to some state.
327 	 * @private
328 	 * @param {HTMLElement} obj form element to set to "valid"
329 	 * @param {Function} stateAction action(obj) that changes the state
330 	 */
331 	function setFieldState (obj, stateAction)
332 	{
333 		stateAction(obj, "invalid");
334 		o = findLabelElement(obj);
335 		if (o)
336 		{
337 			stateAction(o);
338 		}
339 	}
340 };
341 
342 /*
343 
344 FORM FIELD VALIDATOR FUNCTIONS
345 
346 - the validator function gets the value of a field
347 - the functions returns true on success
348 - there can be multiple validators for a single field
349 - the validators are called by using their names as classnames on the field
350 
351 */
352 
353 Nornix.FormHandler.prototype.validators =
354 {
355 	/**
356 	 * Validate required form field.
357 	 * @param {Object} val form field value
358 	 * @return true on successful validation
359 	 * @type boolean
360 	 */
361 	validateRequired : function(val)
362 	{
363 		if (val == null) return false;
364 		return (val.length > 0 && !/^\s+$/.test(val));
365 	},
366 
367 	/**
368 	 * Validate a full name, that needs to have at least one given and one surname.
369 	 * @param {Object}} val form field value
370 	 * @return true on successful validation
371 	 * @type boolean
372 	 */
373 	validateFullName : function(val)
374 	{
375 		var regE = /^(([a-zA-ZŠŒŽšœžŸ¥µÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ-]){1,}( |$){1}){2,4}$/;
376 		return (regE.test(val));
377 	},
378 
379 	/**
380 	 * Validate a name, only characters and hyphens are allowed.
381 	 * @param {Object}} val form field value
382 	 * @return true on successful validation
383 	 * @type boolean
384 	 */
385 	validateName : function(val)
386 	{
387 		var regE = /^(([a-zA-ZŠŒŽšœžŸ¥µÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ-]){1,}( |$){1}){1,4}$/;
388 		return (regE.test(val));
389 	},
390 
391 	/**
392 	 * Validate an email address.
393 	 * @see http://www.quirksmode.org/js/mailcheck.html
394 	 * @param {Object}} val form field value
395 	 * @return true on successful validation
396 	 * @type boolean
397 	 */
398 	validateEmail : function(val)
399 	{
400 		var regE = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4}){1}$/;
401 		return (regE.test(val));
402 	},
403 
404 	/**
405 	 * Validate a number (can be any value).
406 	 * @param {Object}} val form field value
407 	 * @return true on successful validation
408 	 * @type boolean
409 	 */
410 	validateNumber : function(val)
411 	{
412 		return !isNaN(val);
413 	},
414 
415 	/**
416 	 * Validate a number, which has to be greater than zero.
417 	 * @param {Object}} val form field value
418 	 * @return true on successful validation
419 	 * @type boolean
420 	 */
421 	validatePositiveNumber : function(val)
422 	{
423 		return !isNaN(val) && val > 0;
424 	},
425 
426 	/**
427 	 * Validate an integer, which has to be greater than zero.
428 	 * @param {Object}} val form field value
429 	 * @return true on successful validation
430 	 * @type boolean
431 	 */
432 	validatePositiveInteger : function(val)
433 	{
434 		return !isNaN(val) && val > 0 && /^[0-9]+$/.test(val);
435 	}
436 };
437 
438 /*
439 	How to add a validator without making changes to this file.
440 	The function should return true on successful validation.
441 	You can also replace an existing function this way.
442  */
443 Nornix.FormHandler.prototype.validators.validateABC = function(val)
444 {
445 	return val === "ABC";
446 };
447 
448 
449 // settings
450 
451 
452 Nornix.FormHandler.prototype.texts =
453 {
454 	/**
455 	 * Default confirm text for confirm dialogs.
456 	 * @type String
457 	 */
458 	confirmText : "Confirm action",
459 	/**
460 	 * Default error message heading for error dialogs.
461 	 * @type String
462 	 */
463 	errHeading : "Form",
464 	/**
465 	 * Default message for confirming that the form is beeing sent.
466 	 * @type String
467 	 */
468 	okForm : "Thank you, the form is beeing submitted!"
469 };
470 
471 Nornix.FormHandler.prototype.settings =
472 {
473 	/**
474 	 * Set to true to remove non-pushed submit "button" elements in IE (bug fix for IE)
475 	 * @type boolean
476 	 */
477 	ieFixButtons : true
478 };
479 
480 new Nornix.FormHandler();
481