﻿/*
 * The Smield : An unobtrusive javascript UI Helping SMart fIELD
 * Copyright(c) 2007, Dave Transom.
 * http://www.singular.co.nz/blog/archive/2007/05/14/unobtrusive-javascript-smart-field.aspx
 * 
 * This code is licensed under BSD license. Use it as you wish, 
 * but keep this copyright intact.
 *
 * version: 1.0.1
 */

if( typeof CSharpVitamins == "undefined" )
	var CSharpVitamins = {};

(function(){

	// aliases / shorthand
	var ns = CSharpVitamins; // shorthand for namespace to place the control under e.g. var ns = window;, var ns = MyProject.MyControls;, etc...
	
	// private members
	var isIE = /*@cc_on!@*/false,
		counter = 0,  // for auto-generated dom Id's
		addEvent;     // library branching - prototype/yui
	
	var createId = function( prefix ){
		return (prefix||"smieldAutoId") + "_" + (++counter);
	};
	
	var createGroupName = function(){
		return "smield" + (++counter);
	};
	
	var trim = function( value ){
		return String( value ).replace( /^\s+|\s+$/g, "" );
	};
	
	// generic find function e.g. findBy( list, function ) or findBy( list, "member name (key)", value )
	var findBy = function(){
		var a = arguments, list = a[0];
		if( a.length == 3 ){
			var key = a[1], value = a[2];
			for( var i = 0, l = list.length; i < l; ++i ){
				if( list[i][key] === value ) return list[i];
			}
		} else if( a.length == 2 && typeof a[1] == "function" ){
			var predicate = a[1];
			for( var i = 0, l = list.length; i < l; ++i ){
				if( predicate( list[i] ) ) return list[i];
			}
		}
		return null;
	};

	var getElement = function( element ){
		if( typeof element == "string" )
			return document.getElementById( element );
		return element || null;
	};

	// thanks Prototype :)
	var extend = function( destination, source ){
		for( var key in source ){
			destination[ key ] = source[ key ];
		}
		return destination;
	};

	// thanks YUI :)
	var hasClass = function( element, name ){
		var re = new RegExp( "(?:^|\\s+)" + name + "(?:\\s+|$)");
		return re.test( element.className );
	};

	var addClass = function( element, name ){
		if( hasClass( element, name ) ) return;
		element.className = [element.className, name].join( ' ' );
	};

	var removeClass = function( element, name ){
		if( !hasClass( element, name ) ) return;
		var re = new RegExp( "(?:^|\\s+)" + name + "(?:\\s+|$)", "g");
		element.className = element.className.replace( re, ' ' );
	};
	
	// add / remove event depends on library
	if( typeof YAHOO != "undefined" && YAHOO.util.Event ){
		var E = YAHOO.util.Event;
		addEvent = function(){ E.on.apply( E, arguments ); };
	} else if( typeof Prototype != "undefined" ){
		addEvent = function(){ Event.observe.apply( Event, arguments ); };
	} else if( typeof $addHandler == "function" ){ // MS AJAX
		var cache = [];
		
		addEvent = function(){ 
			$addHandler.apply( this, arguments );
			cache.push( arguments[ 0 ] );
		};
		
		Sys.Application.add_unload( function(){
			for( var i = cache.length - 1; i >= 0; --i ){
				$clearHandlers( cache[i] );
				cache[i] = null;
			}
			cache = null;
		} );
	} else {
		var cache = [];
		
		addEvent = function( element, type, handler, capture ){
			if( element.addEventListener )
				element.addEventListener( type, handler, capture || false );
			else if( window.attachEvent )
				element.attachEvent( "on" + type, handler );
			cache.push( arguments );
		};
		
		addEvent( window, "unload", function(){
			for( var i = cache.length - 1; i >= 0; --i ){
				var item = cache[i];
				if( item[0].removeEventListener ){
					item[0].removeEventListener( item[1], item[2], item[3] || false );
				} else if( item[0].detachEvent ){
					try { item[0].detachEvent( "on" + item[1], item[2] ); }
					catch( e ) {}
				}
				item[0] = null;
			}
			cache = null;
    	} );
	}
	
	/**
	* Provides configuration for choices within a smield
	* @namespace CSharpVitamins
	* @class SmieldChoice
	* @constructor
	* @param {String}	name	The name of the choice, should be unique
	* @param {String}	label	The text/prompt that will be displayed for selecting the choice
	* @param {String | Array}	members	The string or array of element ID's to participate in the smield
	* @param {String}	separator	The string to seperate fields with, if applicable. Defaults to a space " ".
	*/
	ns.SmieldChoice = function( name, label, members, separator ){
		this.name = name; // name of the choice
		this.label = label || ""; // text for label
		this.separator = separator || " ";
		
		this.fields = [];
		this.hasFields = false;
		this.trigger = null; // the radio button that will be used to trigger the smield
		
		if( members ){
			if( typeof members == "string" )
				this.fields.push( getElement( members ) );
			else {
				for( var i = 0, field = null; null != ( field = getElement( members[i] ) ); ++i )
					this.fields.push( field );
			}
			this.hasFields = this.fields.length > 0;
		}
	};
	
	ns.SmieldChoice.prototype = {
		getTrigger: function( group ){
			if( !this.trigger ){
				var radio;
				if( isIE ){
					radio = document.createElement( "<input type=\"radio\" id=\"" + createId( group ) 
						+ "\" name=\"" + group + "\" value=\"" + this.name + "\" />" );
				} else {
					radio = document.createElement( "input" );
					radio.type = "radio";
					radio.name = group;
					radio.id = createId( group );
					radio.value = this.name;
				}
				this.trigger = radio;
			}
			return this.trigger;
		},
		
		getValue: function(){
			var results = [];
			for( var i = 0, l = this.fields.length; i < l; ++i ){
				var value = trim( this.fields[i].value );
				if( value.length > 0 )
					results.push( value );
			}
			return results.join( this.separator );
		}
	};
	
	/**
	* Provides the main functionality for creating a smield
	* @namespace CSharpVitamins
	* @class Smield
	* @constructor
	* @param {String | HTMLElement}	input	The input field to changes will be made to.
	* @param {Array}	choices	An array of SmieldChoice objects which will be created at runtime.
	* @param {Object}	options	A hashtable of options which to override. Keys that can 
								be set are: group, listClass, disabledClass, filter, position and protection.
	*/
	ns.Smield = function( input, choices, options ){
		var me = this;
		
		// variables
		this.input = getElement( input );
		this.choices = choices;
		this.current = null; // current choice
		this.options = extend( { 
			group: createGroupName(),
			listClass: "smield",
			disabledClass: "disabled", 
			filter: ns.Smield.Filters.trim,
			position: ns.Smield.Position.below,
			protection: ns.Smield.Protection.readonly
		}, options || {} );
		
		this.event_swap = function( e ){ me.swap( e || window.event ); };
		this.event_update = function(){ me.update(); };
		
		// ui initialisation
		var	ul = document.createElement( "ul" ), 
			value = this.getValue(),
			defaultChoice = null;
		
		addClass( ul, this.options.listClass );
		if( this.options.position == ns.Smield.Position.above ){
			this.input.parentNode.insertBefore( ul, this.input );
		} else {
			this.input.parentNode.appendChild( ul );
		}
		
		for( var i = 0, choice = null; null != ( choice = this.choices[i] ); ++i ) {
			var li = document.createElement( "li" ), 
				radio = choice.getTrigger( me.options.group ), 
				label = document.createElement( "label" );
			
			label.htmlFor = radio.id;
			label.appendChild( document.createTextNode( choice.label ) );
			
			li.appendChild( radio );
			li.appendChild( label );
			ul.appendChild( li );
			
			addEvent( radio, "click", me.event_swap );
			for( var j = 0, field = null; null != ( field = choice.fields[j] ); ++j ){
				addEvent( field, field.tagName == "INPUT" ? "keyup" : "change", me.event_update );
				addEvent( field, "blur", me.event_update );
			}
			
			if( !me.current && choice.hasFields && value == me.options.filter( choice.getValue() ) ){
				me.current = choice;
				radio.checked = true;
			}
			
			if( !defaultChoice && !choice.hasFields ) // first empty choice as the default
				defaultChoice = choice;
		}
		
		if( !this.current ) 
			this.current = defaultChoice || this.choices[0];
		
		this.current.trigger.checked = true;
		this.update();
		
		// form observers
		if( this.input.form ){
			addEvent( this.input.form, "submit", function(){ me.beforeSubmit(); } );
			addEvent( this.input.form, "reset", function(){ me.beforeReset(); } );
		}
	};
	
	ns.Smield.prototype = {
		beforeSubmit: function(){
			if( this.options.protection == ns.Smield.Protection.disabled ){
				this.input.disabled = false;
			}
		},
		
		beforeReset: function(){
			// must occur after the form is reset
			var me = this;
			setTimeout( function(){ me.reset(); }, 10 );
		},
		
		swap: function( ev ){
			var trigger = ev.target || ev.srcElement;
			this.current = findBy( this.choices, "name", trigger.value );
			this.update();
		},
		
		update: function(){
			var value = this.options.filter( this.current.getValue() );
			if( value.length > 0 )
				this.input.value = value;
			
			// if there are fields participating in the current 'choice', 
			// then make sure it's out-of-reach for direct editing.
			this.input[ this.options.protection == ns.Smield.Protection.disabled 
				? "disabled" : "readOnly" ] = this.current.hasFields;
			
			if( this.current.hasFields ) addClass( this.input, this.options.disabledClass );
			else removeClass( this.input, this.options.disabledClass );
		},
		
		getValue: function(){
			return this.options.filter( this.input.value );
		},
		
		reset: function(){
			var value = this.getValue();
			if( value.length > 0 ){
				var me = this;
				
				var predicate = function( item ){ 
					return item.hasFields 
						&& value == me.options.filter( item.getValue() ); 
				};
				var choice = findBy( this.choices, predicate ) // find matching choice
					|| findBy( this.choices, "hasFields", false ) // or, if null, find an empty choice
					|| this.choices[0]; // or, default to first choice
				
				this.current = choice;
				this.current.trigger.checked = true;
				this.update();
			}
		}
	};
	
	/**
	* Provides an enumeration for the smield position 
	* @namespace CSharpVitamins.Smield
	* @class Position
	*/
	ns.Smield.Position = {
		below: "below",
		above: "above"
	};
	
	/**
	* Provides an enumeration for the smield protection 
	* @namespace CSharpVitamins.Smield
	* @class Protection
	*/
	ns.Smield.Protection = {
		readonly: "readonly",
		disabled: "disabled"
	};
	
	/**
	* Provides common filters used to transform smield output
	* @namespace CSharpVitamins.Smield
	* @class Filters
	*/
	ns.Smield.Filters = {
		none: function( value ){
			return value;
		},
		
		trim: trim,
		
		username: function( value ){
			var filters = ns.Smield.Filters;
			return filters.chain( value, [
				filters.company,
				function( v ){ return v.replace( /[^a-z0-9\._@]/ig, "" ); },
				filters.trim,
				filters.lower
			] );
		},
		
		company: function( value ){
			return value.replace( /\b(limited|ltd\.?)/ig, "" );
		},
		
		lower: function( value ){
			return value.toLowerCase();
		},
		
		upper: function( value ){
			return value.toUpperCase();
		},
		
		chain: function( value, filters ){
			for( var i = 0, filter = null; null != ( filter = filters[i] ); ++i )
				value = filter( value );
			return value;
		}
	};
	
})();