//<!-- <script type="text/javascript"> -->

//=============================================================================
// Utilities

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

function clrdebug()
{
    var oDbg = document.getElementById('debug');
    if ( oDbg ) oDbg.innerHTML = '';
}
function debug(txt)
{
    var oDbg = document.getElementById('debug');
    if ( oDbg ) oDbg.innerHTML += txt + '<br>'
}
function debugObj(o)
{
    for ( var i in o )
    {
	if ( bIsNetscape && (typeof(i) == 'string') )
	{
	    if ( i.substr(0,4) == 'DOM_' ) continue;
	}
	debug( i + ':' + o[i] );
    }
}

//=============================================================================
// RichEdit constructor
function RichEdit(name, editStyle, charStyle, sText)
{
    // initialise 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 - or 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.onDivKeyPress;
	this.oTextbox.onfocus = RichEdit.prototype.onDivFocus;
	this.oTextbox.onblur = RichEdit.prototype.onDivBlur;
    }
    else
    {
	// in IE, just attache everything to the <div>
	this.oDiv.onkeypress = RichEdit.prototype.onDivKeyPress;
	this.oDiv.onkeydown = RichEdit.prototype.onDivKeyDown;
	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';
    
    // <blink> is a Netscape thing, so... DIY
    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:
    //    previous: 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};
	}
	if ( !oParent )
	{
	    return {prev:this.oDiv.lastChild,current:null,next:null,insert:null};
	}
	return {prev:oParent.previousSibling,current:oParent,next:oParent.nextSibling,insert:oParent};
    }
    
    if ( !this.oLastCursorPos )
    {
	this.oLastCursorPos = {prev:this.oDiv.lastChild,current:null,next:null,insert:null};
    }
    return this.oLastCursorPos;
}
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.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,insert:oSpan};
	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.onBlur = function()
{
    // when focus goes somewhere else, record the last position
    // and hide the cursor
    this.oLastCursorPos = this.getCursorPos();
    
    // insert can sometimes be the cursor itself and since this
    // is being removed, copy current.
    this.oLastCursorPos.insert = this.oLastCursorPos.current;
    
    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.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.setStyle = function(style)
{
    this.copyStyle(this, style, this.style);
    this.copyStyle(this.oCursor, style, this.style);
}
RichEdit.prototype.compareStyle = function(oSpan1, oSpan2)
{
    // return true if the style of the spans is equivalent
    var style1 = oSpan1.style;
    var style2 = oSpan2.style;
    
    // equivalence depends only on the styles we are interested in.
    for ( var iStyle in this.style )
    {
	if ( style1[iStyle] !== style2[iStyle] ) return false;
    }
    return true;
}
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);
    
    // any node inserted into the document must notify the control
    // when it is clicked on so that the cursor may be moved
    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);
	
	// not all keyboard characters translate well to HTML text
	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);
    }
}
RichEdit.prototype.onKeyPress = function(evt)
{
    // handle keyboard events
    
    // in Netscape, handle movement keys here
    if ( bIsNetscape && this.onKeyDown(evt) ) return;
    
    var keyCode = evt.keyCode;
    
    if ( keyCode == 13 ) // return
    {
	// insert a line break before the cursor
	this.insertNode(document.createElement('br'));
    }
    else
    {
	// insert the character before the cursor
	this.insertText(String.fromCharCode(bIsNetscape ? evt.which : evt.keyCode))
    }
    
    // keep the cursor in view.
    this.seeCursor();
}
RichEdit.prototype.onKeyDown = function(evt)
{
    var bRet = true;
    var keyCode = evt.keyCode;
    
    // find the cursor
    var oPos = this.getCursorPos();
    
    var oPrev = oPos.prev;
    var oNext = oPos.next;
    var oNext2, oPrev2;
    var nLeft = this.oCursor.offsetLeft;
    var nTop = this.oCursor.offsetTop;
    switch ( keyCode )
    {
    case 37: // left arrow
	if ( oPrev ) this.setCursorPos(oPrev);
	else this.setCursorPos(this.oDiv.firstChild);
	break;
    case 39: // right arrow
	this.setCursorPos(oNext);
	break;
    case 38: // up arrow
	// search backwards for a character above and to the left of this one
	while ( oPrev )
	{
	    oPrev2 = oPrev.previousSibling;
	    if ( !oPrev2 )
	    {
		this.setCursorPos(oPrev);
		break;
	    }
	    if ( (oPrev.tagName.toLowerCase() == 'br') && (oPrev2.offsetLeft < nLeft) )
	    {
		this.setCursorPos(oPrev);
		break;
	    }
	    if ( (oPrev2.tagName.toLowerCase() != 'br') && (oPrev2.offsetTop < nTop) && (oPrev2.offsetLeft <= nLeft) )
	    {
		this.setCursorPos(oPrev2);
		break;
	    }
	    oPrev = oPrev2;
	}
	break;
    case 40: // down arrow
	// search forewards for a character just below the cursor
	while ( oNext )
	{
	    oNext2 = oNext.nextSibling;
	    if ( !oNext2 )
	    {
		this.setCursorPos();
		break;
	    }
	    if ( oNext2.offsetTop > nTop )
	    {
		if ( oNext2.offsetLeft >= nLeft )
		{
		    this.setCursorPos(oNext2);
		    break;
		}
		// special case, end of the next line
		if ( (oNext2.offsetTop > oNext.offsetTop) && (oNext.tagName.toLowerCase() != 'br') )
		{
		    this.setCursorPos(oNext2);
		    break;
		}
		if ( oNext2.tagName.toLowerCase() == 'br' )
		{
		    this.setCursorPos(oNext2);
		    break;
		}
	    }
	    oNext = oNext2;
	}
	break;
    case 8 : // backsp
	if ( oPrev )
	{
	    this.oDiv.removeChild(oPrev);
	    this.assimilateStyle(oPos.current);
	}
	break;
    case 46: // del
	if ( oPos.current )
	{
	    this.oDiv.removeChild(oPos.current);
	    this.setCursorPos(oPos.next);
	}
	break;
    case 36: // home
	if ( evt.ctrlKey ) this.setCursorPos(this.oDiv.firstChild);
	else
	{
	    // search backwards to the beginning of the line
	    oPrev = oPos.current;
	    while ( oPrev )
	    {
		if ( oPrev.offsetLeft == 0 )
		{
		    this.setCursorPos(oPrev);
		    break;
		}
		oPrev = oPrev.previousSibling;
	    }
	    if ( !oPrev ) this.setCursorPos(this.oDiv.firstChild);
	}
	break;
    case 35: // end
	if ( evt.ctrlKey ) this.setCursorPos();
	else
	{
	    // search forwards to the end of the line
	    while ( oNext )
	    {
		if ( (oNext.tagName.toLowerCase() == 'br') || (oNext.offsetTop > nTop) )
		{
		    this.setCursorPos(oNext);
		    break;
		}
		oNext = oNext.nextSibling;
	    }
	    if ( !oNext ) this.setCursorPos();
	}
	break;
    case 33: // page up
	// implementation left as exercise for the reader
	break;
    case 34: // page down
	// implementation left as exercise for the reader
	break;
    default:
	bRet = false;
	break;
    }
    
    this.seeCursor();
    if ( bIsNetscape ) return bRet;
}
RichEdit.prototype.seeCursor = function()
{
    var oPos = this.getCursorPos();
    if ( !oPos.insert ) return;
    
    var sh = this.oDiv.scrollHeight;
    var st = this.oDiv.scrollTop;
    var ot = oPos.insert.offsetTop;
    
    var dh = this.oDiv.offsetHeight;
    var oh = this.oCursor.offsetHeight;
    
    // st should be less than ot
    // and greater than ot + oh - dh
    if ( st > ot ) this.oDiv.scrollTop = ot;
    else if ( st < (ot + oh - dh) ) this.oDiv.scrollTop = ot + oh - dh;
}
RichEdit.prototype.getScratchPad = function()
{
    // get a hidden <div> scratch-pad to work on
    if ( !this.oPad )
    {
	this.oPad = document.createElement('div');
	this.oPad.style.position = 'absolute';
	this.oPad.style.visibility = 'hidden';
	this.oInsertionPoint.appendChild(this.oPad);
    }
    else
    {
	// clear anything that might have been there
	this.oPad.innerHTML = '';
    }
    return this.oPad;
}
RichEdit.prototype.getHTML = function()
{
    // return a compact version of HTML contents in text form
    
    // remove the cursor first
    var oCursorPos = this.getCursorPos();
    if ( this.bInFocus )
    {
	this.oDiv.removeChild(this.oCursor);
    }
    
    var oPad = this.getScratchPad();
    
    // iterate through each character-element or image
    // and merge into the scratch-pad
    var oElt = this.oDiv.firstChild;
    var oPost = null;
    while ( oElt )
    {
	if ( oPost &&
	     (oPost.tagName.toLowerCase() == 'span') &&
	     (oElt.tagName.toLowerCase() == 'span') &&
	     this.compareStyle(oPost,oElt) )
	{
	    // styles are the same, so merge
	    oPost.innerHTML += oElt.innerHTML;
	}
	else
	{
	    oPost = oElt.cloneNode(true);
	    oPad.appendChild(oPost);
	}
	oElt = oElt.nextSibling;
    }
    
    if ( this.bInFocus ) this.setCursorPos(oCursorPos.current);
    
    return oPad.innerHTML;
}
RichEdit.prototype.setHTML = function(sHTML)
{
    this.oDiv.innerHTML = '';
    this.oLastCursorPos = null;
    this.bInFocus = false;
    
    // get a hidden scratchpad to work on
    var oPad = this.getScratchPad();
    oPad.innerHTML = sHTML;
    
    // iterate through each element of oPad and extract text, <br>, <img>, etc
    var oElt = oPad.firstChild;
    if ( oElt.nodeType == 3 ) // text
    {
	this.insertText(sHTML);
    }
    else
    {
	while ( oElt )
	{
	    var oNext = oElt.nextSibling;
	    if ( oElt.tagName.toLowerCase() == 'span' )
	    {
		// must convert special HTML codes back to text so they
		// can be translated properly by insertText()
		var sText = oElt.innerHTML.replace('&amp;','&');
		sText = sText.replace('&quot;','"');
		sText = sText.replace('&nbsp;',' ');
		sText = sText.replace('&lt;','<');
		sText = sText.replace('&gt;','>');
		sText = sText.replace('&#39;',"'");
		
		// insert the text from the element with the element's style
		this.insertText(sText, oElt.style);
	    }
	    else
	    {
		// don't know what it is, so insert it verbatim
		this.insertNode(oElt);
	    }
	    
	    oElt = oNext;
	}
    }
}

//=============================================================================
// Textbox event handlers
RichEdit.prototype.onDivKeyDown = function(e)
{
    // fetch event object
    var evt = e ? e : window.event;
    this.oRichEdit.onKeyDown(evt);
}
RichEdit.prototype.onDivKeyPress = function(e)
{
    // fetch event object
    var evt = e ? e : window.event;
    this.oRichEdit.onKeyPress(evt);
}
RichEdit.prototype.onDivFocus = function()
{
    this.oRichEdit.onFocus();
}
RichEdit.prototype.onDivBlur = function()
{
    this.oRichEdit.onBlur();
}
RichEdit.prototype.onDivClick = function()
{
    if ( !this.oRichEdit.bInFocus )
    {
	this.oRichEdit.setCursorPos();
	this.oRichEdit.focus();
    }
}
RichEdit.prototype.onSpanClick = function()
{
    this.oRichEdit.setCursorPos(this);
    this.oRichEdit.focus();
}
RichEdit.prototype.insertImage = function(src)
{
    // insert a new <img> element at the current position
    var oImg = document.createElement('img');
    oImg.src = src;
    
    this.insertNode(oImg);
}
//</script>
