/**
 * JavaScript support library for client-side scripted functionality based on PHP
 * component classes.
 * 
 * @author Alexander Vinkeles Melchers
 * @copyright Alexander Vinkeles Melchers, 2011
 */

/**
 * Changes the mouse over state of the specified form button.
 * 
 * @param the image/button for which to change the state.
 * @param the name of the state to set the button to.
 */
function formButtonState(button, state)
{
	button.className = button.className.replace(/_(on|off)/, "_" + state);
}

/**
 * Opens the preview window associated with the component whose
 * ID has been specified.
 * 
 * @param the HTML id of a component for which to display the
 *        preview pane
 * @return success state
 */
function showPreview(component_id)
{
	var button, preview_window;
	var result = false;
	
	if (document.getElementById)
	{
		button = document.getElementById(component_id + '$PvwBtn');
		preview_window = document.getElementById(component_id + '$Preview');
	}
	else if (document.all)
	{
		button = document.all[component_id + '$PvwBtn'];
		preview_window = document.all[component_id + '$Preview'];
	}
	
	if (button && preview_window)
	{
		// Change the button's appearance to indicate the preview is open.
		button.className = button.className.replace(/_off/i, '_on');
		
		// Make the preview window visible.
		preview_window.style.display = '';
		
		// Position the preview window directly underneath the button.
		pos = clientToScreen(button);
		preview_window.style.top = (pos.top + button.offsetHeight) + 'px';
		preview_window.style.left = pos.left + 'px';
		
		// Set an indicator, so that we know the preview window is open.
		preview_window.open = true;
		
		result = true;
	}
	
	return result;
}

/**
 * Closes the preview window associated with the component whose
 * ID has been specified.
 * 
 * @param the HTML id of a component for which to display the
 *        preview pane
 * @return success state
 */
function closePreview(component_id)
{
	var button, preview_window;
	var result = false;
	
	if (document.getElementById)
	{
		button = document.getElementById(component_id + '$PvwBtn');
		preview_window = document.getElementById(component_id + '$Preview');
	}
	else if (document.all)
	{
		button = document.all[component_id + '$PvwBtn'];
		preview_window = document.all[component_id + '$Preview'];
	}
	
	if (button && preview_window)
	{
		// Change the button's appearance to indicate the preview is closed.
		button.className = button.className.replace(/_on/i, '_off');
		
		// Make the preview window invisible.
		preview_window.style.display = 'none';
		
		// Indicate that the preview window is closed.
		preview_window.open = false;
		
		result = true;
	}
	
	return result;
}

/**
 * Initiates a timed action that will close the preview window if the user
 * does not either 1) re-open the window by moving over the preview button,
 * or 2) by moving the mouse cursor back into the preview window.
 * 
 * @param the HTML id of a component for which to display the
 *        preview pane
 * @return true
 */
function closePreviewOnTimer(component_id)
{
	var preview_window;
	
	// Define the function that closes the preview window.
	var timed_close = function()
	{
		var pvwwnd;
		
		if (document.getElementById)
		{
			pvwwnd = document.getElementById(component_id + '$Preview');
		}
		else if (document.all)
		{
			pvwwnd = document.all[component_id + '$Preview'];
		}
		
		if (pvwwnd&&!pvwwnd.open)
		{
			closePreview(component_id);
		}
	}
	
	if (document.getElementById)
	{
		preview_window = document.getElementById(component_id + '$Preview');
	}
	else if (document.all)
	{
		preview_window = document.all[component_id + '$Preview'];
	}
	
	if (preview_window)
	{
		preview_window.open = false;
		setTimeout(timed_close, 1000);
	}
	
	return true;
}

/**
 * Sets the page to be displayed in the URL preview window.
 * 
 * @param the HTML id of a component in which preview pane the
 *        browser window is located
 * @return success state 
 */
function createUrlPreview(component_id)
{
	var url_text, browser_panel;
	var result = false;
	
	if (document.getElementById)
	{
		url_text = document.getElementById(component_id);
		browser_panel = document.getElementById(component_id + '$Panel');
	}
	else if (document.all)
	{
		url_text = document.all[component_id];
		browser_panel = document.all[component_id + '$Panel'];
	}
	
	if (url_text && browser_panel)
	{
		if (url_text.value.match(/^\s*$/))
		{
			browser_panel.src = 'about:blank';
		}
		else
		{
			browser_panel.src = url_text.value;
		}
		url_text.old_value = url_text.value;
		
		result = true;
	}
	
	return result;
}

/**
 * Updates the page to be displayed in the URL preview window.
 * 
 * @param the HTML id of a component in which preview pane the
 *        browser window is located
 * @return success state 
 */
function updateUrlPreview(component_id)
{
	var preview_window;
	var result = false;
	
	if (document.getElementById)
	{
		preview_window = document.getElementById(component_id + '$Preview');
	}
	else if (document.all)
	{
		preview_window = document.all[component_id + '$Preview'];
	}
	
	if (preview_window && (preview_window.style.display == ''))
	{
		result = createUrlPreview(component_id);
	}
	
	return result;
}

/**
 * Opens the calendar view for a date picker component, allowing
 * the user to select a date using an intuitive interface.
 * 
 * @param the HTML id of a component that represents the date
 *        picker component
 * @return success state
 */
