text: ﻿/*
   * FCKeditor - The text editor for Internet - http://www.fckeditor.net
   * Copyright (C) 2003-2009 Frederico Caldeira Knabben
   *
   * == BEGIN LICENSE ==
   *
   * Licensed under the terms of any of the following licenses at your
   * choice:
   *
   *  - GNU General Public License Version 2 or later (the "GPL")
   *    http://www.gnu.org/licenses/gpl.html
   *
   *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
   *    http://www.gnu.org/licenses/lgpl.html
   *
   *  - Mozilla Public License Version 1.1 or later (the "MPL")
   *    http://www.mozilla.org/MPL/MPL-1.1.html
   *
   * == END LICENSE ==
   *
   * Creation and initialization of the "FCK" object. This is the main
   * object that represents an editor instance.
   * (Gecko specific implementations)
   */
  
  FCK.Description = "FCKeditor for Gecko Browsers" ;
  
  FCK.InitializeBehaviors = function()
  {
  	// When calling "SetData", the editing area IFRAME gets a fixed height. So we must recalculate it.
  	if ( window.onresize )		// Not for Safari/Opera.
  		window.onresize() ;
  
  	FCKFocusManager.AddWindow( this.EditorWindow ) ;
  
  	this.ExecOnSelectionChange = function()
  	{
  		FCK.Events.FireEvent( "OnSelectionChange" ) ;
  	}
  
  	this._ExecDrop = function( evt )
  	{
  		if ( FCK.MouseDownFlag )
  		{
  			FCK.MouseDownFlag = false ;
  			return ;
  		}
  
  		if ( FCKConfig.ForcePasteAsPlainText )
  		{
  			if ( evt.dataTransfer )
  			{
  				var text = evt.dataTransfer.getData( 'Text' ) ;
  				text = FCKTools.HTMLEncode( text ) ;
  				text = FCKTools.ProcessLineBreaks( window, FCKConfig, text ) ;
  				FCK.InsertHtml( text ) ;
  			}
  			else if ( FCKConfig.ShowDropDialog )
  				FCK.PasteAsPlainText() ;
  
  			evt.preventDefault() ;
  			evt.stopPropagation() ;
  		}
  	}
  
  	this._ExecCheckCaret = function( evt )
  	{
  		if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG )
  			return ;
  
  		if ( evt.type == 'keypress' )
  		{
  			var keyCode = evt.keyCode ;
  			// ignore if positioning key is not pressed.
  			// left or up arrow keys need to be processed as well, since <a> links can be expanded in Gecko's editor
  			// when the caret moved left or up from another block element below.
  			if ( keyCode < 33 || keyCode > 40 )
  				return ;
  		}
  
  		var blockEmptyStop = function( node )
  		{
  			if ( node.nodeType != 1 )
  				return false ;
  			var tag = node.tagName.toLowerCase() ;
  			return ( FCKListsLib.BlockElements[tag] || FCKListsLib.EmptyElements[tag] ) ;
  		}
  
  		var moveCursor = function()
  		{
  			var selection = FCKSelection.GetSelection() ;
  			var range = selection.getRangeAt(0) ;
  			if ( ! range || ! range.collapsed )
  				return ;
  
  			var node = range.endContainer ;
  
  			// only perform the patched behavior if we're at the end of a text node.
  			if ( node.nodeType != 3 )
  				return ;
  
  			if ( node.nodeValue.length != range.endOffset )
  				return ;
  
  			// only perform the patched behavior if we're in an <a> tag, or the End key is pressed.
  			var parentTag = node.parentNode.tagName.toLowerCase() ;
  			if ( ! (  parentTag == 'a' || ( !FCKBrowserInfo.IsOpera && String(node.parentNode.contentEditable) == 'false' ) ||
  					( ! ( FCKListsLib.BlockElements[parentTag] || FCKListsLib.NonEmptyBlockElements[parentTag] )
  					  && keyCode == 35 ) ) )
  				return ;
  
  			// our caret has moved to just after the last character of a text node under an unknown tag, how to proceed?
  			// first, see if there are other text nodes by DFS walking from this text node.
  			// 	- if the DFS has scanned all nodes under my parent, then go the next step.
  			//	- if there is a text node after me but still under my parent, then do nothing and return.
  			var nextTextNode = FCKTools.GetNextTextNode( node, node.parentNode, blockEmptyStop ) ;
  			if ( nextTextNode )
  				return ;
  
  			// we're pretty sure we need to move the caret forcefully from here.
  			range = FCK.EditorDocument.createRange() ;
  
  			nextTextNode = FCKTools.GetNextTextNode( node, node.parentNode.parentNode, blockEmptyStop ) ;
  			if ( nextTextNode )
  			{
  				// Opera thinks the dummy empty text node we append beyond the end of <a> nodes occupies a caret
  				// position. So if the user presses the left key and we reset the caret position here, the user
  				// wouldn't be able to go back.
  				if ( FCKBrowserInfo.IsOpera && keyCode == 37 )
  					return ;
  
  				// now we want to get out of our current parent node, adopt the next parent, and move the caret to
  				// the appropriate text node under our new parent.
  				// our new parent might be our current parent's siblings if we are lucky.
  				range.setStart( nextTextNode, 0 ) ;
  				range.setEnd( nextTextNode, 0 ) ;
  			}
  			else
  			{
  				// no suitable next siblings under our grandparent! what to do next?
  				while ( node.parentNode
  					&& node.parentNode != FCK.EditorDocument.body
  					&& node.parentNode != FCK.EditorDocument.documentElement
  					&& node == node.parentNode.lastChild
  					&& ( ! FCKListsLib.BlockElements[node.parentNode.tagName.toLowerCase()]
  					  && ! FCKListsLib.NonEmptyBlockElements[node.parentNode.tagName.toLowerCase()] ) )
  					node = node.parentNode ;
  
  
  				if ( FCKListsLib.BlockElements[ parentTag ]
  						|| FCKListsLib.EmptyElements[ parentTag ]
  						|| node == FCK.EditorDocument.body )
  				{
  					// if our parent is a block node, move to the end of our parent.
  					range.setStart( node, node.childNodes.length ) ;
  					range.setEnd( node, node.childNodes.length ) ;
  				}
  				else
  				{
  					// things are a little bit more interesting if our parent is not a block node
  					// due to the weired ways how Gecko's caret acts...
  					var stopNode = node.nextSibling ;
  
  					// find out the next block/empty element at our grandparent, we'll
  					// move the caret just before it.
  					while ( stopNode )
  					{
  						if ( stopNode.nodeType != 1 )
  						{
  							stopNode = stopNode.nextSibling ;
  							continue ;
  						}
  
  						var stopTag = stopNode.tagName.toLowerCase() ;
  						if ( FCKListsLib.BlockElements[stopTag] || FCKListsLib.EmptyElements[stopTag]
  							|| FCKListsLib.NonEmptyBlockElements[stopTag] )
  							break ;
  						stopNode = stopNode.nextSibling ;
  					}
  
  					// note that the dummy marker below is NEEDED, otherwise the caret's behavior will
  					// be broken in Gecko.
  					var marker = FCK.EditorDocument.createTextNode( '' ) ;
  					if ( stopNode )
  						node.parentNode.insertBefore( marker, stopNode ) ;
  					else
  						node.parentNode.appendChild( marker ) ;
  					range.setStart( marker, 0 ) ;
  					range.setEnd( marker, 0 ) ;
  				}
  			}
  
  			selection.removeAllRanges() ;
  			selection.addRange( range ) ;
  			FCK.Events.FireEvent( "OnSelectionChange" ) ;
  		}
  
  		setTimeout( moveCursor, 1 ) ;
  	}
  
  	this.ExecOnSelectionChangeTimer = function()
  	{
  		if ( FCK.LastOnChangeTimer )
  			window.clearTimeout( FCK.LastOnChangeTimer ) ;
  
  		FCK.LastOnChangeTimer = window.setTimeout( FCK.ExecOnSelectionChange, 100 ) ;
  	}
  
  	this.EditorDocument.addEventListener( 'mouseup', this.ExecOnSelectionChange, false ) ;
  
  	// On Gecko, firing the "OnSelectionChange" event on every key press started to be too much
  	// slow. So, a timer has been implemented to solve performance issues when typing to quickly.
  	this.EditorDocument.addEventListener( 'keyup', this.ExecOnSelectionChangeTimer, false ) ;
  
  	this._DblClickListener = function( e )
  	{
  		FCK.OnDoubleClick( e.target ) ;
  		e.stopPropagation() ;
  	}
  	this.EditorDocument.addEventListener( 'dblclick', this._DblClickListener, true ) ;
  
  	// Record changes for the undo system when there are key down events.
  	this.EditorDocument.addEventListener( 'keydown', this._KeyDownListener, false ) ;
  
  	// Hooks for data object drops
  	if ( FCKBrowserInfo.IsGecko )
  	{
  		this.EditorWindow.addEventListener( 'dragdrop', this._ExecDrop, true ) ;
  	}
  	else if ( FCKBrowserInfo.IsSafari )
  	{
  		this.EditorDocument.addEventListener( 'dragover', function ( evt )
  				{ if ( !FCK.MouseDownFlag && FCK.Config.ForcePasteAsPlainText ) evt.returnValue = false ; }, true ) ;
  		this.EditorDocument.addEventListener( 'drop', this._ExecDrop, true ) ;
  		this.EditorDocument.addEventListener( 'mousedown',
  			function( ev )
  			{
  				var element = ev.srcElement ;
  
  				if ( element.nodeName.IEquals( 'IMG', 'HR', 'INPUT', 'TEXTAREA', 'SELECT' ) )
  				{
  					FCKSelection.SelectNode( element ) ;
  				}
  			}, true ) ;
  
  		this.EditorDocument.addEventListener( 'mouseup',
  			function( ev )
  			{
  				if ( ev.srcElement.nodeName.IEquals( 'INPUT', 'TEXTAREA', 'SELECT' ) )
  					ev.preventDefault()
  			}, true ) ;
  
  		this.EditorDocument.addEventListener( 'click',
  			function( ev )
  			{
  				if ( ev.srcElement.nodeName.IEquals( 'INPUT', 'TEXTAREA', 'SELECT' ) )
  					ev.preventDefault()
  			}, true ) ;
  	}
  
  	// Kludge for buggy Gecko caret positioning logic (Bug #393 and #1056)
  	if ( FCKBrowserInfo.IsGecko || FCKBrowserInfo.IsOpera )
  	{
  		this.EditorDocument.addEventListener( 'keypress', this._ExecCheckCaret, false ) ;
  		this.EditorDocument.addEventListener( 'click', this._ExecCheckCaret, false ) ;
  	}
  
  	// Reset the context menu.
  	FCK.ContextMenu._InnerContextMenu.SetMouseClickWindow( FCK.EditorWindow ) ;
  	FCK.ContextMenu._InnerContextMenu.AttachToElement( FCK.EditorDocument ) ;
  }
  
  FCK.MakeEditable = function()
  {
  	this.EditingArea.MakeEditable() ;
  }
  
  // Disable the context menu in the editor (outside the editing area).
  function Document_OnContextMenu( e )
  {
  	if ( !e.target._FCKShowContextMenu )
  		e.preventDefault() ;
  }
  document.oncontextmenu = Document_OnContextMenu ;
  
  // GetNamedCommandState overload for Gecko.
  FCK._BaseGetNamedCommandState = FCK.GetNamedCommandState ;
  FCK.GetNamedCommandState = function( commandName )
  {
  	switch ( commandName )
  	{
  		case 'Unlink' :
  			return FCKSelection.HasAncestorNode('A') ? FCK_TRISTATE_OFF : FCK_TRISTATE_DISABLED ;
  		default :
  			return FCK._BaseGetNamedCommandState( commandName ) ;
  	}
  }
  
  // Named commands to be handled by this browsers specific implementation.
  FCK.RedirectNamedCommands =
  {
  	Print	: true,
  	Paste	: true
  } ;
  
  // ExecuteNamedCommand overload for Gecko.
  FCK.ExecuteRedirectedNamedCommand = function( commandName, commandParameter )
  {
  	switch ( commandName )
  	{
  		case 'Print' :
  			FCK.EditorWindow.print() ;
  			break ;
  		case 'Paste' :
  			try
  			{
  				// Force the paste dialog for Safari (#50).
  				if ( FCKBrowserInfo.IsSafari )
  					throw '' ;
  
  				if ( FCK.Paste() )
  					FCK.ExecuteNamedCommand( 'Paste', null, true ) ;
  			}
  			catch (e)	{
  				if ( FCKConfig.ForcePasteAsPlainText )
  					FCK.PasteAsPlainText() ;
  				else
  					FCKDialog.OpenDialog( 'FCKDialog_Paste', FCKLang.Paste, 'dialog/fck_paste.html', 400, 330, 'Security' ) ;
  			}
  			break ;
  		default :
  			FCK.ExecuteNamedCommand( commandName, commandParameter ) ;
  	}
  }
  
  FCK._ExecPaste = function()
  {
  	// Save a snapshot for undo before actually paste the text
  	FCKUndo.SaveUndoStep() ;
  
  	if ( FCKConfig.ForcePasteAsPlainText )
  	{
  		FCK.PasteAsPlainText() ;
  		return false ;
  	}
  
  	/* For now, the AutoDetectPasteFromWord feature is IE only. */
  	return true ;
  }
  
  //**
  // FCK.InsertHtml: Inserts HTML at the current cursor location. Deletes the
  // selected content if any.
  FCK.InsertHtml = function( html )
  {
  	var doc = FCK.EditorDocument,
  		range;
  
  	html = FCKConfig.ProtectedSource.Protect( html ) ;
  	html = FCK.ProtectEvents( html ) ;
  	html = FCK.ProtectUrls( html ) ;
  	html = FCK.ProtectTags( html ) ;
  
  	// Save an undo snapshot first.
  	FCKUndo.SaveUndoStep() ;
  
  	if ( FCKBrowserInfo.IsGecko )
  	{
  		html = html.replace( /&nbsp;$/, '$&<span _fcktemp="1"/>' ) ;
  
  		var docFrag = new FCKDocumentFragment( this.EditorDocument ) ;
  		docFrag.AppendHtml( html ) ;
  
  		var lastNode = docFrag.RootNode.lastChild ;
  
  		range = new FCKDomRange( this.EditorWindow ) ;
  		range.MoveToSelection() ;
  		range.DeleteContents() ;
  		range.InsertNode( docFrag.RootNode ) ;
  
  		range.MoveToPosition( lastNode, 4 ) ;
  	}
  	else
  		doc.execCommand( 'inserthtml', false, html ) ;
  
  	this.Focus() ;
  
  	// Save the caret position before calling document processor.
  	if ( !range )
  	{
  		range = new FCKDomRange( this.EditorWindow ) ;
  		range.MoveToSelection() ;
  	}
  	var bookmark = range.CreateBookmark() ;
  
  	FCKDocumentProcessor.Process( doc ) ;
  
  	// Restore caret position, ignore any errors in case the document
  	// processor removed the bookmark <span>s for some reason.
  	try
  	{
  		range.MoveToBookmark( bookmark ) ;
  		range.Select() ;
  	}
  	catch ( e ) {}
  
  	// For some strange reason the SaveUndoStep() call doesn't activate the undo button at the first InsertHtml() call.
  	this.Events.FireEvent( "OnSelectionChange" ) ;
  }
  
  FCK.PasteAsPlainText = function()
  {
  	// TODO: Implement the "Paste as Plain Text" code.
  
  	// If the function is called immediately Firefox 2 does automatically paste the contents as soon as the new dialog is created
  	// so we run it in a Timeout and the paste event can be cancelled
  	FCKTools.RunFunction( FCKDialog.OpenDialog, FCKDialog, ['FCKDialog_Paste', FCKLang.PasteAsText, 'dialog/fck_paste.html', 400, 330, 'PlainText'] ) ;
  
  /*
  	var sText = FCKTools.HTMLEncode( clipboardData.getData("Text") ) ;
  	sText = sText.replace( /\n/g, '<BR>' ) ;
  	this.InsertHtml( sText ) ;
  */
  }
  /*
  FCK.PasteFromWord = function()
  {
  	// TODO: Implement the "Paste as Plain Text" code.
  
  	FCKDialog.OpenDialog( 'FCKDialog_Paste', FCKLang.PasteFromWord, 'dialog/fck_paste.html', 400, 330, 'Word' ) ;
  
  //	FCK.CleanAndPaste( FCK.GetClipboardHTML() ) ;
  }
  */
  FCK.GetClipboardHTML = function()
  {
  	return '' ;
  }
  
  FCK.CreateLink = function( url, noUndo )
  {
  	// Creates the array that will be returned. It contains one or more created links (see #220).
  	var aCreatedLinks = new Array() ;
  
  	// Only for Safari, a collapsed selection may create a link. All other
  	// browser will have no links created. So, we check it here and return
  	// immediatelly, having the same cross browser behavior.
  	if ( FCKSelection.GetSelection().isCollapsed )
  		return aCreatedLinks ;
  
  	FCK.ExecuteNamedCommand( 'Unlink', null, false, !!noUndo ) ;
  
  	if ( url.length > 0 )
  	{
  		// Generate a temporary name for the link.
  		var sTempUrl = 'javascript:void(0);/*' + ( new Date().getTime() ) + '*/' ;
  
  		// Use the internal "CreateLink" command to create the link.
  		FCK.ExecuteNamedCommand( 'CreateLink', sTempUrl, false, !!noUndo ) ;
  
  		// Retrieve the just created links using XPath.
  		var oLinksInteractor = this.EditorDocument.evaluate("//a[@href='" + sTempUrl + "']", this.EditorDocument.body, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null) ;
  
  		// Add all links to the returning array.
  		for ( var i = 0 ; i < oLinksInteractor.snapshotLength ; i++ )
  		{
  			var oLink = oLinksInteractor.snapshotItem( i ) ;
  			oLink.href = url ;
  
  			aCreatedLinks.push( oLink ) ;
  		}
  	}
  
  	return aCreatedLinks ;
  }
  
  FCK._FillEmptyBlock = function( emptyBlockNode )
  {
  	if ( ! emptyBlockNode || emptyBlockNode.nodeType != 1 )
  		return ;
  	var nodeTag = emptyBlockNode.tagName.toLowerCase() ;
  	if ( nodeTag != 'p' && nodeTag != 'div' )
  		return ;
  	if ( emptyBlockNode.firstChild )
  		return ;
  	FCKTools.AppendBogusBr( emptyBlockNode ) ;
  }
  
  FCK._ExecCheckEmptyBlock = function()
  {
  	FCK._FillEmptyBlock( FCK.EditorDocument.body.firstChild ) ;
  	var sel = FCKSelection.GetSelection() ;
  	if ( !sel || sel.rangeCount < 1 )
  		return ;
  	var range = sel.getRangeAt( 0 );
  	FCK._FillEmptyBlock( range.startContainer ) ;
  }
  