// richedit.js - created by Michael Leonhard as he follows the article:
// How to Create a WYSIWYG Rich Text Editor in JavaScript. Pt. 1
// http://www.webreference.com/programming/javascript/gr/column11/
// http://www.webreference.com/programming/javascript/gr/column12/

// test the browser
var bIsNetscape = (navigator.appName == "Netscape");

// RichEdit constructor
function RichEdit (name, editStyle, charStyle, sText)
{
    // initialize some internal values
    this.bInFocus = false;
    
    // save the character style for later
    this.style = charStyle;
    
    // create an insertion point.
    document.write('<div id="' + name + '"></div>');
    this.oInsertionPoint = document.getElementById(name);
    
    // the 'text' box, where the text goes
    this.oDiv = document.createElement('div');

    // set the styles
    this.copyStyle(this.oDiv, editStyle);
    this.oDiv.style.overflow = "auto"; 
    this.oDiv.style.wordWrap = "break-word";
    
    // insert the text
    if ( sText ) this.setHTML(sText);
    
    // link back to this object
    this.oDiv.oRichEdit = this;
    
    // handle clicks
    this.oDiv.onclick = RichEdit.prototype.onDivClick;
    
    if (bIsNetscape)
    {
	// In Netscape, <div> elements don't have keyboard events, so
	// create a small, hidden text <input> box to catch them
	var oTextSpan = document.createElement('span');
	oTextSpan.style.position = 'absolute';
	oTextSpan.style.visibility = 'hidden';
	
	this.oTextbox = document.createElement('input');
	this.oTextbox.type = 'text';
	this.oTextbox.style.width = "1px";
	oTextSpan.appendChild(this.oTextbox);
	this.oInsertionPoint.appendChild(oTextSpan);
	
	// add event handlers
	this.oTextbox.oRichEdit = this;
	this.oTextbox.onkeypress = RichEdit.prototype.onKeyPress;
	this.oTextbox.onfocus = RichEdit.prototype.onDivFocus;
	this.oTextbox.onblur = RichEdit.prototype.onDivBlur;
    }
    else
    {
	// in IE, just attach everything to the <div>
	this.oDiv.onkeypress = RichEdit.prototype.onKeyPress;
	this.oDiv.onkeydown = RichEdit.prototype.onKeyDown;
	this.oDiv.onfocus = RichEdit.prototype.onDivFocus;
	this.oDiv.onblur = RichEdit.prototype.onDivBlur;
    }
    
    // create the cursor
    this.oCursor = document.createElement('span');
    this.copyStyle(this.oCursor, this.style);
    this.oCursor.innerHTML = '|';
    this.oCursor.style.position = 'absolute';
    this.oCursor.style.verticalAlign = 'bottom';
    
    // make it blink
    var oCursor = this.oCursor;
    window.setTimeout(function(){RichEdit.prototype.onBlinkCursor(oCursor);}, 500);
    
    this.oInsertionPoint.appendChild(this.oDiv);
}

RichEdit.prototype.onBlinkCursor = function(oCursor)
{
    if ( oCursor.style.visibility == 'hidden' )
	oCursor.style.visibility = 'visible';
    else
	oCursor.style.visibility = 'hidden';
    
    window.setTimeout(function(){RichEdit.prototype.onBlinkCursor(oCursor);},500);
}

RichEdit.prototype.focus = function()
{
    // grab the input focus
    if ( bIsNetscape )
	this.oTextbox.focus();
    else
	this.oDiv.focus();
}

RichEdit.prototype.getCursorPos = function()
{
    // return an object describing the current cursor position:
    //    prev: the node before the cursor
    //    current: the current node
    //    next: the node following the cursor
    //    insert: A node to insert new nodes before. Not always the same as current
    if (this.bInFocus)
    {
	var oParent = this.oCursor.parentNode;
	if (oParent == this.oDiv)
	{
	    // this can happen when an element other than <span> is current.
	    var oCurrent = this.oCursor.nextSibling;
	    var oNext = oCurrent ? oCurrent.nextSibling : null;
	    return {prev:this.oCursor.previousSibling,
		    current:oCurrent,
		    next:oNext,
		    insert:this.oCursor
		   };
	}
	else if (!oParent)
	{
	    return {prev:this.oDiv.lastChild,
		    current:null,
		    next:null,
		    insert:null
		   };
	}
	else return {prev:oParent.previousSibling,
		     current:oParent,
		     next:oParent.nextSibling,
		     insert:oParent
		    };
    }
    else if (!this.oLastCursorPos)
    {
	this.oLastCursorPos = {prev:this.oDiv.lastChild,
			       current:null,
			       next:null,
			       insert:null
			      };
    }
    return this.oLastCursorPos;
}