function showCalendar(component_id)
{
	var date_field, calendar_window, calendar_view;
	var min_year, max_year, date_format;
	var result = false;
	
	if (document.getElementById)
	{
		date_field = document.getElementById(component_id);
		calendar_window = document.getElementById(component_id + '$Calendar');
		calendar_view = document.getElementById(component_id + '$View');
		min_year = document.getElementById(component_id + '$MinYear');
		max_year = document.getElementById(component_id + '$MaxYear');
		date_format = document.getElementById(component_id + '$DateFormat');
	}
	else if (document.all)
	{
		date_field = document.all[component_id];
		calendar_window = document.all[component_id + '$Calendar'];
		calendar_view = document.all[component_id + '$View'];
		min_year = document.all[component_id + '$MinYear'];
		max_year = document.all[component_id + '$MaxYear'];
		date_format = document.all[component_id + '$DateFormat'];
	}
	
	if (date_field && calendar_window && calendar_view)
	{
		// Make the calendar window visible.
		calendar_window.style.display = '';
		
		// Position the preview window directly underneath the button.
		pos = clientToScreen(date_field);
		calendar_window.style.top = (pos.top + date_field.offsetHeight) + 'px';
		calendar_window.style.left = pos.left + 'px';
		
		// Prepare a number of parameters for the calendar viewer.
		calendar_view.minimum_year = (min_year)?(parseInt(min_year.value)):(1970);
		calendar_view.maximum_year = (max_year)?(parseInt(max_year.value)):(2030);
		calendar_view.date_format = (date_format)?(date_format.value):('Y-N-d');
		
		// Set which date the calendar viewer will show as selected (i.e. corresponds to the date
		// in the textbox).
		calendar_view.selected_date = parseDate(date_field.value);
		if (typeof(calendar_view.selected_date) != 'object')
		{
			calendar_view.selected_date = new Date();
		}
		
		// Now render a calendar view.
		drawCalendar(component_id, calendar_view.selected_date.getFullYear(), calendar_view.selected_date.getMonth() + 1);
		
		result = true;
	}
	
	return result;
}

/**
 * Outputs the view of a calendar, stating the days of the month and
 * allowing the user to interactively pick a new date for the date
 * picker component.
 * 
 * @param the HTML id of a component that represents the date
 *        picker component
 * @param the four-digit year of the date for which to draw the
 *        calendar
 * @param the month of the date for which to draw the calendar (0-11)
 * @return success state
 */
function drawCalendar(cal_com_id, year, month)
{
	var view, calendar_window;
	var dd_months, dd_years;
	var month_dec, month_inc;
	var cal_date = new Date(year, month - 1, 1, 0, 0, 0, 0);
	var tr, td;
	
	if (document.getElementById)
	{
		view = document.getElementById(cal_com_id + '$View');
		calendar_window = document.getElementById(cal_com_id + '$Calendar');
		dd_months =  document.getElementById(cal_com_id + '$Months');
		dd_years =  document.getElementById(cal_com_id + '$Years');
		month_dec = document.getElementById(cal_com_id + '$MonthDec');
		month_inc = document.getElementById(cal_com_id + '$MonthInc');
	}
	else if (document.all)
	{
		view = document.all[cal_com_id + '$View'];
		calendar_window = document.all[cal_com_id + '$Calendar'];
		dd_months =  document.all[cal_com_id + '$Months'];
		dd_years =  document.all[cal_com_id + '$Years'];
		month_dec = document.all[cal_com_id + '$MonthDec'];
		month_inc = document.all[cal_com_id + '$MonthInc'];
	}
	
	if (view && view.insertRow)
	{
		// Copy the date range rendered within this view for future redrawing.
		view.view_date = new Date(cal_date.getFullYear(), cal_date.getMonth(), 1, 0, 0, 0, 0);
		
		// First clear the existing view, so that we may start outputting a new one.
		for (i = view.rows.length - 1; i > 1; i--)
		{
			view.deleteRow(i);
		}
		
		// Output a number of days from the previous month leading up to the first
		// date of this month.
		if (cal_date.getDay() != 1)
		{
			cal_date.setDate(cal_date.getDate()- ((cal_date.getDay() + 6) % 7));
			
			// Output the first seven days of the calendar.
			tr = view.insertRow(view.rows.length);
			for (i = 0; i < 7; i++)
			{
				td = tr.insertCell(i);
				td.innerHTML = cal_date.getDate();
				if (cal_date.getMonth() + 1 == month)
				{
					if ((cal_date.getFullYear() == view.selected_date.getFullYear()) &&
						(cal_date.getMonth() == view.selected_date.getMonth()) &&
						(cal_date.getDate() == view.selected_date.getDate()))
					{
						td.className = view.className.replace(/_view$/i, '_selected');
					}
					else
					{
						td.className = view.className.replace(/_view$/i, '_off');
						td.onmouseover = function(){formButtonState(this, 'on');};
						td.onmouseout = function(){formButtonState(this, 'off');};
						td.date = new Date(cal_date.getFullYear(), cal_date.getMonth(), cal_date.getDate(), 0, 0, 0, 0);
						td.onclick = function()
						{
							var dpf;
							var dpv = this.offsetParent;
							
							if (document.getElementById)
							{
								dpf = document.getElementById(dpv.id.replace(/\$View$/i, ''));
							}
							else if (document.all)
							{
								dpf = document.all[dpv.id.replace(/\$View$/i, '')];
							}
							
							if (dpf)
							{
								dpf.value = formatDate(this.date, dpv.date_format);
								dpv.selected_date = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), 0, 0, 0, 0);
								redrawCalendar(dpf.id);
							}
							
							refocus(dpf.id);
						};
					}
				}
				else
				{
					td.className = view.className.replace(/_view$/i, '_disabled');
				}
				cal_date.setDate(cal_date.getDate() + 1);
			}
		}
		
		// Output the (rest of the) calendar, including additional days at the end of the
		// month to fill out the view.
		while (cal_date.getMonth() + 1 == month)
		{
			if (!tr||(tr.cells.length >= 7))
			{
				tr = view.insertRow(view.rows.length);
			}
			
			td = tr.insertCell(tr.cells.length);
			td.innerHTML = cal_date.getDate();
			if ((cal_date.getFullYear() == view.selected_date.getFullYear()) &&
					(cal_date.getMonth() == view.selected_date.getMonth()) &&
					(cal_date.getDate() == view.selected_date.getDate()))
			{
				td.className = view.className.replace(/_view$/i, '_selected');				
			}
			else
			{
				td.className = view.className.replace(/_view$/i, '_off');
				td.onmouseover = function(){formButtonState(this, 'on');};
				td.onmouseout = function(){formButtonState(this, 'off');};
				td.date = new Date(cal_date.getFullYear(), cal_date.getMonth(), cal_date.getDate(), 0, 0, 0, 0);
				td.onclick = function()
				{
					var dpf;
					var dpv = this.offsetParent;
					
					if (document.getElementById)
					{
						dpf = document.getElementById(dpv.id.replace(/\$View$/i, ''));
					}
					else if (document.all)
					{
						dpf = document.all[dpv.id.replace(/\$View$/i, '')];
					}
					
					if (dpf)
					{
						dpf.value = formatDate(this.date, dpv.date_format);
						dpv.selected_date = new Date(this.date.getFullYear(), this.date.getMonth(), this.date.getDate(), 0, 0, 0, 0);
						redrawCalendar(dpf.id);
					}
					
					refocus(dpf.id);
				};
			}	
			
			cal_date.setDate(cal_date.getDate() + 1);
		}
		while (tr.cells.length < 7)
		{
			td = tr.insertCell(tr.cells.length);
			td.innerHTML = cal_date.getDate();
			td.className = view.className.replace(/_view$/i, '_disabled');
			
			cal_date.setDate(cal_date.getDate() + 1);
		}
		
		// Adjust the month and year selection drop down menus to reflect the current month and year.
		if (dd_months && (dd_months.options.length > view.view_date.getMonth()))
		{
			dd_months.options[view.view_date.getMonth()].selected = true;
		}
		if (dd_years)
		{
			for (i = 0; i < dd_years.options.length; i++)
			{
				dd_years.options[i].selected = dd_years.options[i].value == view.view_date.getFullYear();
			}
		}
		
		// Adjust the month decrease and increase buttons to switch to the right month
		// when pressed.
		if ((view.view_date.getFullYear() > view.minimum_year) || (view.view_date.getMonth() > 0))
		{
			cal_date = new Date(view.view_date.getFullYear(), view.view_date.getMonth() - 1, 1, 0, 0, 0, 0);
			month_dec.component_id = cal_com_id;
			month_dec.to_month = cal_date.getMonth();
			month_dec.of_year = cal_date.getFullYear();
			month_dec.onclick = function(){drawCalendar(this.component_id, this.of_year, this.to_month + 1);refocus(this.component_id);};
			month_dec.innerHTML = '&lt;&lt;';
			month_dec.style.cursor = 'pointer';
		}
		else
		{
			month_dec.innerHTML = '&nbsp;';
			month_dec.onclick = function(){return false;};
			month_dec.style.cursor = 'default';
		}
		if ((view.view_date.getFullYear() < view.maximum_year) || (view.view_date.getMonth() < 11))
		{
			cal_date = new Date(view.view_date.getFullYear(), view.view_date.getMonth() + 1, 1, 0, 0, 0, 0);
			month_inc.component_id = cal_com_id;
			month_inc.to_month = cal_date.getMonth();
			month_inc.of_year = cal_date.getFullYear();
			month_inc.onclick = function(){drawCalendar(this.component_id, this.of_year, this.to_month + 1);refocus(this.component_id);};
			month_inc.innerHTML = '&gt;&gt;';
			month_inc.style.cursor = 'pointer';
		}
		else
		{
			month_inc.innerHTML = '&nbsp;';
			month_inc.onclick = function(){return false;};
			month_inc.style.cursor = 'default';
		}
		
		// Adjust the height of the containing window to match that of our date
		// picker view.
		if (calendar_window)
		{
			calendar_window.style.height = view.clientHeight + 'px';
		}
				
		result = true;
	}
	
	return result;
}

