/*
 * InlineEditor v.4
 *
 * Copyright (c) 2006 Mackley F. Pexton.  All rights reserved.
 *
 * This is free software for individual, educational, and non-profit
 * use provided that this copyright notice appears on all copies.
 * Instructions and source code are available at http://www.acmebase.org/InlineEditor.
 * Optimized version is for sale at https://order.acmebase.com.
 * Commercial web sites are required to have an inexpensive license.
 * Send correspondence and feedback to: mack_pexton[at]acmebase.org.
 */

/******************************************************************************

InlineEditor v.4 -- edit a document element inline on a page

The edit_inline() function changes a document element (e.g. a div tag) into
an edit form allowing the user to change its contents with out refreshing
the page.  Upon saving (or canceling) the changes, the edit form changes
back to the original display.

Usage:
	<button onclick="edit_inline(edit_element,field_name,url,this)">Edit</button>
	<div id="edit_element">some content</div>

Arguments:
	edit_element	-- either an document element or element's id that is to be edited.
	field_name	-- the name of the input field posted (sent to server program)
	url		-- the editor form's action property, the server program saving the edits.
	button 		-- is an optional element or element id of an "Edit" button
			   that triggers the editor.

The edit_inline() program can be used as an onclick handler attached to the
element to be edited or it can be invoked from a button.

A parameter named "return_func" is added to the url identifying the JavaScript
function to call from the server's returned page. The server adds two
arguments to the function before executing it: the first is a true/false
status whether the input was saved or not, and the second is an optional
error messages to display upon return.

Edit form appearance can be customized by assigning your style settings to
InlineEditor.CSS.

Note for Internet Explorer usage: BEWARE
IE removes newlines from the HTML source when it renders a document UNLESS 
the source is contained within <pre></pre> tags. Other browsers are not as
stupid.

******************************************************************************/


function edit_inline(edit_element,field_name,url,button) {
	// Avoid re-invoking editor on same element.
	var e = getRef(edit_element);
	for (editor in InlineEditor.editors) {
		if (e == InlineEditor.editors[editor].edit_element) { return; }
	}

	// Create new inline editor
	var editor = new InlineEditor(edit_element,field_name,url,button)
	editor.edit();
}

//
// InlineEditor control object.
//

/////// Configuration Variables ///////////////////////////

InlineEditor.CSS = '';			// optional styles for edit form (surround with <style> tags).
InlineEditor.HideEditButton = false;	// set to true to hide/show edit button when editor is active
InlineEditor.DisableEditButton = true;	// set to true to disable/enable edit button when editor is active
InlineEditor.ButtonHeight = 32;		// extra height added to display for buttons
InlineEditor.LineHeight = 16;		// editor line height (easy lookup to calc textarea height)
InlineEditor.MinHeight = 16;		// minimum height of textarea (pixels)
InlineEditor.MaxHeight = 550;		// maximum height of textarea (pixels)
InlineEditor.ErrorFunc = function(message) { alert(message); }

/////// End of Configuration Variables ///////////////////

// Variables
InlineEditor.index = 0;
InlineEditor.editors = {};

// Constructor
function InlineEditor(edit_element,field_name,url,button) {

	this.edit_element = getRef(edit_element);
	this.form_field_name = field_name;
	this.form_action_url = url;
	this.edit_button = button ? getRef(button) : null;

	this.configure();

	// Initialize edit buffers. (IE hack)
	//this.original_content = this.edit_element.innerHTML;
	this.original_content = new InlineEditor.editbuf;
	this.original_content.getInnerHTML(this.edit_element);
	//this.changed_content = null;
	this.changed_content = new InlineEditor.editbuf;

	// Compute editor height.
	this.base_height = this.compute_base_height();

	// Make up names and ids.
	InlineEditor.index++;
	this.id = 'e' + InlineEditor.index;
	this.iframe_container_id = 'inline_editor_container' + InlineEditor.index;
	this.iframe_name = 'inline_editor_' + InlineEditor.index;

	// Register
	InlineEditor.editors[this.id] = this;
}

