// © Copyright Kenny Grant 2007 
// Released under the BSD licence (save 2 extensions to Element and RegExp at end)
// Filter fragments of a page by date, text contents or other criteria
// On filtering those which match criteria are set to visible and others are hidden...

// Requires Prototype and Scriptaculous, and mc_utils

// Normally the filter values would be set by form fields in the HTML document
// which call this object to set_date_pattern or set_search_pattern
// Text searches may contain | or () for regexps but no other symbols

/*
	Simple example document
	
	<input  type="search" class="filter_control" name="text" value="" />

	<div class="fragment">
		This is a test
	</div>

	<div class="fragment">
		This is a second test
	</div>

*/



var FragmentFilter = {
	Version			: 1.1,
	FragmentTag		: null,						// element tag to filter (if present takes precendence over class)
	FragmentClass	: 'slide_container',		// class of elements to filter
	FilterClass		: 'filter_control', 		// form controls which filter
	FilterAction	: 'appear',					// the action to use when filtering (by default just fades in element)
	ProgressID		: 'progress_indicator', 	// a progress indicator (optional)
	fragments		: null, // list of fragment divs
	filters			: null, // list of filters to apply
	typing_timeout  : null, // A timeout set on window each time we receive keys

	visible_by_default : true, // Should items be visible by default - we say yes
	
	// initialize our list of fragments and our list of filters
	//
	//
	initialize: function() {
		FragmentFilter.filters = new Array();
				
	
		FragmentFilter.parse_arguments();
	
		
		// We look in page content first, if not use entire document
		// This avoids us filtering things in head or foot inadvertently
		var f_container = ($('page_content')) ? $('page_content') : document
	
		// find our fragments - either by tag or by classname (if FragmentTag is null)
		if (FragmentFilter.FragmentTag)
			FragmentFilter.fragments 		= $A(f_container.getElementsByTagName(FragmentFilter.FragmentTag));
		else
			FragmentFilter.fragments 		= $A(f_container.getElementsByClassName(FragmentFilter.FragmentClass));
		
		
		
		// find our filter controls and attach to them
		filter_controls = $A(document.getElementsByClassName(FragmentFilter.FilterClass));
		
		filter_controls.each(function(control) {
			FragmentFilter.attach_to_filter_control(control);
		});
	},
	
	// Parse the arguments given in script tag (if any)
	//
	//
	parse_arguments: function() {
		$A(document.getElementsByTagName("script")).findAll( function(s) 
			{return (s.src && s.src.match(/mc_filter\.js(\?.*)?$/))}).each( function(s) 
				{
				if (s.src.match(/\?.*fragment_tag=([^&;]*)/))
					FragmentFilter.FragmentTag = s.src.match(/\?.*fragment_tag=([^&;]*)/)[1];

				if (s.src.match(/\?.*action=([^&;]*)/))
					FragmentFilter.FilterAction = s.src.match(/\?.*action=([^&;]*)/)[1];	
	
					
				if (s.src.match(/\?.*filter=([^&;]*)/))
					FragmentFilter.FilterClass = s.src.match(/\?.*filter=([^&;]*)/)[1];
		
				if (s.src.match(/\?.*fragment=([^&;]*)/))
					FragmentFilter.FragmentClass = s.src.match(/\?.*fragment=([^&;]*)/)[1];
		
				if (s.src.match(/\?.*progress=([^&;]*)/))
					FragmentFilter.ProgressID = s.src.match(/\?.*progress=([^&;]*)/)[1];	
					
				});
		
		
	},
	
	// Attach ourself as observer to this filter control (a menu or a search field)
	//
	// 
	// Also set up a filter in filters array for each filter_control
	attach_to_filter_control: function(filter_control) {

	//	switch(filter_control.tagName.toLowerCase()) {
		
		
		// first observer for control sets off progress - no need for this is observer below is reasonably frequent
	//	new Form.Element.Observer(filter_control, 0.5,FragmentFilter.show_progress.bindAsEventListener(FragmentFilter));
		
		// then we respond
		new Form.Element.Observer(filter_control, 1.0,FragmentFilter.filter_changed.bindAsEventListener(FragmentFilter));

		// Set an initial lastvalue of an empty string
		filter_control.last_value = ""

	    // add to filter list
		FragmentFilter.update_filter_from_control(filter_control);
		
	},
	
	
	// The control has changed, so update the corresponding filter in our filter list, then filter
	//
	//
	filter_changed: function(filter_control) {
	//	debug_log("last_value-"+filter_control.value);		
	
		if (filter_control.value != filter_control.last_value)
			{
			FragmentFilter.show_progress();
			
			FragmentFilter.update_filter_from_control(filter_control);

			// if we already have a timeout, clear it
			if (FragmentFilter.typing_timeout != null)
				clearTimeout(FragmentFilter.typing_timeout);	

			FragmentFilter.typing_timeout = setTimeout('FragmentFilter.filter();',500);

			filter_control.last_value = filter_control.value;
			}
		
	},
	
	
	
	
	
	
	// Show the progress indicator (if it exists)
	//
	//
	show_progress: function(){
		p = $(FragmentFilter.ProgressID);
		if (p)
			Effect.Appear(p,{duration:0.3});
	},
	
	// Hide the progress indicator (if it exists)
	//
	//
	hide_progress: function(){
		p = $(FragmentFilter.ProgressID);
		if (p && Element.visible(p))
			Effect.Fade(p,{duration:0.4});	
	},

	
	// Add or set the values of a filter in our filter list
	//
	// Each filter control has a corresponding filter which holds the values to filter on
	update_filter_from_control: function(in_control) {
		var filter_id = in_control.id;
		var in_target = FragmentFilter.get_filter_target(in_control);
		var in_pattern = FragmentFilter.get_filter_pattern(in_control);
		
		var filter = FragmentFilter.find_filter(filter_id)
	
		if (!filter) 
			{
			// no corresponding filter exists - create a new one
			filter = new Filter(filter_id,in_pattern,in_target);
			FragmentFilter.filters.push(filter);
			//	debug_log("created-"+in_pattern+in_control.className);		
			}
		else
			{
			if ((filter.filter_pattern == in_pattern) || in_pattern == "")	
				FragmentFilter.hide_progress(); // results should be identical
						
		//	debug_log("set-"+in_target+"pat:"+in_pattern);
			filter.set_target(in_target);
			filter.set_pattern(in_pattern);	
			}
			
	},
	
	
	// Find a filter with a given id in our list of filters
	//
	//
	find_filter: function(in_filter_id) {
		return FragmentFilter.filters.find(function(f){ return f.filter_id == in_filter_id});
	},
	
	
	// Return the control name (used to store filter target)
	//
	// we have to special case menus, as name attributes are on options, not select element itself
	get_filter_target: function(filter_control) {
	
		if(filter_control.tagName.toLowerCase() == 'select')// is this a menu?
			return filter_control[filter_control.selectedIndex].getAttribute('name');
		else
			return filter_control.name;
	},
	
	
	// Return the control value
	//
	//
	get_filter_pattern: function(filter_control) {
		
		if (Element.hasClassName(filter_control,"placeholder"))
			{
			return "";
			}
		else
			return filter_control.value;	
	},
	


	// Run through our fragments filtering them and hiding if filtered
	//
	// 
	filter: function() {
 		// run through our list of fragments and filter each one with our filters
		FragmentFilter.fragments.each(function(fragment) {
			var visible = true; // set to visible
			
			// if any filters false, hide elsif all filters true, show
			FragmentFilter.filters.each(function(filter) {
				if (visible)
					visible = FragmentFilter.filter_fragment(fragment,filter);	

			}); // end of Filters iterator
		
			FragmentFilter.hide_or_show(fragment,visible);
		
		}); // end of Fragments iterator


		// always hide indicator regardless once we're done
		FragmentFilter.hide_progress();
	},


	// Get the element to show/hide (may be something inside fragment)
	//
	//
	hide_or_show: function(fragment,show){
		var element_to_act_on = fragment;
	/*	var fragment_class = fragment.getAttribute('class');
		
		if (FragmentFilter.VisibleClass != fragment_class)
			{
			// find the class within it
			element_to_act_on = fragment.getElementsByClassName(FragmentFilter.VisibleClass)[0];
			}*/
	
		if (show)	
			{

			if (!Element.visible(element_to_act_on))// if already visible do nothing	
				{
				if (FragmentFilter.FilterAction == 'blind')
					new Effect.BlindDown(element_to_act_on, {duration:0.3});	
				else
					Element.show(element_to_act_on);
				}
			}
		else
			{
			if (Element.visible(element_to_act_on))// if already hidden do nothing	
				{
				if (FragmentFilter.FilterAction == 'blind')
					new Effect.BlindUp(element_to_act_on, {duration:0.2});	
				else
					Element.hide(element_to_act_on);
				}			
			}

	},
	

	// Filter the given fragment with the given filter
	//
	// Returns boolean result indicating match or not
	filter_fragment: function(in_fragment,in_filter) {
		matched = false;
		
		target_name = in_filter.filter_target;
		filter_pattern = in_filter.filter_pattern;
					
		// no target or empty pattern means no search, so reset to default
		if (target_name == 'none' || filter_pattern == '')
			{
			matched = FragmentFilter.visible_by_default;
			}
		else if (target_name == 'text')
			{	
			// work around bug in safari meaning we get no text unless this element hidden
			Element.hide(in_fragment);
			matched = FragmentFilter.filter_text(in_fragment.getInnerText(),filter_pattern);
			
			//	debug_log("/"+in_filter.filter_pattern+"/ --  text:"+in_fragment.getInnerText()+"-- match"+matched);
			}
		else 
			{
			f_target = in_fragment.getElementsByClassName(target_name)[0];
			if (f_target)
				{
				matched = FragmentFilter.filter_text(f_target.getInnerText(),filter_pattern);
				}	
			//	debug_log("Error - Couldn't find class-"+in_filter.filter_target);				
			}

	//	debug_log("/"+in_filter.filter_pattern+"/ --  match:"+matched);
		
		return matched;
	},
	
	
	// Filter the given text with the pattern
	//
	// Returns boolean result indicating match or not
	filter_text: function(in_text,in_pattern) {
		if (in_pattern == "") 
			return true;
		
		var	no_match	= -1;	
		var reg_exp		= new RegExp(in_pattern, "i");// i means matches case insensitive
		return (in_text.search(reg_exp) != no_match);
	},


	
	// Returns regular expression matching dates of the form 2007-08-16 standard ISO date
	//
	// Matches month and year and days for last in_days days
	// Note days must be an integer
	match_last_days: function(in_days) {
		var now = new Date();
		
		
		years 	= new Array();
		months 	= new Array();
		days 	= new Array();
		
		var days_into_past = in_days;
				
		// Walk through the last x days, getting the date and comparing year, month and day with now
		// If it's different, add it to our list of years, months or days	
	
			in_days.times(function() { 
				var old_date = new Date(now.getTime() - (days_into_past * 86400000));// go back day_count days

				if (now.getYear() != old_date.getYear())
					years.push(old_date.getFullYear());

				if (now.getMonth() != old_date.getMonth())
					months.push((old_date.getMonth() + 1).toPaddedString(2));

				days.push( old_date.getDate().toPaddedString(2) );

				// note we don't need to bother with hours here
				
				days_into_past --;
			});		

		// now add today
		years.push(  now.getFullYear() );
		months.push( (now.getMonth() + 1).toPaddedString(2) );
		days.push(  now.getDate().toPaddedString(2) );
		
		var options 	= { years_list		: years.join("|"),
						    months_list		: months.join("|"),
							days_list		: days.join("|")};
							
		return new Template('(#{years_list})-(#{months_list})-(#{days_list})').evaluate(options);
		
	},
	
	
	// Returns regular expression matching dates of the form 2007-08-16
	//
	// Only matches month and year
	match_date_this_month: function() {
		return FragmentFilter.date_string(new Date(),true);
	},
	
	
	// Returns regular expression matching dates of the form 2007-08-16
	//
	// Matches full date so only matches today's date
	match_date_today: function() {
		return FragmentFilter.date_string(new Date(),false);
	},

	
	// Returns a formatted string of the form 2007-08-16
	//
	// pass in omit_day true to miss of day
	date_string: function(in_date,omit_day) {
		var options 	= { /* call insane JS date functions */
							year	: in_date.getFullYear(),
						   	month	: (in_date.getMonth() + 1).toPaddedString(2),
							day		: in_date.getDate().toPaddedString(2)
							};
			
		if (omit_day) 	// return a date string of the form 2007-08
			return new Template('#{year}-#{month}').evaluate(options);			
		else 			// return a date string of the form 2007-08-16
			return new Template('#{year}-#{month}-#{day}').evaluate(options);	
		
	}
	
	
};