/**
 * Redraws a previously drawn calendar view.
 * 
 * @param the HTML id of a component that represents the date
 *        picker component
 * @return success state
 */
function redrawCalendar(component_id)
{
	var calendar_view;
	var result = false;
	
	if (document.getElementById)
	{
		calendar_view = document.getElementById(component_id + '$View');
	}
	else if (document.all)
	{
		calendar_view = document.all[component_id + '$View'];
	}
	
	if (calendar_view)
	{
		result = drawCalendar(component_id, calendar_view.view_date.getFullYear(), calendar_view.view_date.getMonth() + 1);
	}
	
	return result;
}

/**
 * Closes the calendar window associated with a date picker
 * component again.
 * 
 * @param the HTML id of a component that represents the date
 *        picker component
 * @return success state
 */
function closeCalendar(component_id)
{
	var calendar_window;
	var result = false;
	
	if (document.getElementById)
	{
		calendar_window = document.getElementById(component_id + '$Calendar');
	}
	else if (document.all)
	{
		calendar_window = document.all[component_id + '$Calendar'];
	}
	
	if (calendar_window)
	{
		if (!calendar_window.block_close)
		{
			calendar_window.style.display = 'none';
		}
		
		result = true;
	}
		
	return result;
}

/**
 * Formats the date contained within the specified date object into
 * a string representation of that date in accordance with the
 * supplied format. The format may be constructed of below keys:
 * 
 * y - two digit year number 
 * Y - four digit year number
 * n - numeric month, without leading zero
 * N - numeric month, with leading zero
 * m - short textual month name
 * M - long textual month name
 * d - day of the month number, without leading zero
 * D - day of the month number, with leading zero
 * 
 * @param the Date object to be formatted into a string
 * @param a string specifying the format according to which the
 *        supplied Date object is to be formatted
 * @return formatted date string
 */