RichEdit.prototype.setCursorPos = function(oSpan, bCopyStyle)
{
    // set the position of the cursor to just before oSpan
    // or if null/undefined, append to the end.
    var oElt;
    if (oSpan)
    {
	if (this.bInFocus)
	{
	    if (oSpan.tagName.toLowerCase() != 'span')
	    {
		this.oDiv.insertBefore(this.oCursor, oSpan);
	    }
	    else if (oSpan.firstChild)
	    {
		oSpan.insertBefore(this.oCursor, oSpan.firstChild);
	    }
	    else oSpan.appendChild(this.oCursor);
	}
	else this.oLastCursorPos = {prev:oSpan.previousSibling,
				    current:oSpan,
				    next:oSpan.nextSibling
				   };
	oElt = oSpan;
    }
    else
    {
	oElt = this.oDiv.lastChild;
	if (this.bInFocus)
	    this.oDiv.appendChild(this.oCursor);
	else
	    this.oLastCursorPos = null;
    }
    
    if ( (bCopyStyle == undefined) || (bCopyStyle == true) )
	this.assimilateStyle(oElt);
}

RichEdit.prototype.assimilateStyle = function(oElt)
{
    if (!oElt)
	oElt = this.oDiv.lastChild;
    
    // when the cursor moves, it is natural that it should take on
    // the style of its surroundings
    while ( oElt && oElt.previousSibling && (!oElt.bHasStyle || (oElt == this.oCursor)) )
    {
	oElt = oElt.previousSibling;
    }
    
    if (oElt && oElt.tagName.toLowerCase() == 'span')
    {
	this.setStyle(oElt.style);
	if (this.onstylechange != undefined)
	    this.onstylechange(this.style);
    }
}

RichEdit.prototype.copyStyle = function(oElt, style, template)
{
    // the template is used to determine what to copy
    if (!template) template = style;
    
    // set the styles
    for (var iStyle in template)
    {
	if (style[iStyle] != undefined) oElt.style[iStyle] = style[iStyle];
    }
    oElt.bHasStyle = true;
}

RichEdit.prototype.onBlur = function()
{
    // when focus goes somewhere else, record the last position
    // and hide the cursor
    this.oLastCursorPos = this.getCursorPos();
    this.bInFocus = false;
    this.oCursor.parentNode.removeChild(this.oCursor);
}

RichEdit.prototype.onFocus = function()
{
    // focus has returned, so re-insert the cursor
    var oPos = this.getCursorPos();
    this.bInFocus = true; 
    this.setCursorPos(oPos.current, false);
}

RichEdit.prototype.insertNode = function(oNode)
{
    // insert a node at the current insertion point.
    var oPos = this.getCursorPos();
    if (oPos.insert)
	this.oDiv.insertBefore(oNode, oPos.insert);
    else
	this.oDiv.appendChild(oNode);
    
    // attach an onclick handler to the node so that the
    // cursor can be positioned properly when the user clicks.
    oNode.onclick = RichEdit.prototype.onSpanClick;
    oNode.oRichEdit = this;
}

RichEdit.prototype.insertText = function(sText, oStyle)
{
    // insert a text string
    if ( !oStyle ) oStyle = this.style;
    
    var n = sText.length;
    for (var i = 0; i < n; i++)
    {
	// for each character, insert a new <span> element with current style
	var oSpan = document.createElement('span');
	this.copyStyle(oSpan, oStyle, this.style);
	var c = sText.charAt(i);
	
	// characters to be translated
	switch ( c )
	{
	case '\n':  oSpan = document.createElement('br'); break;
	case ' ':   oSpan.innerHTML = '&nbsp;';  break;
	case '<':   oSpan.innerHTML = '&lt;';  break;
	case '>':   oSpan.innerHTML = '&gt;';  break;
	case '&':   oSpan.innerHTML = '&amp;';  break;
	case '"':   oSpan.innerHTML = '&quot;';  break;
	case "'":   oSpan.innerHTML = '&#39;';  break;
	default:   oSpan.innerHTML = c;  break;
	}
    }
    this.insertNode(oSpan);
}