///////////////////////////////////////Filter Object  

// Filter constructor
//
// Holds data on each filtering control (e.g. select or input element)
function Filter(in_id,in_pattern,in_target) {
	this.filter_id		= in_id;		// the id of the filter control we hold info for
	this.filter_pattern = in_pattern; 	// the pattern to match
	this.filter_target  = in_target; 	// the class of target within the html fragment (ie date or whole fragment text etc)

}


// Set the target of this filter
//
// The target is the class name of the element within the fragment which should be matched against
// If target is set to 'text' then the whole fragment is searched
Filter.prototype.set_target = function (in_target) {
	this.filter_target = in_target.toLowerCase();
}


// Set the filter pattern
//
// If target is date 'Today' 'Month' or a number of days ago are valid values
// If target is price value should be of form 1 or 2-3 (not a real price, just an indicator)
// ALSO the values in document MUST be padded like so 02
// Otherwise search is on plain text (either in all text or text of classnamed div)
// We should probably instead support setting type of search (e.g. numeric, text)
Filter.prototype.set_pattern = function (in_pattern) {
	
		switch(this.filter_target) {
			case 'none':
				this.filter_pattern = "";	
			break;
			
			case 'numeric-rating':
			case 'numeric-price':
			case 'price':
				// interpret as a numeric range of form 03-12
				// or as a simple number
				numbers = in_pattern.split("-");
				if (numbers && (numbers.length > 1) )
					{
					pattern_array = new Array();
					lowest = parseInt(numbers[0]);
					highest =  parseInt(numbers[1]);
					while (lowest <= highest)
						{
						pattern_array.push(lowest.toPaddedString(2));
						lowest ++;	
						}
					// we want to end up with a regexp like this - 09|10|11|12|13
					this.filter_pattern = pattern_array.join("|");
					}
				else
					{
					this.filter_pattern = parseInt(in_pattern).toPaddedString(2);	
					}
					
					
			//	debug_log("filter-"+this.filter_pattern+" pattern-"+in_pattern);		
			break;

			case 'iso-date':
			case 'date':
				var no_of_days = parseInt(in_pattern);
			
				if (in_pattern == "Today")
					{
					this.filter_pattern  = FragmentFilter.match_date_today();				
					}
				else if (in_pattern == "Month")
					{
					this.filter_pattern  = FragmentFilter.match_date_this_month();		
					}
				else if ( no_of_days > 0)
					{
					this.filter_pattern = FragmentFilter.match_last_days(no_of_days);	
					}
				else 
					{
					this.filter_pattern = in_pattern;	
					}
			break;
			
			// it's just a text search, so interpret pattern literally
			// Note we escape regexp's to avoid problems with special characters
			case 'text':
			default:
				this.filter_pattern = RegExp.escape(in_pattern);
			break;
			
			
		}
	
		
}



Event.observe(window,'load',FragmentFilter.initialize,false);