function formatDate(date_object, format)
{
	var result = format;
	
	// Prepare mappings for month numbers to month names.
	var map_month_short = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
	var map_month_long = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
	
	// First replace all non-text date elements.
	result = result.replace(/y/g, date_object.getFullYear().toString().replace(/^.*?(\d{2})$/, '$1'));
	result = result.replace(/Y/g, date_object.getFullYear());
	result = result.replace(/n/g, date_object.getMonth() + 1);
	result = result.replace(/N/g, ('0' + (date_object.getMonth() + 1)).replace(/^.*?(\d{2})$/, '$1'));
	result = result.replace(/d/g, date_object.getDate());
	result = result.replace(/D/g, ('0' + date_object.getDate()).replace(/^.*?(\d{2})$/, '$1'));
	
	// In order to substitute textual representations, we first need to translate their keys to
	// non-alphanumeric signs, which we can then substitute as above.
	result = result.replace(/m/g, String.fromCharCode(1));
	result = result.replace(/M/g, String.fromCharCode(2));
	result = result.replace(/\x01/g, map_month_short[date_object.getMonth()]);
	result = result.replace(/\x02/g, map_month_long[date_object.getMonth()]);
	
	return result;
}

/**
 * Takes the string representation of a date and translates it into a
 * Date object. Valid formats for dates are: "year month day", "month
 * day year" and "day month year".
 * 
 * @param the string value to convert into a Date object
 * @result Date object if the string could successfully be converted,
 *         or false otherwise
 */
function parseDate(date_string)
{
	var fields;
	var year, month, day;
	var interpretation = 0;
	var result = false;
	var month_list = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
	
	// First translate all possible field separators ([.- /]) into a single one for
	// easier parsing.
	date_string = date_string.replace(/[-.\/]|\s+|(,\s*)/g, String.fromCharCode(2));
	
	// Next, split the fields.
	fields = date_string.split(String.fromCharCode(2));
	
	// Only if we've got exactly three fields...
	if (fields.length == 3)
	{
		// If the first field is a textual representation of a month, assume our date
		// format to be "month day year".
		if (fields[0].match(/^(jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|june?|july?|aug(ust)?|sep(tember)?|oct(ober)?|nov(ember)?|dec(ember)?)$/))
		{
			// Make the month name easier to parse.
			fields[0] = fields[0].substr(0, 3).toLowerCase();
			
			// Now, according to our format, first of all the second and third fields should be numerical,
			// but the second field should be <= 31 as well.
			if (fields[1].match(/^\d+$/) && fields[2].match(/^\d+$/))
			{
				fields[1] = parseInt(fields[1].replace(/^0*([^0]\d?|0)$/, '$1'));
				if (fields[1] <= 31)
				{
					for (i = 0; i < month_list.length; i++)
					{
						if (fields[0] == month_list[i])
						{
							month = i;
							break;
						}
					}
					day = fields[1];
					year = parseInt(fields[2].replace(/^0*([^0]\d?|0)$/, '$1'));
					
					// Now that we have all components to make a date, create the Date object.
					result = new Date(year, month, day, 0, 0, 0, 0);
				}
			}
		}
		// Either "day month year" or "year month day"
		else if (fields[1].match(/^(jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|june?|july?|aug(ust)?|sep(tember)?|oct(ober)?|nov(ember)?|dec(ember)?)$/))
		{
			// Make the month name easier to parse.
			fields[1] = fields[1].substr(0, 3).toLowerCase();
			
			// Now, according to our format, first of all the second and third fields should be numerical,
			// but either the first or third field should be <= 31 as well.
			if (fields[0].match(/^\d+$/) && fields[2].match(/^\d+$/))
			{
				fields[0] = parseInt(fields[0].replace(/^0*([^0]\d?|0)$/, '$1'));
				fields[2] = parseInt(fields[2].replace(/^0*([^0]\d?|0)$/, '$1'));
				
				if ((fields[0] <= 31) || (fields[2] <= 31))
				{
					for (i = 0; i < month_list.length; i++)
					{
						if (fields[1] == month_list[i])
						{
							month = i;
							break;
						}
					}
					if (fields[0] > 31)
					{
						year = fields[0];
						day = fields[2];
					}
					else if (fields[2] > 31)
					{
						year = fields[2];
						day = fields[0];
					}
					// Give preference to the "year month day" format...
					else
					{
						year = fields[0];
						day = fields[2];
					}
					
					// Now that we have all components to make a date, create the Date object.
					result = new Date(year, month, day, 0, 0, 0, 0);
				}
			}
		}
		// All fields are expected to be numeric in all other accepted date formats.
		else if (fields[0].match(/^\d+$/) && fields[1].match(/^\d+$/) && fields[2].match(/^\d+$/))
		{
			fields[0] = parseInt(fields[0].replace(/^0*([^0]\d?|0)$/, '$1'));
			fields[1] = parseInt(fields[1].replace(/^0*([^0]\d?|0)$/, '$1'));
			fields[2] = parseInt(fields[2].replace(/^0*([^0]\d?|0)$/, '$1'));
			
			// Determine an interpretation for the date:
			// 0 = year, month, day
			// 1 = day, month, year
			// 2 = month, day, year
			if ((fields[0] <= 31) && (fields[1] <= 12))
			{
				interpretation = 1;
			}
			else if ((fields[0] <= 12) && (fields[1] <= 31))
			{
				intepretation = 2;
			}
			
			// Now perform the actual conversion.
			switch (interpretation)
			{
				case 1:
					result = new Date(fields[2], fields[1] - 1, fields[0], 0, 0, 0, 0);
					break;
				case 2:
					result = new Date(fields[2], fields[0] - 1, fields[1], 0, 0, 0, 0);
					break;
				default:
					result = new Date(fields[0], fields[1] - 1, fields[2], 0, 0, 0, 0);
					break;
			}
		}
	}
	
	return result;
}

