//aryooeye.ValidationManager;r.0.0.0;richarduie[at]yahoo[dot]com;(c)2008

/////////////////////////////////////////////////////////////////////////
//                                                                     //
//   validation message management using JavaScript object prototype   //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
//                                                                     //
//   RUI library member that enforces containment under "aryooeye"     //
//   namespace; offers pseudo-static reference options for public      //
//   members via name by dot(.)-references to members that can rely    //
//   on "instantiation" calls to default, no-argument constructor.     //
//                                                                     //
/////////////////////////////////////////////////////////////////////////
//                                                                     //
//   Filename.......ValidationManager.js                               //
//   Author.........Richard Harrison (richarduie[at]yahoo[dot]com)     //
//   Created On.....2007-01-31                                         //
//   Last Revised...2008-01-31                                         //
//                                                                     //
/////////////////////////////////////////////////////////////////////////

/**
 *  @Object: ValidationManager
 *
 *
 *  @Syntax: var valMgr = new aryooeye.ValidationManager()
 *           ValidationManager.{method_name}() [static]
 *
 *  @Goals:  add or apply validation to page elements
 */


/////////////////////////////////////////////////////////////////////
//   N A M E S P A C E   C O N T A I N E R   O V E R H E A D
//
//   Enforce the condition that any RUI object can only be
//   created under the namespace aryooeye in order to further
//   ensure protection from potential name-collisions.
//
	// If namespace object does not yet exist, create it.
	if ('undefined' == typeof aryooeye) var aryooeye = new Function();
	// If object has already been attached, exit.
	if ('undefined' == typeof aryooeye.ValidationManager)
{	// open conditional object creation
	// Now add current object to aryooeye namespace.
	aryooeye.ValidationManager =

function ValidationManager() 

{
	//////////////////////////////////////////////////////////////////
	//  D E P E N D E N C Y   M A N A G E M E N T  ///////////////////
	//  aryooeye.StringUtil
	//  aryooeye.PopupDivManager

	//////////////////////////////////////////////////////////////////
	//  P R I V A T E   M E M B E R S  ///////////////////////////////
		// Declare private methods.
		// convenience function
		function get(eid) { return document.getElementById(eid); }

		function composeBadCharMsg(msgStub, badChars) {
			var tail = '';
			// if only one bad character report it
			if (1 == badChars.length) tail = ': ' + badChars;
			// ...otherwise, report a comma-delimited list of the bad characters
			else tail = 's: ' + strUtil.delimitListItems(badChars.split(''));
			return msgStub + tail;
		}
	
		function register(pid, item) {
		// record association of error messages with corresponding items
			msgRegistry[pid] = item;
		}

		function setSnap() {
			var cmd = 'window.onresize = ' + name + '.snapToItems';
			// this must be set into external document frame of calling stack 
			// in order to become available for call as an event-handler from 
			// outside - no delay required...won't be "instantaeous," but 
			// should easily happen before user can resize window
			setTimeout(cmd, 0);
			isSnapSet = true;	// won't need to happen again
		}

		// Declare private fields.
			var strUtil = new aryooeye.StringUtil();
			var msgRegistry = new Array();
			var isSnapSet = false;
			var isIE = -1 != navigator.userAgent.indexOf('MSIE');
			var ieHeightScalar = 1.8;

			var name = 'vm';	// default name for object - reset as needed to avoid name-collisions

			// - publicly gettable and settable
			// string array of valid domain suffixes
			var domainSuffixArray = [
				'ac','ad','ae','aero','af','ag','ai','al','am','an','ao','aq','ar','arpa','as','at','au','aw','az',
				'ba','bb','bd','be','bf','bg','bh','bi','biz','bj','bm','bn','bo','br','bs','bt','bv','bw','by','bz',
				'ca','cc','cf','cg','ch','ci','ck','cl','cm','cn','co','com','coop','cr','cs','cu','cv','cx','cy','cz',
				'de','dj','dk','dm','do','dz',
				'ec','edu','ee','eg','eh','er','es','et','eu',
				'fi','firm','fj','fk','fm','fo','fr','fx',
				'ga','gb','gd','ge','gf','gh','gi','gl','gm','gn','gov','gp','gq','gr','gs','gt','gu','gw','gy',
				'hk','hm','hn','hr','ht','hu',
				'id','ie','il','in','info','int','io','iq','ir','is','it',
				'jm','jo','jobs','jp',
				'ke','kg','kh','ki','km','kn','kp','kr','kw','ky','kz',
				'la','lb','lc','li','lk','lr','ls','lt','lu','lv','ly',
				'ma','mc','md','mg','mh','mil','mk','ml','mm','mn','mo','mp','mq','mr','ms','mt','mu','museu','mv','mw','mx','my','mz',
				'na','name','nato','nc','ne','net','nf','ng','ni','nl','no','nom','np','nr','nt','nu','nz',
				'om','org','pa',
				'pe','pf','pg','ph','pk','pl','pm','pn','pr','pro','pt','pw','py',
				'qa',
				're','ro','ru','rw',
				'sa','sb','sc','sd','se','sg','sh','si','sj','sk','sl','sm','sn','so','sr','st','store','su','sv','sy','sz',
				'tc','td','tf','tg','th','tj','tk','tm','tn','to','tp','tr','trave','tt','tv','tw','tz',
				'ua','ug','uk','um','us','uy',
				'va','vc','ve','vg','vi','vn','vu',	
				'web','wf',	'ws',	
				'ye',	'yt',	'yu',	
				'za',	'zm',	'zr','zw'
			]

			// set default characters used to validate semantic types
			var emailChars = 'abcdefghijklmnopqrstuvwxyz';
				emailChars += emailChars.toUpperCase(nameChars) + '@.-_';
				emailChars += '0123456789';
			var nameChars = 'abcdefghijklmnopqrstuvwxyz';
				nameChars += nameChars.toUpperCase(nameChars) + '-\'';
			var numeralChars = '0123456789.-';
			var phoneChars = '0123456789-';
			var zipChars = '0123456789';
			var zip4Chars = '0123456789-';
			var passChars = nameChars + numeralChars;
			var wordFilterList = '';

	//////////////////////////////////////////////////////////////////
	//  P U B L I C   M E M B E R S  /////////////////////////////////
		// Declare public methods.
			// - accessors and mutators...privileged updates of private fields
			//  use of any of the setters will override previous settings with
			//  new values that will be used for all future calls
			this.getEmailChars = function() { return emailChars; }
			this.setEmailChars = function(e) { emailChars = e; }

			this.getNameChars = function() { return nameChars; }
			this.setNameChars = function(n) { nameChars = n; }

			this.getNumeralChars = function() { return numeralChars; }
			this.setNumeralChars = function(n) { numeralChars = n; }

			this.getPhoneChars = function() { return phoneChars; }
			this.setPhoneChars = function(p) { phoneChars = p; }

			this.getZipChars = function() { return zipChars; }
			this.setZipChars = function(z) { zipChars = z; }

			this.getZip4Chars = function() { return zip4Chars; }
			this.setZip4Chars = function(z) { zip4Chars = z; }

			this.getName = function() { return name; }
			this.setName = function(n) { name = n; }

			this.getIeHeightScalar = function() { return ieHeightScalar; }
			this.setIeHeightScalar = function(i) { ieHeightScalar = i; }

			this.getWordFilterList = function() { return wordFilterList; }
			this.setWordFilterList = function(l) { if (l && l.length) wordFilterList = l; }

			// - general validation utilities
			this.isInList = function(word, list, strict) {
				if ('undefined' == typeof strict) strict = true;
				var is = false;
				var last = list.length;
				for (var i = 0; i < last; i++) {
					is = strict?(word == list[i]):(-1 != word.indexOf(list[i]));
					if (is) break;
				}
				return is;
			}

			// - validation of strings with semantic types
			this.validateEmail = function(email, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
//rfh:ts-	if (0 == email.length) msg = 'email address missing';
				if (5 > email.length || 200 < email.length) msg = 'email must be 5-200 characters';	//rfh:ts+
				// ...otherwise, check content and structure
				else {
					if (!cset) cset = emailChars;		// use default, if none specified
					// throw out all valid characters from email address
					var badChars = strUtil.dAN(email, cset);
					// check for remaining, invalid characters
					if (0 != badChars.length) {
						// set base text for error message
						msg = 'email address contains invalid character';
						// if only one bad character report it
						if (1 == badChars.length) msg += ': ' + badChars;
						// ...otherwise, report a comma-delimited list of the bad characters
						else msg += 's: ' + strUtil.delimitListItems(badChars.split(''));
					}
					// check for missing @ sign
					else if (-1 == email.indexOf('@')) {
						msg = 'missing [at] in email address';
					}
					// check for missing . in domain name
					else if (-1 == email.indexOf('.')) {
						msg = 'missing [dot] in email address';
					}
					// verify that @ sign follows at least one preceding character
					else if (0 == email.indexOf('@')) {
						msg = 'email address can\'t start with the @ sign';
					}
					// verify that the domain suffix is valid, e.g., com, edu, etc.
					else if (!this.isInList(email.substring(1 + email.lastIndexOf('.')), domainSuffixArray) ) {
						var s = email.substring(1 + email.lastIndexOf('.'));
						if (0 == s.length) msg = 'missing domain suffix for email address';
						else msg = s + ' is not a valid domain suffix for an email address';
					}
				}
				return msg;	// advise caller of result
			}

			this.validatePass = function(pass, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
//rfh:ts-	if (0 == pass.length) msg = 'password missing';
				if (8 > pass.length || 16 < pass.length) msg = 'password must be 8-16 characters';	//rfh:ts+
				// ...otherwise, check content and structure
				else {
					if (!cset) cset = passChars;		// use default, if none specified
					// throw out all valid characters from name
					var badChars = strUtil.dAN(pass, cset);
					// check for remaining, invalid characters
					if (0 != badChars.length) {
						msg = composeBadCharMsg('password contains invalid character', badChars);
					}
				}
				return msg;
			}

			this.validateName = function(name, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
//rfh:ts-	if (0 == name.length) msg = 'name missing';
				if (2 > name.length || 32 < name.length) msg = 'name must be 2-32 characters';	//rfh:ts+
				// ...otherwise, check content and structure
				else {
					if (!cset) cset = nameChars;		// use default, if none specified
					// throw out all valid characters from name
					var badChars = strUtil.dAN(name, cset);
					// check for remaining, invalid characters
					if (0 != badChars.length) {
						msg = composeBadCharMsg('name contains invalid character', badChars);
					}
				}
				return msg;
			}

			this.validatePhone = function(phone, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
				if (0 == phone.length) msg = 'phone number missing';
				// ...otherwise, check content and structure
				else {
					if (!cset) cset = phoneChars;		// use default, if none specified
					// throw out all valid characters from phone number
					var badChars = strUtil.dAN(phone, cset);
					// check for remaining, invalid characters
					if (0 != badChars.length) {
						msg = composeBadCharMsg('phone number contains invalid character', badChars);
					}
					// verify conformance to format
					else if ('-' != phone[3] || '-' != phone[7] || 
						10 != (strUtil.dAN(phone, '-')).length) {
						msg = 'phone number must be like: 999-999-9999';
					}
				}
				return msg;
			}

			this.validateZip = function(zip, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
				if (0 == zip.length) {
					msg = 'zip code missing';
				}
				// ...otherwise, check content and structure
				else {
					if (!cset) cset = zipChars;
					// throw out all valid characters from zip
					var badChars = strUtil.dAN(zip, cset);
					// check for remaining, invalid characters
					if (0 != badChars.length) {
						msg = composeBadCharMsg('zip code contains invalid character', badChars);
					}
					// verify length
					else if (5 != zip.length) {
						msg = 'zip code must be like: 99999';
					}
				}
				return msg;
			}

			this.validateZip4 = function(zip, cset) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
				if (0 == zip.length) msg = 'zip code missing';
				// ...otherwise, check content and structure
				else {
					// throw out all valid characters from zip+4
					if (!cset) cset = zip4Chars;
					// check for remaining, invalid characters
					var badChars = strUtil.dAN(zip, cset);
					if (0 != badChars.length) {
						msg = composeBadCharMsg('zip code +4 contains invalid character', badChars);
					}
					// verify conformance to format
					else if ('-' != zip[5] || 9 != (strUtil.dAN(zip, '-')).length) {
						msg = 'zip code +4 must be like: 99999-9999';
					}
				}
				return msg;
			}

			this.validateWordFilter = function(word, flist, wname) {
			// if only validation and potentially corresponding error message are required, 
			// this method may be called directly - empty, string return indicates "good"
				var msg = '';		// initialize message to indicate "no problems"
				if ('undefined' == typeof flist || 0 == flist.length) return msg;
				if ('undefined' == typeof wname) wname = 'word';
				if (0 == word.length) msg = wname + ' missing';
				// ...otherwise, check content and structure
				else {
					// check for occurrence of word in comma-delimited f(ilter) list of words - 
					// first test is trivial, fast check that works in one pass for exact matches
					// if the cheap check fails, examine words in {flist} to determine whether 
					// any of them is (or is embedded in) the {word}
					if (-1 != (flist + ',').indexOf(word + ',') || 
						this.isInList(word.toLowerCase(), flist.split(','), false)) 
						msg = '"' + word + '" is not allowed';
				}
				return msg;
			}


			// - form validation
			this.validateForm = function(fid) {
			// step through the inputs of a form, validating each by type
				var msg = '';					// initialize to indicate "no problem"
				var valid = true;
				var f = get(fid);				// get convenience reference to form
				var last = f.length;			// how many children of form?
				for (var i = 0; i < last; i++) {
					var item = f[i];			// get pointer to current child item
					switch (item.type) {		// branch control by input type
						case 'button': {
							continue;			// nothing at this time
						}
						case 'checkbox': {
							continue;			// nothing at this time
						}
						case 'radio': {
							continue;			// nothing at this time
						}
						case 'select': {
							continue;			// nothing at this time
						}
						case 'password': 		// same rules for password and text
						case 'text': {
							// item should have one or more of semantic type names 
							// assigned to its class attribute - if multiple, types 
							// are given, each validation will be run until one 
							// fails - semantic classes begin with 'ruivm'
							var className = item.className.split(',');
							var lastCN = className.length;
							for(var j = 0; j < lastCN; j++) {
								// check each class name for possibility that it is 
								// aryooeye.ValidationManager (ruivm) semantic class, 
								// and, if so, pass for validation
								if (-1 != className[j].indexOf('ruivm')) {
									valid = valid && this.validateTextItem(item, true, null, className[j]);
								}
							}
						}
						case 'textarea': {
							continue;	// nothing at this time
						}
					}
				}
				return valid;
			}

			this.validateTextItem = function(item, forceFocus, targetId, className) {
			// validate contents and structure of string content of text inputs
			// by giving ruivm prefixed semantic classes to elements to be validated and 
			// passing all validation calls here, messages are registered for ongoing 
			// management - individual validation methods may be be called directly, if 
			// this is not required
				// register window resize event-handler, if not yet done
				if (!isSnapSet && null == targetId) setSnap();
				// if method called directly, not all arguments mayhave been given - 
				// in that case, presume that semantic class is in first position, if 
				// more than one class is provided
				if (!className) {
					className = item.className;
					// comma-delimited list indicates multiple classes
					if (-1 != className.indexOf(',')) {
						// parse first class name from list
						className = className.substring(0, className.indexOf(','));
					}
				}
				if ('undefined' == typeof forceFocus) forceFocus = true;
				if ('undefined' == typeof targetId) targetId = null;
				var pid = '_ruivm_' + item.id;	// create page id for new message
				var msg = '';							// initialize message to indicate "no problem"
				var valid = true;						// initialize validation switch to "good"
				// do the validation by semantic class
				switch (className) {
					case 'ruivmEmail': {
						msg = this.validateEmail(item.value);
						break;
					}
					case 'ruivmPass': {
						msg = this.validatePass(item.value);
						break;
					}
					case 'ruivmName': {
						msg = this.validateName(item.value);
						if ('' == msg && 0 != this.getWordFilterList().length) 
							msg = this.validateWordFilter(item.value, this.getWordFilterList(), 'name');
						break;
					}
					case 'ruivmPhone': {
						msg = this.validatePhone(item.value);
						break;
					}
					case 'ruivmZip': {
						msg = this.validateZip(item.value);
						break;
					}
					case 'ruivmZip4': {
						msg = this.validateZip4(item.value);
						break;
					}
				}
				// if there is a non-empty message, there is a problem
				if (0 < msg.length) {
					valid = false;		// set return to advise caller
					if (null == targetId) {
						var el = get(item.id);				// get convenience reference to element
						// get top and left positions of element relative to its immediate parent
						// if needed, use popMgr's font-size for the IE correction here, since the 
						// message doesn't yet exist and will be created with popMgr's current value 
						// of font-size get left position of element
						var t = el.offsetTop;
						var l = el.offsetLeft + (isIE?(parseInt(this.popMgr.getFontSize())):0);
						// get height of element
						var h = el.offsetHeight + (isIE?(ieHeightScalar*parseInt(this.popMgr.getFontSize())):0);
						var w = el.offsetWidth;				// get width of element
						// create a popup div to contain to message
						this.popMgr.doPopup(pid, msg, null, null, l, t + h, false);
					}
					else {
						document.getElementById(targetId).innerHTML = msg;
					}
					if (forceFocus) {
						// item.focus() isn't guaranteed to work for direct calls - 
						// set timer call into page frame of calling stack
						var cmd = "document.getElementById('" + item.id + "').focus()";
						setTimeout(cmd, 100);
					}
					// record message and corresponding item to which it pertains
					register(pid, item);
				}
				else {
					if (null == targetId) {
						// ...otherwise, all is well - remove any existing message/item 
						// association from registry
						this.popMgr.destroyPopup('_ruivm_' + item.id);
						register(pid, null);
					}
					else {
						document.getElementById(targetId).innerHTML = '&nbsp;';
					}
				}

				return valid;	// tell caller about state
			}


			this.snapToItems = function() {
			// event-handler for window resize events - for all of the outstanding 
			// error messages, move them to positions that keep the asscociation 
			// of the message to its corresponding item  visually unambiguous
				for (key in msgRegistry) {
					// get reference to current message
					var msg = document.getElementById(key);
					// get reference to item associated with current message
					try {
						var item = document.getElementById(msgRegistry[key].id);
					}
					catch (e) {
						return;
					}
					var h = item.offsetHeight;			// get item's height
					// if needed, use message's actual font-size for IE corrections here - 
					// popMgr's value may have been changed, since this item was created
					// get left position of element
					var l = item.offsetLeft + (isIE?(parseInt(msg.style.fontSize)):0);
					// get height of element
					var h = item.offsetHeight + (isIE?(ieHeightScalar*parseInt(msg.style.fontSize)):0);
					var t = item.offsetTop;				// get item's top position
					msg.style.left = l + 'px';			// update message's left position
					msg.style.top = (h + t) + 'px';	// update message's top position
				}
			}

			// make public, so that default display attributes may altered, if desired
			this.popMgr = new aryooeye.PopupDivManager();
				this.popMgr.setColor('red');
				this.popMgr.setBgColor('transparent');
				this.popMgr.setBorderColor('transparent');
				this.popMgr.setFontSize('9px');
				this.popMgr.setPadding(0);

			// general functions - - - - - - - - - - - - - - - - - -

			this.getClassName = function() {
					return 'ValidationManager';
			}

		// Declare public fields.
			// in case instance needs to know its name, set it directly,
			// after instantiation, i.e., {instance}.name = '{name}' -
			// this is purely an optional, convenience field, which
			// could be attached independent of this definition, but is
			// declared here with a default value for possible use in
			// the report() method
			// set default value - may be used by report()
			this.name = '_name_not_set_';

			// provide dot referencable field of standard property name - can
			// be overridden, but will be refreshed to actual name of "class,"
			// if getter is caller
			this.className = this.getClassName();


	//////////////////////////////////////////////////////////////////
	//  C O N S T R U C T O R  ///////////////////////////////////////
		// Construction steps - only run at instantiation.
		// ...no differential construction required at present...
}

}

/////////////////////////////////////////////////////////////////////
//   S U P P O R T    P S E U D O - S T A T I C    C L A S S - 
//   S T Y L E    R E F E R E N C E S
	//	Since constructor supports default, no-argument instantiation, 
	// provide a "static" reference version to the public members.
	// Note that this removes rui namespace insulation against name-
	// collisions; developer is responsible for managing name-safety 
	// in implementation context. Next statement can be commented or 
	// de-commented without affecting base object in any way.
//	var ValidationManager = new aryooeye.ValidationManager();
