/*
 * This class replaces all textareas with a class of 'PURichText' with a rich text editor
 *
 * There are 2 elements that hold the content of the editor, the textarea which holds the source
 * and the iframe which hold a 'document' representation of that source.
 * On Load, Submit and 'View Source' the content is copied into the non-visible element(textarea or iframe).
 * Version 0.3
 * Author: Mark brown
**/

var rt = [];

var PURichTextEditor = Class.create({

	// This array holds the stylesheets for editors windows.
	stylesheets: ['css/iframe.css'],
	
	// Formatting Selection
	formatOptions: $H({
		P: 'Normal',
		H1: 'Heading 1',
		H2: 'Heading 2',
		H3: 'Heading 3',
		H4: 'Heading 4',
		H5: 'Heading 5',
		H6: 'Heading 6'
	}),
	
	// Create an array of hash groups with the default tools that use execCommand
	tools: [
		$H({
			bold: 'Bold',
			italic: 'Italic'
		}), 
		$H({
			// underline: 'Underline',
			justifyleft: 'Align Left',
			justifycenter: 'Align Center',
			justifyright: 'Align Right'
		}),
		$H({
			insertorderedlist: 'Nurtred List',
			insertunorderedlist: 'Bulletpoint List',
			outdent: 'Outdent',
			indent: 'Indent'
		}),
		$H({
			undo: 'Undo',
			redo: 'Redo'
		})
	],
	
	// Custom Tools
	customTools: $H({
		image: 'Insert Image',
		document: 'Insert Document',
		table: 'Insert Table',
		link: 'Insert Link',
		anchor: 'Insert Anchor',
		manageFiles: 'Manage Files',
		toggleSource: 'Edit HTML Source'
	}),
	
	// Tools for HTML editing
	sourceTools: $H({
		toggleSource: 'View Editor'
	}),
	
	initialize: function(i, textarea) {
	
		this.i = i;
		this.textarea = textarea;
		this.id = textarea.getAttribute('id');
		var dim = textarea.getDimensions();
		this.height = dim.height;
		this.width = dim.width;
		
		// Get form that is holding the textarea, and setup submit listener
		this.form = textarea.up('form').observe('submit', this.submitHandler.bind(this));
		
		// Array to hold our elements to add into the document
		var elements = [];
		
		// To create reference to a function given a subject and a name of the function as a String.
		var makeHandler = function(subject, method) {
			return function() { subject[method](); }
		}
		
		// Create Toolbar
		this.toolbar = new Element('ul', {
			id: this.id+'_toolbar',
			className: 'rt_toolbar'
		}).setStyle({
			width: this.width + 'px'
		});
		elements.push(this.toolbar);
		
		// Add Format Selection
		var li = this.toolbar.appendChild(new Element('li'));
		var select = li.appendChild(new Element('select', {
			id: this.id + '_format',
			className: 'formatSelect'
		}));
		select.options.add(new Option('Format Selection', '', false));
		this.formatOptions.each(function(format) {
			select.options.add(new Option(format.value, format.key, false));
		});
		this.formatSelect = $(select).observe('change', this.formatSelection.bind(this));
		
		// Within prototype's iterators you lose reference to current object
		var self = this;
		
		// Add Editor Tools
		this.tools.each(function(toolGroup) {
			var ulli = self.toolbar.appendChild(new Element('li'));
			var ulul = ulli.appendChild(new Element('ul'));
			toolGroup.each(function(tool) {		
				var lili = ulul.appendChild(new Element('li'));
				var a = lili.appendChild(new Element('a', {
					title: tool.value,
					className: tool.key
				}).update(tool.value));
				if (!Prototype.Browser.IE) {
					var clickHandler = function() { self['editorCommand'](tool.key, ''); };
					a.observe('click', clickHandler);
				}
				else {
					/* IE loses the selection object when you click elsewhere - do it the Bad Old Way
					   Other browsers keep selections for each window */
					a.href= 'javascript:rt['+i+'].editorCommand("'+tool.key+'", "")';
				}
			});
		});
		
		// Add Custom Tools Selection
		var li = this.toolbar.appendChild(new Element('li'));
		var select = li.appendChild(new Element('select'), {
			id: this.id + '_custom_tool',
			className: 'customToolSelect'
		});
		select.options.add(new Option('Tools', '', false));
		this.customTools.each(function(tool) {
			select.options.add(new Option(tool.value, tool.key, false));
		});
		this.toolSelect = $(select).observe('change', this.toolSelection.bind(this));
		
		// Add View Source Tools
		this.srcToolbar = new Element('ul', {
			id: this.id + '_source_toolbar',
			className: 'rt_toolbar'
		}).setStyle({
			width: this.width + 'px',
			display: 'none'
		});
		elements.push(this.srcToolbar);
		
		this.sourceTools.each(function(tool) {
			var li = self.srcToolbar.appendChild(new Element('li'));
			var a = li.appendChild(new Element('a', {
				title: tool.value,
				className: tool.key
			}));
			a.update(tool.value).observe('click', makeHandler(self, tool.key));
		});
		
		// Create iframe of same dimensions as textarea
		this.iframe = new Element('iframe', {
			id: this.id + '_iframe',
			name: this.id + '_iframe',
			className: 'rt_iframe',
			frameBorder: 0
		}).setStyle({
			width: this.width + 'px',
			height: this.height + 'px'
		});
		elements.push(this.iframe);
		
		// Add toolbar and iframe before textarea
		elements.each(function(element) {
			textarea.parentNode.insertBefore(element, textarea);
		});

		this.textarea.hide();
		this.contentWindow = this.iframe.contentWindow;
		// FF
		Event.observe(this.contentWindow, 'keyup', this.editorKeyUpHandler.bind(this));
		// Safari
		Event.observe(this.contentWindow.document, 'keyup', this.editorKeyUpHandler.bind(this));

		// write the content of textarea to the iframe, an ertdded stylesheet was all that worked in multiple browsers..
		this.contentWindow.document.open();
		this.contentWindow.document.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head>');
		
		// For Prototypes iterator loss of 'this' reference
		var cw = this.contentWindow;
		
		// IE only works with ertdded styles
		if (Prototype.Browser.IE) {
			this.contentWindow.document.write('<style type="text/css">');
			this.stylesheets.each(function(ss) {
				cw.document.write('@import url('+ss+');');
			});
			
			cw.document.write('</style>');
		}
		cw.document.write('</head><body class="editor_body">'+this.textarea.value+'</body></html>');
		cw.document.close();
		
		if (!Prototype.Browser.IE) {
			// Attach Editors stylsheets the DOM way
			this.stylesheets.each(function(ssLink) {
				var ss = new Element('link', {
					href: ssLink,
					type: 'text/css',
					rel: 'stylesheet',
					media: 'screen'
				});
				
				cw.document.getElementsByTagName('head')[0].appendChild(ss);
			});
		}
		// enable the designMode	
		try {
			this.contentWindow.document.designMode = 'on';
			this.contentWindow.designMode = 'on';
		}
		catch(e) { /* alert(e); */ }
		
		// FF's 'styledWithCSS' html is bloated
		this.editorCommand('useCSS', false);
		this.editorCommand('styleWithCSS', false);
	},
	
	// assign the value of the iframe to the textarea
	updateTextarea: function() {
		this.textarea.value = this.contentWindow.document.body.innerHTML;
	},
	
	// assign the value of the textarea to the iframe
	updateIframe: function() {
		this.contentWindow.document.body.innerHTML = this.textarea.value;
	},
	
	// View Source
	toggleSource: function() {
		// Hide visible editor and show hidden editor
		(this.textarea.visible()) ? this.updateIframe() : this.updateTextarea() ;
		this.textarea.toggle();
		this.srcToolbar.toggle();
		this.toolbar.toggle();
		this.iframe.toggle();
	},
	
	/* A format from this.formatOptions has been selected */
	formatSelection: function() {
		this.editorCommand('formatblock', '<' + this.formatSelect.options[this.formatSelect.selectedIndex].value + '>');
		this.formatSelect.selectedIndex = 0;
	},
	
	/* A tool from this.customTools has been selected */
	toolSelection: function() {
		var key = this.toolSelect.options[this.toolSelect.selectedIndex].value;
		switch(key) {
		
			case 'toggleSource':
				this.toggleSource();
				break;
			case 'image':
				this.launchDialog('insertImage.html', 400, 150);
				break;
			case 'link':
				this.launchDialog('insertLink.html', 400, 150);
				break;
			case 'table':
				this.launchDialog('insertTable.html', 400, 250);
				break;
			case 'anchor':
				this.launchDialog('insertAnchor.html', 400, 100);
				break;
			case 'document':
				this.launchDialog('insertDocument.html', 400, 150);
				break;
			case '': break;
			default:
				alert('Sorry, The Tool "' + key + '" has not been implemented yet :(');
				break;
		}
		this.toolSelect.selectedIndex = 0;
	},
	
	// Tools from the view source menu
	sourceToolSelection: function(key) {
		switch(key) {
		
			// View HTML Source
			case 'toggleSource':
				this.toggleSource();
				break;
			case '': break;
			default:
				alert('Sorry, The Source Tool "' + key + '" has not been implemented yet :(');
				break;
		}
	},

	// Uses insertHTML execCommand to put HTML into the editor at the point of selection
	addContent: function(content) {
		
		// IE doesn't have an insertHTML command
		if (Prototype.Browser.IE) {
			var elm = this.getSelectedContainer();
			if (this.contentWindow.document.body.innerHTML == '') {
				// Empty textarea - Just set the contents of the body
				this.contentWindow.document.body.innerHTML = content;
			}
			else {
				// Add content after the selected element
				new Insertion.After(elm, content);
			}
		}
		else {
			this.editorCommand('insertHTML', content);
		}
	},
	
	// insert an HTML element *after* the current one
	insertAfter: function(elem, currentElem) {
		if (currentElem.nextSibling != null)
			currentElem.parentNode.insertBefore(elem,currentElem.nextSibling);
		else if (currentElem.nodeName.toLowerCase() == 'body')
			currentElem.appendChild(elem);
		else if (currentElem.parentElement)
		    currentElem.parentElement.appendChild(elem);
		else
			currentElem.parentNode.appendChild(elem);
	},
	
   /** Calls execCommand function - various levels of support in browsers and different html output
	 * Helpful links regarding designMode and execCommand
	 * http://msdn2.microsoft.com/en-us/library/ms533049(VS.85).aspx
	 * http://www.mozilla.org/editor/ie2midas.html 
	**/
	editorCommand: function(command, options) {
		try {
			this.contentWindow.focus();
			this.contentWindow.document.execCommand(command, false, options);
			this.contentWindow.focus();
		} catch (e) { /* alert(e); */ }
	},
	
	/* Range and Selection objects are created and used to get information about the currently 
	 * selected content in a document, IE uses a different implementation to other browsers.
	**/
	getSelection: function() {
		return (window.getSelection) ? this.contentWindow.getSelection() : this.contentWindow.document.selection.createRange();
	},
	
	/* Returns the block that is in the currect Selection */
	getSelectedContainer: function() {
		
		var s = this.getSelection();
		if (s.parentElement && s.parentElement != undefined) {
			elm = s.parentElement();
			/* IE loses selection focus when you click outside the editor so ensure the selection 
			 * is within a text editor. A bit of a hack to see where the selection is */
			try {
				var test = elm.up('');
			}
			catch(e) {return elm;}
			return false;
		}
		else {
			var node = s.focusNode;
			if (node.nodeName == '#text') {
				return node.parentNode;
			}
			else {
				return node;
			}
		}
		return false;
	},
	
	/* Do cleanup of editor before form data */
	submitHandler: function(event) {

		if (this.textarea.visible())
			this.toggleSource();
		// Convert anchor images to real anchors
		// DOM on iframe's window
		var images = this.contentWindow.document.getElementsByTagName('img');
		for(var i =0; i<images.length; i++) {
			var img = images[i];
			if (img.className == 'namedAnchor')
				Element.replace(img, '<a name="'+img.alt+'"></a>');
		}
		this.updateTextarea();
		// Show content in textarea and stop Submit
		alert(this.textarea.value);
		Event.stop(event);
	},
	
	launchDialog: function(url, width, height) {
		if (this.getSelectedContainer() == false)
			alert('Please click the editor at the point of insertion before selecting tool.');
		else
			window.open(url+'?id=' + this.i, '', 'width='+width+',height='+height);
	},
	
	updateFromPopup: function(formData) {
		// alert(formData.asString);
		switch(formData.asHash.tool) {
			case 'image':
				this.addContent('<img src="'+formData.asHash.src+'" alt="'+formData.asHash.alt+'" />');
				break;
			case 'link':
				this.addContent('<a href="'+formData.asHash.href+'">'+formData.asHash.text+'</a>');
				break;
			case 'table':
				var rows = parseInt(formData.asHash.rows);
				var cols = parseInt(formData.asHash.cols);
				var border = (formData.asHash.border == 'checked');
				var headings = (formData.asHash.headings == 'checked')
				var table = this.getTable(rows, cols, border, headings);
				this.addContent(table);
				break;
			case 'anchor':
			    /* create img anchor to be transformed into a real anchor on submit
			       Make's it more manageable in editor */
				var a = $(this.contentWindow.document.createElement('img'));
				a.src = 'images/anchor.gif';
				a.className = 'namedAnchor';
				a.title = formData.asHash.name;
				a.alt = formData.asHash.name;

				// Insert after selection
				this.insertAfter(a, this.getSelectedContainer());
				// Setup event on click to remove anchor
				Event.observe(a, 'click', function(event) { a.parentNode.removeChild(a); });
				Event.observe(a, 'mouseover', function(event) { a.src = 'images/anchor_remove.gif'; });
				Event.observe(a, 'mouseout', function(event) { a.src = 'images/anchor.gif'; });
				break;
			case 'document':
				this.addContent('<img src="https://secure.pageuppeople.com/people/218/people/66353/icon_file_small.gif" alt="Download File" />&nbsp;<a href="'+formData.asHash.href+'">'+formData.asHash.text+'</a>');
				break;
			default:
				alert('Sorry, The popup Tool "' + formData.asHash.tool + '" has not been implemented yet :(');
				break;
		}
	},
	
	// Generate table markup
	getTable: function(rows, cols, border, headings) {
		var table = '<table';
		if (border)
			table += ' class="border" border="1"';
		table += '>';
		if (headings) {
			table += '<thead><tr>';
			for (var i=0; i<cols; i++) {
				table += '<th></th>';
			}
			table += '</tr></thead>';
		}
		table += '<tbody>';
		for (var i=0; i<rows; i++) {
			table += '<tr>';
			for (var j=0; j<cols; j++) {
				table += '<td></td>';
			}
			table += '</tr>';
		}
		table += '</tbody>';
		table += '</table>';
		
		return table;
	},

	// Cleans up generated markup - makes ff closer to IE's rendering
	editorKeyUpHandler: function(e) {

		var key = e.which || e.keyCode;
		var elm = this.getSelectedContainer();
		var name = elm.tagName.toLowerCase();
		var shift = e.shiftKey;
		
		switch (key) {
			case Event.KEY_RETURN:
				if (!shift) {
					// Cleanup <br><br>'s in ff
					var nodes = this.contentWindow.document.body.childNodes;
					for (var i=0; i<nodes.length; i++) {
						if (nodes.item(i).nodeName.toLowerCase() == 'br') {
							this.contentWindow.document.body.removeChild(nodes.item(i));
						}
					}
				}
				var inlineTags = ['strong', 'b', 'em', 'i', 'sub', 'sup', 'a'];
				if (inlineTags.indexOf(name) != -1)
					name = elm.parentNode.tagName.toLowerCase();
				if (name == 'body')
					this.editorCommand('formatblock', '<P>');
				break;
			default:
				if (name == 'body') {
					// make stray elements paragraphs
					this.editorCommand('formatblock', '<P>');
				}
				break;
		}
	}
});

// init method
Event.observe(window, 'load', function() {
	
	// textarea will only be added if browser supports designMode
	if (document.designMode) {
	
		// Get all textarea's in page with class='PURichTextEditor'
		rtes = $$('textarea.PURichText');
		var i = 0;
		rtes.each(function(textarea) {
			// Replace textarea with editable iframe
			rt[i] = new PURichTextEditor(i, textarea);
			i++;
		});
	}
});