/**
 * Executes the supplied text editor macro on specified multiline text
 * component.
 * 
 * @param the multiline text component on which to execute the macro
 * @param the name of the macro, in case any information about the
 *        latter needs to be displayed
 * @param either a scalar (simple insert) or array (open and close tag)
 *        macro to insert into the text field
 * @return success state
 */
function te_exec_macro(component_id, macro_name, macro)
{
	var result = false;
	var text_editor;
	
	if (document.getElementById)
	{
		text_editor = document.getElementById(component_id);
	}
	else if (document.all)
	{
		text_editor = document.all[component_id];
	}
	
	if (text_editor && !text_editor.disabled)
	{
		if (!text_editor.caretPos)
		{
			te_store_caret(text_editor);
		}
		if (text_editor.caretPos)
		{
			if ((typeof(macro)=='object') && (macro.constructor.toString().indexOf('Array') != -1))
			{
				// If there's a percentage sign in the macro we need to display the parameter dialog
				// in order to get a value which we can substitute for the percentage sign.
				if (macro[0].match(/(^|\W)%(\W|$)/))
				{
					return te_param_dialog(text_editor, macro_name, macro);
				}
				else
				{
					text_editor.caretPos.text = macro[0] + text_editor.caretPos.text + macro[1];
					te_finalize_macro(text_editor);
					result = true;
				}
			}
			else
			{
				// If there's a percentage sign in the macro we need to display the parameter dialog
				// in order to get a value which we can substitute for the percentage sign.
				if (macro.match(/(^|\W)%(\W|$)/))
				{
					return te_param_dialog(text_editor, macro_name, macro);
				}
				else
				{
					text_editor.caretPos.text = macro;
					te_finalize_macro(text_editor);
					result = true;
				}
			}
		}
	}
	
	return result;
}

/**
 * Executes an extended macro. That is, it performs the same function
 * as the te_exec_macro() function, except that replaces any percentage
 * sign in the macro by a parameter that has been previously specified.
 * The function cannot be called separately, however, as it does not
 * test any preconditions, instead building on functionality supplied by
 * the te_exec_macro() and te_param_dialog() functions.
 * 
 * @param object reference to the text editor component for which to
 *        execute the macro
 * @param object refernece to the text field containing the parameter
 *        as its value property
 * @param the extended macro to execute
 * @return true
 */
function te_exec_param_macro(editor, parameter, macro)
{
	var result = false;
	
	if (!parameter.value.match(/^\s*$/))
	{
		// Insert the macro.
		if (typeof(macro)=='object')
		{
			editor.caretPos.text = macro[0].replace(/(^|\W)%(\W|$)/, '$1' + parameter.value + '$2') + editor.caretPos.text + macro[1];
		}
		else
		{
			editor.caretPos.text = macro.replace(/(^|\W)%(\W|$)/, '$1' + parameter.value + '$2');
		}
		
		// Once the macro has been inserted, the text editor can be enabled again.
		te_finalize_macro(editor);
	}
	// No value was specified, but we should still re-enabled the text editor.
	else
	{
		editor.disabled = false;
	}
	
	return result;
}

/**
 * Places the text editor's caret at the end of a recently inserted
 * text, which is necessary to prevent the user from overwriting recent
 * inserts. Also places a request to update the preview.
 * 
 * @param text editor component to re-establish the caret for
 * @returns true
 */
function te_finalize_macro(component)
{
	component.disabled = false;
	component.focus();
	component.caretPos.setEndPoint("EndToStart", component.caretPos);
	component.caretPos.select();
	
	te_update_preview(component.id);
	
	return true;
}

/**
 * For extended macro's (those using a parameter), prompts the user
 * to specify a text value. The text input dialog is displayed on
 * top of the text editor component, and the component itself is
 * temporarily disabled, until the user presses "Accept".
 * 
 * @param an object reference to the text editor for which to show
 *        the parameter dialog
 * @param name of the macro, used for display when asking the user
 *        for input
 * @param the macro itself: either a string or array of strings
 * @returns success state
 */
function te_param_dialog(text_editor, macro_name, macro)
{
	var result = false;
	var text_editor_pane;
	var param_dialog, param_label, param_value, param_button;
	var pos;
	
	if (document.getElementById)
	{
		text_editor_pane = document.getElementById(text_editor.id + '$Pane');
		param_dialog = document.getElementById(text_editor.id + '$ParamDialog');
		param_label = document.getElementById(text_editor.id + '$ParamLabel');
		param_value = document.getElementById(text_editor.id + '$ParamValue');
		param_button = document.getElementById(text_editor.id + '$ParamButton');
	}
	else if (document.all)
	{
		text_editor_pane = document.all[text_editor.id + '$Pane'];
		param_dialog = document.all[text_editor.id + '$ParamDialog'];
		param_label = document.all[text_editor.id + '$ParamLabel'];
		param_value = document.all[text_editor.id + '$ParamValue'];
		param_button = document.all[text_editor.id + '$ParamButton'];
	}
	
	if (param_dialog && param_label && param_value && param_button && (param_dialog.style.display != '') &&
			text_editor_pane)
	{
		// We don't want the user editing the text while the dialog is still open.
		text_editor.disabled = true;
		
		// Change the label that is displayed in the dialog.
		param_label.innerHTML = 'Please specify a ' + macro_name.toLowerCase() + ' in the textbox below:';
		
		// Reset the parameter value.
		param_value.value = '';
		
		// Assign an action to the accept button.
		param_button.editor = text_editor;
		param_button.parameter = param_value;
		param_button.macro = macro;
		param_button.dialog = param_dialog;
		param_button.onclick = function() {
			te_exec_param_macro(this.editor, this.parameter, this.macro);
			this.dialog.style.display = 'none';
			return true;
		}
		
		// Show the dialog.
		param_dialog.style.display = '';
		param_value.focus();
		
		// Position the dialog.
		pos = clientToScreen(text_editor_pane);
		param_dialog.style.top = ((pos.top + Math.round(text_editor_pane.offsetHeight / 2)) - Math.round(param_dialog.offsetHeight / 2)) + 'px';
		param_dialog.style.left = ((pos.left + Math.round(text_editor_pane.offsetWidth / 2)) - Math.round(param_dialog.offsetWidth / 2)) + 'px';
		
		result = true;
	}
	
	return result;
}