InlineEditor.prototype.configure = function() {
        // Assign capitalized global class settings to object settings.
	var copy_from = InlineEditor;
	var is_capitalized = function(v) { var c = v.substr(0,1); return c == c.toUpperCase(); }
        for (var v in copy_from) {
		if (is_capitalized(v)) {
                        this[v] = copy_from[v];
                }
        }
}

InlineEditor.prototype.compute_base_height = function() {
	// Base height is essentially height of textarea, no chrome.
	var count_lines = function(s) { var arr = s.match(/\n/g); return arr == null ? 1 : arr.length; }
	var base_height = getElementHeight(this.edit_element);
	base_height = Math.max(base_height, count_lines(this.original_content.get()) * this.LineHeight);
	base_height = Math.max(base_height, this.MinHeight);
	base_height = Math.min(base_height, this.MaxHeight);
	return base_height;
}

InlineEditor.prototype.submit = function(f) {
	this.changed_content.set(f[this.form_field_name].value);

	// Embed return fuction to call upon return from server in form's action url.
	url = this.form_action_url;
	if (! url.match(/\breturn_func\b/)) {
		url += (url.match(/\?/) ? '&' : '?') + 'return_func=InlineEditor.editors["'+this.id+'"].return_from_server';
	}

	f.action = url;
	f.method = 'post';
	return true;
}

InlineEditor.prototype.cancel = function() {
	//this.edit_element.innerHTML = this.original_content;
	this.original_content.setInnerHTML(this.edit_element);	// for IE
	this.remove_form();
}

InlineEditor.prototype.return_from_server = function(saved, message) {
	// Return from server-side program

	if (! saved) {
		if (! message) message = "Error, your changes were not saved on the server.";
		this.ErrorFunc(message);
		this.cancel();
		return;
	}

	//this.edit_element.innerHTML = this.changed_content;
	this.changed_content.setInnerHTML(this.edit_element);	// for IE

	this.remove_form();
}

InlineEditor.prototype.remove_form = function() {
	if (this.edit_button) {
		if (this.DisableEditButton) { this.edit_button.disabled = false; }
		if (this.HideEditButton) { this.edit_button.style.display = ''; }
	}
	delete InlineEditor.editors[this.id];

	// Note: Firefox does not delete a frame even though its enclosing DOM is removed.
	try { delete window.frames[this.iframe_name]; } catch(e) {}	// for FireFox
}

InlineEditor.prototype.edit = function() {

	if (this.edit_button) {
		if (this.DisableEditButton) { this.edit_button.disabled = true; }
		if (this.HideEditButton) { this.edit_button.style.display = 'none'; }
	}
	this.edit_element.innerHTML = this.editor_html(this.form_field_name);

	setTimeout('InlineEditor.editors["'+this.id+'"].load_content()',1);
}

InlineEditor.prototype.load_content = function() {

	// Trim original content of leading and trailing white space.
	var textarea = window.frames[this.iframe_name].document.forms[0].elements[0];
	textarea.value = this.original_content.get().replace(/^\s+|\s+$/g,'');
}

InlineEditor.prototype.editor_html = function(field_name) {
	var height = (this.base_height + this.ButtonHeight);
	return ''
 + '<div id="'+this.iframe_container_id+'" style="height:'+height+'px;">'
 + '<iframe id="'+ this.iframe_name + '" name="'+this.iframe_name+'" src="javascript:parent.InlineEditor.editors[\''+this.id+'\'].editor_form_html(\''+field_name+'\')" width="100%" height="'+height+'" marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="0"></iframe>'
 + '</div>\n';
}

