Click here to Skip to main content
15,885,366 members
Articles / Web Development / XHTML

Integrating FCKeditor in ASP.NET

Rate me:
Please Sign up or sign in to vote.
4.43/5 (15 votes)
20 Jul 2008CPOL2 min read 269.8K   10.1K   64  
Integrating FCKeditor in ASP.NET
/*
 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
 * Copyright (C) 2003-2008 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 ==
 *
 * Implementation for the "Insert/Remove Ordered/Unordered List" commands.
 */

var FCKListCommand = function( name, tagName )
{
	this.Name = name ;
	this.TagName = tagName ;
}

FCKListCommand.prototype =
{
	GetState : function()
	{
		// Disabled if not WYSIWYG.
		if ( FCK.EditMode != FCK_EDITMODE_WYSIWYG || ! FCK.EditorWindow )
			return FCK_TRISTATE_DISABLED ;

		// We'll use the style system's convention to determine list state here...
		// If the starting block is a descendant of an <ol> or <ul> node, then we're in a list.
		var startContainer = FCKSelection.GetBoundaryParentElement( true ) ;
		var listNode = startContainer ;
		while ( listNode )
		{
			if ( listNode.nodeName.IEquals( [ 'ul', 'ol' ] ) )
				break ;
			listNode = listNode.parentNode ;
		}
		if ( listNode && listNode.nodeName.IEquals( this.TagName ) )
			return FCK_TRISTATE_ON ;
		else
			return FCK_TRISTATE_OFF ;
	},

	Execute : function()
	{
		FCKUndo.SaveUndoStep() ;

		var doc = FCK.EditorDocument ;
		var range = new FCKDomRange( FCK.EditorWindow ) ;
		range.MoveToSelection() ;
		var state = this.GetState() ;

		// Midas lists rule #1 says we can create a list even in an empty document.
		// But FCKDomRangeIterator wouldn't run if the document is really empty.
		// So create a paragraph if the document is empty and we're going to create a list.
		if ( state == FCK_TRISTATE_OFF )
		{
			FCKDomTools.TrimNode( doc.body ) ;
			if ( ! doc.body.firstChild )
			{
				var paragraph = doc.createElement( 'p' ) ;
				doc.body.appendChild( paragraph ) ;
				range.MoveToNodeContents( paragraph ) ;
			}
		}

		var bookmark = range.CreateBookmark() ;

		// Group the blocks up because there are many cases where multiple lists have to be created,
		// or multiple lists have to be cancelled.
		var listGroups = [] ;
		var markerObj = {} ;
		var iterator = new FCKDomRangeIterator( range ) ;
		var block ;

		iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ;
		var nextRangeExists = true ;
		var rangeQueue = null ;
		while ( nextRangeExists )
		{
			while ( ( block = iterator.GetNextParagraph() ) )
			{
				var path = new FCKElementPath( block ) ;
				var listNode = null ;
				var processedFlag = false ;
				var blockLimit = path.BlockLimit ;

				// First, try to group by a list ancestor.
				for ( var i = path.Elements.length - 1 ; i >= 0 ; i-- )
				{
					var el = path.Elements[i] ;
					if ( el.nodeName.IEquals( ['ol', 'ul'] ) )
					{
						// If we've encountered a list inside a block limit
						// The last group object of the block limit element should
						// no longer be valid. Since paragraphs after the list
						// should belong to a different group of paragraphs before
						// the list. (Bug #1309)
						if ( blockLimit._FCK_ListGroupObject )
							blockLimit._FCK_ListGroupObject = null ;

						var groupObj = el._FCK_ListGroupObject ;
						if ( groupObj )
							groupObj.contents.push( block ) ;
						else
						{
							groupObj = { 'root' : el, 'contents' : [ block ] } ;
							listGroups.push( groupObj ) ;
							FCKDomTools.SetElementMarker( markerObj, el, '_FCK_ListGroupObject', groupObj ) ;
						}
						processedFlag = true ;
						break ;
					}
				}

				if ( processedFlag )
					continue ;

				// No list ancestor? Group by block limit.
				var root = blockLimit ;
				if ( root._FCK_ListGroupObject )
					root._FCK_ListGroupObject.contents.push( block ) ;
				else
				{
					var groupObj = { 'root' : root, 'contents' : [ block ] } ;
					FCKDomTools.SetElementMarker( markerObj, root, '_FCK_ListGroupObject', groupObj ) ;
					listGroups.push( groupObj ) ;
				}
			}

			if ( FCKBrowserInfo.IsIE )
				nextRangeExists = false ;
			else
			{
				if ( rangeQueue == null )
				{
					rangeQueue = [] ;
					var selectionObject = FCKSelection.GetSelection() ;
					if ( selectionObject && listGroups.length == 0 )
						rangeQueue.push( selectionObject.getRangeAt( 0 ) ) ;
					for ( var i = 1 ; selectionObject && i < selectionObject.rangeCount ; i++ )
						rangeQueue.push( selectionObject.getRangeAt( i ) ) ;
				}
				if ( rangeQueue.length < 1 )
					nextRangeExists = false ;
				else
				{
					var internalRange = FCKW3CRange.CreateFromRange( doc, rangeQueue.shift() ) ;
					range._Range = internalRange ;
					range._UpdateElementInfo() ;
					if ( range.StartNode.nodeName.IEquals( 'td' ) )
						range.SetStart( range.StartNode, 1 ) ;
					if ( range.EndNode.nodeName.IEquals( 'td' ) )
						range.SetEnd( range.EndNode, 2 ) ;
					iterator = new FCKDomRangeIterator( range ) ;
					iterator.ForceBrBreak = ( state == FCK_TRISTATE_OFF ) ;
				}
			}
		}

		// Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
		// We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
		// at the group that's not rooted at lists. So we have three cases to handle.
		var listsCreated = [] ;
		while ( listGroups.length > 0 )
		{
			var groupObj = listGroups.shift() ;
			if ( state == FCK_TRISTATE_OFF )
			{
				if ( groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )
					this._ChangeListType( groupObj, markerObj, listsCreated ) ;
				else
					this._CreateList( groupObj, listsCreated ) ;
			}
			else if ( state == FCK_TRISTATE_ON && groupObj.root.nodeName.IEquals( ['ul', 'ol'] ) )
				this._RemoveList( groupObj, markerObj ) ;
		}

		// For all new lists created, merge adjacent, same type lists.
		for ( var i = 0 ; i < listsCreated.length ; i++ )
		{
			var listNode = listsCreated[i] ;
			var stopFlag = false ;
			var currentNode = listNode ;
			while ( ! stopFlag )
			{
				currentNode = currentNode.nextSibling ;
				if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )
					continue ;
				stopFlag = true ;
			}

			if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )
			{
				currentNode.parentNode.removeChild( currentNode ) ;
				while ( currentNode.firstChild )
					listNode.appendChild( currentNode.removeChild( currentNode.firstChild ) ) ;
			}

			stopFlag = false ;
			currentNode = listNode ;
			while ( ! stopFlag )
			{
				currentNode = currentNode.previousSibling ;
				if ( currentNode && currentNode.nodeType == 3 && currentNode.nodeValue.search( /^[\n\r\t ]*$/ ) == 0 )
					continue ;
				stopFlag = true ;
			}
			if ( currentNode && currentNode.nodeName.IEquals( this.TagName ) )
			{
				currentNode.parentNode.removeChild( currentNode ) ;
				while ( currentNode.lastChild )
					listNode.insertBefore( currentNode.removeChild( currentNode.lastChild ),
						       listNode.firstChild ) ;
			}
		}

		// Clean up, restore selection and update toolbar button states.
		FCKDomTools.ClearAllMarkers( markerObj ) ;
		range.MoveToBookmark( bookmark ) ;
		range.Select() ;

		FCK.Focus() ;
		FCK.Events.FireEvent( 'OnSelectionChange' ) ;
	},

	_ChangeListType : function( groupObj, markerObj, listsCreated )
	{
		// This case is easy...
		// 1. Convert the whole list into a one-dimensional array.
		// 2. Change the list type by modifying the array.
		// 3. Recreate the whole list by converting the array to a list.
		// 4. Replace the original list with the recreated list.
		var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ;
		var selectedListItems = [] ;
		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
		{
			var itemNode = groupObj.contents[i] ;
			itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;
			if ( ! itemNode || itemNode._FCK_ListItem_Processed )
				continue ;
			selectedListItems.push( itemNode ) ;
			FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;
		}
		var fakeParent = FCKTools.GetElementDocument( groupObj.root ).createElement( this.TagName ) ;
		for ( var i = 0 ; i < selectedListItems.length ; i++ )
		{
			var listIndex = selectedListItems[i]._FCK_ListArray_Index ;
			listArray[listIndex].parent = fakeParent ;
		}
		var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ;
		for ( var i = 0 ; i < newList.listNode.childNodes.length ; i++ )
		{
			if ( newList.listNode.childNodes[i].nodeName.IEquals( this.TagName ) )
				listsCreated.push( newList.listNode.childNodes[i] ) ;
		}
		groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;
	},

	_CreateList : function( groupObj, listsCreated )
	{
		var contents = groupObj.contents ;
		var doc = FCKTools.GetElementDocument( groupObj.root ) ;
		var listContents = [] ;

		// It is possible to have the contents returned by DomRangeIterator to be the same as the root.
		// e.g. when we're running into table cells.
		// In such a case, enclose the childNodes of contents[0] into a <div>.
		if ( contents.length == 1 && contents[0] == groupObj.root )
		{
			var divBlock = doc.createElement( 'div' );
			while ( contents[0].firstChild )
				divBlock.appendChild( contents[0].removeChild( contents[0].firstChild ) ) ;
			contents[0].appendChild( divBlock ) ;
			contents[0] = divBlock ;
		}

		// Calculate the common parent node of all content blocks.
		var commonParent = groupObj.contents[0].parentNode ;
		for ( var i = 0 ; i < contents.length ; i++ )
			commonParent = FCKDomTools.GetCommonParents( commonParent, contents[i].parentNode ).pop() ;

		// We want to insert things that are in the same tree level only, so calculate the contents again
		// by expanding the selected blocks to the same tree level.
		for ( var i = 0 ; i < contents.length ; i++ )
		{
			var contentNode = contents[i] ;
			while ( contentNode.parentNode )
			{
				if ( contentNode.parentNode == commonParent )
				{
					listContents.push( contentNode ) ;
					break ;
				}
				contentNode = contentNode.parentNode ;
			}
		}

		if ( listContents.length < 1 )
			return ;

		// Insert the list to the DOM tree.
		var insertAnchor = listContents[listContents.length - 1].nextSibling ;
		var listNode = doc.createElement( this.TagName ) ;
		listsCreated.push( listNode ) ;
		while ( listContents.length )
		{
			var contentBlock = listContents.shift() ;
			var docFrag = doc.createDocumentFragment() ;
			while ( contentBlock.firstChild )
				docFrag.appendChild( contentBlock.removeChild( contentBlock.firstChild ) ) ;
			contentBlock.parentNode.removeChild( contentBlock ) ;
			var listItem = doc.createElement( 'li' ) ;
			listItem.appendChild( docFrag ) ;
			listNode.appendChild( listItem ) ;
		}
		commonParent.insertBefore( listNode, insertAnchor ) ;
	},

	_RemoveList : function( groupObj, markerObj )
	{
		// This is very much like the change list type operation.
		// Except that we're changing the selected items' indent to -1 in the list array.
		var listArray = FCKDomTools.ListToArray( groupObj.root, markerObj ) ;
		var selectedListItems = [] ;
		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
		{
			var itemNode = groupObj.contents[i] ;
			itemNode = FCKTools.GetElementAscensor( itemNode, 'li' ) ;
			if ( ! itemNode || itemNode._FCK_ListItem_Processed )
				continue ;
			selectedListItems.push( itemNode ) ;
			FCKDomTools.SetElementMarker( markerObj, itemNode, '_FCK_ListItem_Processed', true ) ;
		}

		var lastListIndex = null ;
		for ( var i = 0 ; i < selectedListItems.length ; i++ )
		{
			var listIndex = selectedListItems[i]._FCK_ListArray_Index ;
			listArray[listIndex].indent = -1 ;
			lastListIndex = listIndex ;
		}

		// After cutting parts of the list out with indent=-1, we still have to maintain the array list
		// model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
		// list cannot be converted back to a real DOM list.
		for ( var i = lastListIndex + 1; i < listArray.length ; i++ )
		{
			if ( listArray[i].indent > listArray[i-1].indent + 1 )
			{
				var indentOffset = listArray[i-1].indent + 1 - listArray[i].indent ;
				var oldIndent = listArray[i].indent ;
				while ( listArray[i] && listArray[i].indent >= oldIndent)
				{
					listArray[i].indent += indentOffset ;
					i++ ;
				}
				i-- ;
			}
		}

		var newList = FCKDomTools.ArrayToList( listArray, markerObj ) ;
		// If groupObj.root is the last element in its parent, or its nextSibling is a <br>, then we should
		// not add a <br> after the final item. So, check for the cases and trim the <br>.
		if ( groupObj.root.nextSibling == null || groupObj.root.nextSibling.nodeName.IEquals( 'br' ) )
		{
			if ( newList.listNode.lastChild.nodeName.IEquals( 'br' ) )
				newList.listNode.removeChild( newList.listNode.lastChild ) ;
		}
		groupObj.root.parentNode.replaceChild( newList.listNode, groupObj.root ) ;
	}
};

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Bangladesh Bangladesh
Software Engineer, Bangladesh.

Comments and Discussions