/**
 * For the parameter dialog specified text editor component, act as if the
 * "accept"-button was pressed.
 * 
 * @param ID of the component for which to "soft press" the button
 * @return success state
 */
function te_accept_param(component_id)
{
	var result = false;
	var param_button;
	
	if (document.getElementById)
	{
		param_button = document.getElementById(component_id + '$ParamButton');
	}
	else if (document.all)
	{
		param_button = document.all[component_id + '$ParamButton'];
	}
	
	if (param_button && param_button.onclick)
	{
		result = param_button.onclick();
	}
	
	return result;
}

/**
 * Runs a text editor macro for inserting a special character at the
 * current caret position.
 * 
 * @param the ID of the component for which to run the macro
 * @param a list of characters that may be inserted into the text
 * @return success state
 */
function te_insert_symbol(component_id, char_list)
{
	var result = false;
	var list_col_count = Math.ceil(Math.sqrt(char_list.length));
	var text_editor, act_tlbtn;
	var charmap, list_of_chars;
	var tr, td;
	var pos;
	var onclickFunc;
	
	if (document.getElementById)
	{
		text_editor = document.getElementById(component_id);
		act_tlbtn = document.getElementById(component_id + '$Tlbtn_Insert_symbol');
		charmap = document.getElementById(component_id + '$CharMap');
		list_of_chars = document.getElementById(component_id + '$Chars');
	}
	else if (document.all)
	{
		text_editor = document.all[component_id];
		act_tlbtn = document.all[component_id + '$Tlbtn_Insert_symbol'];
		charmap = document.all[component_id + '$CharMap'];
		list_of_chars = document.all[component_id + '$Chars'];
	}
	
	if (text_editor && !text_editor.disabled && act_tlbtn && charmap && list_of_chars && list_of_chars.insertRow)
	{
		if (charmap.style.display == 'none')
		{
			// Make sure we (can get or) have a point where to insert a symbol in the text.
			if (!text_editor.caretPos)
			{
				te_store_caret(text_editor);
			}
			
			// If we have an insertion point...
			if (text_editor.caretPos)
			{
				// Clear the list of characters so that we can add a new one.
				while (list_of_chars.rows.length > 0)
				{
					list_of_chars.deleteRow(0);
				}
				
				// Now add the list of characters to the character map.
				for ($i = 0; $i < char_list.length; $i++)
				{
					if ($i % list_col_count == 0)
					{
						tr = list_of_chars.insertRow(list_of_chars.rows.length);
					}
					td = tr.insertCell($i % list_col_count);
					td.innerHTML = char_list.substr($i, 1);
					td.className = text_editor.className + '_charmap_char_off';
					td.editor = text_editor;
					td.charmap = charmap;
					td.onmouseover = function(){formButtonState(this, 'on');};
					td.onmouseout = function(){formButtonState(this, 'off');};
					td.onclick = function() {
						if (this.editor.caretPos && this.editor.caretPos.parentElement() &&
						    (this.editor.caretPos.parentElement()==this.editor))
						{
							this.editor.focus();
							this.editor.caretPos.text = this.innerHTML;
							te_store_caret(this.editor);
							te_update_preview(this.editor.id);
							this.charmap.style.display = 'none';
						}
					}
				}
				// If our table doesn't add up in cell count, add a new cell to span the missing ones.
				if (char_list.length % list_col_count > 0)
				{
					td = tr.insertCell(tr.cells.length);
					td.innerHTML = '';
					td.colSpan = list_col_count - (char_list.length % list_col_count);
				}
				
				// Position the character map on the screen.
				pos = clientToScreen(act_tlbtn);
				charmap.style.top = (pos.top + act_tlbtn.offsetHeight + 1) + 'px';
				charmap.style.left = pos.left + 'px';
				
				// Show the character map.
				charmap.style.display = '';
				
				// We want the menu te close automatically when the user click
				// somewhere outside of the defined area, which we achieve by creating
				// a hook for the document.onclick event handler.
				if (document.onclick)
				{
					if (typeof(document.onclickHandlers)=='undefined')
					{
						document.onclickHandlers = new Array();
					}
					document.onclickHandlers[document.onclickHandlers.length] = document.onclick;
				}
				onclickFunc = "document.onclick = function() {" +
							  "var result = false;" +
							  "var charmap;" +
							  "if (document.getElementById)" +
							  "{" +
							  "charmap = document.getElementById('" + charmap.id + "');" +
							  "}" +
							  "else if (document.all)" +
							  "{" +
							  "charmap = document.all['" + charmap.id + "'];" +
							  "}" +
							  "if (charmap)" +
							  "{" +
							  "charmap.style.display = 'none';" +
							  "result = true;" +
							  "}";
				if (document.onclickHandlers)
				{
					onclickFunc += "if (document.onclickHandlers && (document.onclickHandlers.length >= " + document.onclickHandlers.length + "))" +
								   "{" +
								   "document.onclickHandlers[" + (document.onclickHandlers.length - 1) + "]();" +
								   "document.onclick = document.onclickHandlers.splice(" + (document.onclickHandlers.length - 1) + ", 1);" +
								   "}" +
								   "else" +
								   "{" +
								   "document.onclick = null;" +
								   "}";
				}
				else
				{
					onclickFunc += "document.onclick = null;"; 
				}
				onclickFunc += "return result;" +
							   "};";
				setTimeout('eval("' + onclickFunc + '");', 500);
			}
		}
		else
		{
			charmap.style.display = 'none';
		}
	}
	
	return result;
}