InlineEditor.prototype.editor_form_html = function(field_name) {
	var height = (this.base_height + this.ButtonHeight);
	return ''
 + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
 + '<html>\n'
 + '<head>\n'
 + '<base href="'+location.href+'">\n'
 + '<style type="text/css">\n'
 + 'body, html { margin:0px; padding:0px; }\n'
 + '#edit_form { overflow:hidden; height:'+height+'px; }\n'
 + '#edit_form textarea { width:99%; height:'+this.base_height+'px; }\n'
 + '</style>\n'
 + this.CSS
 + '</head>\n'
 + '<body>\n'
 + '<div id="edit_form">\n'
 + '<form onsubmit="return parent.InlineEditor.editors[\''+this.id+'\'].submit(this)">\n'
 + '<textarea name="'+field_name+'" onkeyup="parent.InlineEditor.enable_save(event,this)">\n'
 + '</textarea>\n'
 + '<div class="buttons">\n'
 + '<input type="submit" name="save" value="Save" disabled="disabled" />\n'
 + '<input type="button" name="cancel" value="Cancel" onclick="parent.InlineEditor.editors[\''+this.id+'\'].cancel(this.form)" />\n'
 + '</div>\n'
 + '</form>\n'
 + '</div>\n';
 + '</body>\n'
 + '</html>\n';
}

//
// Class functions
//

InlineEditor.non_edit_keycodes = {
			16:1,	// shift
			17:1,	// ctl
			18:1,	// alt
			19:1,	// pause
			20:1,	// caps lock
			27:1,	// escape
			33:1,	// page up
			34:1,	// page down
			35:1,	// end
			36:1,	// home
			37:1,	// arrow left
			38:1,	// arrow up
			39:1,	// arrow right
			40:1,	// arrow down
			44:1,	// print scrn
			45:1,	// insert
			91:1,	// windows key
			112:1,	// F1
			113:1,	// F2
			114:1,	// F3
			115:1,	// F4
			116:1,	// F5
			117:1,	// F6
			118:1,	// F7
			119:1,	// F8
			120:1,	// F9
			121:1,	// F10
			122:1,	// F11
			123:1,	// F12
			144:1,	// numlock
			145:1	// scroll lock
			};

InlineEditor.is_edit_key = function(k) {
	// return true if keyCode k alters an input field.
	return (k && ! InlineEditor.non_edit_keycodes[k]) ? true : false;
}

InlineEditor.enable_save = function(evt,ctl) {
	// Input field keyup handler.
	if (evt.keyCode && InlineEditor.is_edit_key(evt.keyCode)) {
		ctl.form.save.disabled=false;
		ctl.onkeyup=null	// remove handler
	}
}

// The following atrocity is for IE's benefit.
// IE innerHTML trashes all newlines if originating element is not <pre> or white-space:pre.
// You also cannot assign innerHTML to a variable and retrieve it without IE trashing newlines.
// Setting any kind of an element's innerHTML removes newlines unless surrounded by <pre></pre>
// The editbuf object provides a place to store innerHTML with newlines and methods to set a
// DOM element's innerHTML.

InlineEditor.editbuf = function(s) {
	this.buf = document.createTextNode(s != null ? s : '');	
}
InlineEditor.editbuf.prototype.set = function(s) {
	this.buf = document.createTextNode(s);
}
InlineEditor.editbuf.prototype.get = function() {
	return this.buf.data;
}
InlineEditor.editbuf.prototype.setInnerHTML = function(e) {
	if (navigator.userAgent.match(/MSIE/)) {
		// Save edit buffer of source code (with newlines) in node for next time.
		e.editBuf = this;

		if (e.nodeName == 'PRE') {
			// To trick IE into displaying newlines in <pre> tags, add two more.
			e.innerHTML = '<pre>'+this.get()+'</pre>';
		}
		else {
			e.innerHTML = this.get();
		}
	}
	else {
		e.innerHTML = this.get();
	}
}
InlineEditor.editbuf.prototype.getInnerHTML = function(e) {
	// Retreive previous edit buffer (with newlines) if previously saved in node.
	if (e.editBuf) {
		this.set(e.editBuf.get());
	}
	else {
		this.set(e.innerHTML);
	}
	return this.get();
}

//
// Global support functions referenced by InlineEditor.
// 

function getRef(id) {
        if (typeof id == "string") return document.getElementById(id);
        return id;
}

function getElementHeight(e) {
	if (!e) return;

	var result = 0;
	if (e.offsetHeight) {
		result = e.offsetHeight;
	}
	else if (e.clip && e.clip.height) {
		result = e.clip.height;
	}
	else if (e.style && e.style.pixelHeight) {
		result = e.style.pixelHeight;
	}
	return parseInt(result);
}