/**
 * Attempts to store the caret position and text selection information
 * from the specified text field as a property of that field. This
 * property can then be used later in order to perform text editing tasks
 * on the parent field.
 * 
 * @param reference to the component object for which to record the caret
 *        position and selection data
 * @return success state
 */
function te_store_caret(component)
{
	var result = false;
	
	// Internet Explorer
	if (component.createTextRange)
	{
		// Make sure the component is focused in order for us to get its selection properly.
		if (document.selection.type.toLowerCase()=='none')
		{
			component.focus();
		}
		component.caretPos = document.selection.createRange().duplicate();
		result = true;
	}
	// FireFox: we need to mimic the IE range behaviour
	else if (typeof(component.selectionStart)!='undefined')
	{
		component.caretPos = {
				_parent: component,
				_selStart: component.selectionStart,
				_selEnd: component.selectionEnd,
				text: '',
				parentElement: function()
				{
					return this._parent;
				},
				select: function()
				{
					this._parent.selectionStart = this._selStart;
					this._parent.selectionEnd = this._selEnd;
					return true;
				},
				move: function(unit, count)
				{
					this.moveStart(unit, count);
					this.moveEnd(unit, count);
					return true;
				},
				moveEnd: function(unit, count)
				{
					var result = false;
					
					// Only deal with movements specified in number of characters.
					if (unit.toLowerCase()=='character')
					{
						// Make sure that the position is within the allowed range.
						if (count > this._parent.value.length)
						{
							count = this._parent.value.length;
						}
						else if (count < 0)
						{
							count = 0;
						}
						
						// Update the internal object's selection end marker.
						this._selEnd = count;
						
						// The end of the selection must be at least equal to the start, but cannot lie before.
						if (this._selEnd > this._selStart)
						{
							this._selStart = this._selEnd;
						}
						
						// Pass the changes on to selection's parent field.
						if (this._selStart != this._parent.selectionStart)
						{
							this._parent.selectionStart = this._selStart;
						}
						if (this._selEnd != this._parent.selectionEnd)
						{
							this._parent.selectionEnd = this._selEnd;
						}
						
						result = true;
					}
					
					return result;
				},
				moveStart: function(unit, count)
				{
					var result = false;
					
					// Only deal with movements specified in number of characters.
					if (unit.toLowerCase()=='character')
					{
						// Make sure that the position is within the allowed range.
						if (count > this._parent.value.length)
						{
							count = this._parent.value.length;
						}
						else if (count < 0)
						{
							count = 0;
						}
						
						// Update the internal object's selection start marker.
						this._selStart = count;
						
						// The end of the selection must be at least equal to the start, but cannot lie before.
						if (this._selStart > this._selEnd)
						{
							this._selEnd = this._selStart;
						}
						
						// Pass the changes on to selection's parent field.
						if (this._selStart != this._parent.selectionStart)
						{
							this._parent.selectionStart = this._selStart;
						}
						if (this._selEnd != this._parent.selectionEnd)
						{
							this._parent.selectionEnd = this._selEnd;
						}
						
						result = true;
					}
					
					return result;
				},
				inRange: function(otherRange)
				{
					var result = false;
					if ((typeof(otherRange)=='object') && (!((typeof(otherRange._selStart)=='undefined') ||
															 (typeof(otherRange._selEnd)=='undefined'))))
					{
						result = (otherRange._selStart>=this._selStart) && (otherRange._selStart<=this._selEnd) &&
								 (otherRange._selEnd>=this._selStart) && (otherRange._selEnd<=this._selEnd);
					}
					return result;
				},
				isEqual: function(otherRange)
				{
					var result = false;
					if ((typeof(otherRange)=='object') && (!((typeof(otherRange._parent)=='undefined') ||
															 (typeof(otherRange._selStart)=='undefined') ||
															 (typeof(otherRange._selEnd)=='undefined'))))
					{
						result = (otherRange._parent==this._parent) && (otherRange._selStart==this._selStart) && (otherRange._selEnd==this._selEnd);
					}
					return result;
				},
				setEndPoint: function(type, range)
				{
					var result = false;
					var source;
					if ((typeof(range)=='object') && (!((typeof(range._selStart)=='undefined') ||
														(typeof(range._selEnd)=='undefined'))) &&
													 (type.match(/^(start|end)to(start|end)$/i)))
					{
						// Determine the source...
						if (type.match(/^start/i))
						{
							source = range._selStart;
						}
						else
						{
							source = range._selEnd;
						}
						// ...and copy it to the destination.
						if (type.match(/start$/i))
						{
							this.moveStart('character', source);
						}
						else
						{
							this.moveEnd('character', source);
						}
						result = true;
					}
					return result;
				}
		};
		component.caretPos.__defineGetter__("text", function(){
			return this._parent.value.substr(this._selStart, this._selEnd - this._selStart);
		});
		component.caretPos.__defineSetter__("text", function(value){
			if (value != this.text)
			{
				this._parent.value = this._parent.value.substr(0, this._selStart) + value + this._parent.value.substr(this._selEnd, this._parent.value.length - this._selEnd);
				this._selEnd = this._selStart + value.length;
				this._selStart = this._selEnd;
				this._parent.selectionEnd = this._selEnd;
				this._parent.selectionStart = this._selStart;
			}
		});
	}
	
	return result;
}

/**
 * For the given component either shows or hides the preview pane,
 * depending on whether the preview pane had been visible prior to
 * the call to this function or not.
 * 
 * @param the text editor component for which to toggle visibility
 *        of the preview pane
 * @returns success state
 */
function te_toggle_preview(component_id)
{
	var result = false;
	var text_editor;
	var te_preview_button, te_preview_pane, te_preview;
	var pos;
	
	if (document.getElementById)
	{
		text_editor = document.getElementById(component_id);
		te_preview_button = document.getElementById(component_id + '$PreviewButton');
		te_preview_pane = document.getElementById(component_id + '$PreviewPane');
		te_preview = document.getElementById(component_id + '$Preview');
	}
	else if (document.all)
	{
		text_editor = document.all[component_id];
		te_preview_button = document.all[component_id + '$PreviewButton'];
		te_preview_pane = document.all[component_id + '$PreviewPane'];
		te_preview = document.all[component_id + '$Preview'];
	}
	
	if (te_preview_button && te_preview_pane && te_preview && text_editor)
	{
		// Toggle the visibility of the preview panel.
		if (te_preview_pane.style.display=='')
		{
			te_preview_pane.style.display = 'none';
		}
		else
		{
			te_preview_pane.style.display = '';
			
			pos = clientToScreen(te_preview_button);
			te_preview_pane.style.top = pos.top + 'px';
			te_preview_pane.style.left = (pos.left + te_preview_button.offsetWidth) + 'px';
			te_update_preview(component_id);
		}
		
		// Update the preview button to indicate either "hide" or "show" preview.
		if (te_preview_pane.style.display=='')
		{
			te_preview_button.className = te_preview_button.className.replace(/clsd/i, 'opnd');
		}
		else
		{
			te_preview_button.className = te_preview_button.className.replace(/opnd/i, 'clsd');
		}
		
		result = true;
	}
	
	return result;
}

/**
 * Performs an update of the specified component's text editor preview.
 * The update will only take place if the preview window is actually
 * visible, and has been placed in a timeout (of about 50 keys a minute),
 * so that showing the updated preview won't take too much system resources.
 * 
 * @param ID of the component for which to update the preview display
 * @return success state
 */
function te_update_preview(component_id)
{
	var result = false;
	var text_editor;
	var preview_pane, preview, preview_url;
	var te_preview_update;
	
	if (document.getElementById)
	{
		text_editor = document.getElementById(component_id);
		preview_pane = document.getElementById(component_id + '$PreviewPane');
		preview = document.getElementById(component_id + '$Preview');
		preview_url = document.getElementById(component_id + '$PreviewUrl');
	}
	else if (document.all)
	{
		text_editor = document.all[component_id];
		preview_pane = document.all[component_id + '$PreviewPane'];
		preview = document.all[component_id + '$Preview'];
		preview_url = document.all[component_id + '$PreviewUrl'];
	}
	
	// Only if the preview pane is visible, the text to be shown is other than that previously displayed,
	// and then placed in a time-out to minimize impact on system resources.
	if (preview_pane && preview && preview_url && (preview_pane.style.display == '') && text_editor)
	{
		if ((typeof(preview.oldContent)=='undefined') || (preview.oldContent != text_editor.value))
		{
			if ((typeof(preview.updatePending)=='undefined') || (!preview.updatePending))
			{
				preview.updatePending = true;
				setTimeout("var preview, text_editor;" +
						   "if (document.getElementById)" +
						   "{" +
						   "preview = document.getElementById('" + preview.id + "');" +
						   "text_editor = document.getElementById('" + text_editor.id + "');" +
						   "}" +
						   "else if (document.all)" +
						   "{" +
						   "preview = document.all['" + preview.id + "'];" +
						   "text_editor = document.all['" + text_editor.id + "'];" +
						   "}" +
						   "if (preview && text_editor)" +
						   "{" +
						   "preview.src = '" + preview_url.value + "?content=' + escape(text_editor.value);" +
						   "preview.updatePending = false;" +
						   "}", 20);
			}
			preview.oldContent = text_editor.value;
		}
		result = true;
	}
	
	return result;
}

/**
 * Renews a form field's focus, without, however, calling it's onFocus
 * event handler.
 * 
 * @param the ID of the component to renew the focus of
 * @return success state
 */
function refocus(component_id)
{
	var focus_field;
	var result = false;
	
	if (document.getElementById)
	{
		focus_field = document.getElementById(component_id);
	}
	else if (document.all)
	{
		focus_field = document.all[component_id];
	}
	
	if (focus_field)
	{
		focus_field.old_onfocus = focus_field.onfocus;
		focus_field.onfocus = function()
		{
			if (focus_field.old_onfocus)
			{
				focus_field.onfocus = focus_field.old_onfocus;
			}
			focus_field.old_onfocus = null;
		}
		focus_field.focus();
		
		result = true;
	}
	
	return result;
}

/**
 * Returns the key code of the key pressed in a key press, key down,
 * or key up event.
 * 
 * @param the event from which to retrieve the key.
 * @return key code or false on failure
 */
function keycode(e)
{
	var kcode = false;
	
	if (window.event)
	{
		kcode = window.event.keyCode;
	}
	else if (e)
	{
		kcode = e.which;
	}
	
	return kcode;
}

/**
 * For onMouseOut events returns the element which the mouse cursor
 * moves on to, based on the event arguments.
 * 
 * @param event whose target element to return
 * @return target element for the event
 */
function eventTarget(e)
{
	var event_target;
	
	if (window.event)
	{
		event_target = window.event.toElement;
	}
	else if (e)
	{
		event_target = e.relatedTarget;
	}
	
	return event_target;
}

/**
 * Translates the relative offset of a HTML control element into its
 * absolute top and left positions within the owner document.
 * 
 * @param an HTML-tag object to translate the coordinates for
 * @return object(top, left)
 */
function clientToScreen(control)
{
	coords = new Object();
	coords.top = 0;
	coords.left = 0;
	
	while ((control) && (control != document))
	{
		coords.top += control.offsetTop;
		coords.left += control.offsetLeft;
		control = control.offsetParent;
	}
	
	return coords;
}

