Click here to Skip to main content
15,894,017 members
Articles / Web Development / ASP.NET

How to Integrate CKEditor with ASP.NET

Rate me:
Please Sign up or sign in to vote.
4.95/5 (20 votes)
22 Jan 2013CPOL3 min read 159.1K   16.9K   19  
This is meant for easy integration of CKEditor with ASP.NET
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/

/**
 * @file Insert and remove numbered and bulleted lists.
 */

(function()
{
	var listNodeNames = { ol : 1, ul : 1 },
		emptyTextRegex = /^[\n\r\t ]*$/;

	var whitespaces = CKEDITOR.dom.walker.whitespaces(),
		bookmarks = CKEDITOR.dom.walker.bookmark(),
		nonEmpty = function( node ){ return !( whitespaces( node ) || bookmarks( node ) );},
		blockBogus = CKEDITOR.dom.walker.bogus();

	function cleanUpDirection( element )
	{
		var dir, parent, parentDir;
		if ( ( dir = element.getDirection() ) )
		{
			parent = element.getParent();
			while ( parent && !( parentDir = parent.getDirection() ) )
				parent = parent.getParent();

			if ( dir == parentDir )
				element.removeAttribute( 'dir' );
		}
	}

	// Inheirt inline styles from another element.
	function inheirtInlineStyles( parent, el )
	{
		var style = parent.getAttribute( 'style' );

		// Put parent styles before child styles.
		style && el.setAttribute( 'style',
			style.replace( /([^;])$/, '$1;' ) +
			( el.getAttribute( 'style' ) || '' ) );
	}

	CKEDITOR.plugins.list = {
		/*
		 * Convert a DOM list tree into a data structure that is easier to
		 * manipulate. This operation should be non-intrusive in the sense that it
		 * does not change the DOM tree, with the exception that it may add some
		 * markers to the list item nodes when database is specified.
		 */
		listToArray : function( listNode, database, baseArray, baseIndentLevel, grandparentNode )
		{
			if ( !listNodeNames[ listNode.getName() ] )
				return [];

			if ( !baseIndentLevel )
				baseIndentLevel = 0;
			if ( !baseArray )
				baseArray = [];

			// Iterate over all list items to and look for inner lists.
			for ( var i = 0, count = listNode.getChildCount() ; i < count ; i++ )
			{
				var listItem = listNode.getChild( i );

				// Fixing malformed nested lists by moving it into a previous list item. (#6236)
				if( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list )
					CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 );

				// It may be a text node or some funny stuff.
				if ( listItem.$.nodeName.toLowerCase() != 'li' )
					continue;

				var itemObj = { 'parent' : listNode, indent : baseIndentLevel, element : listItem, contents : [] };
				if ( !grandparentNode )
				{
					itemObj.grandparent = listNode.getParent();
					if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' )
						itemObj.grandparent = itemObj.grandparent.getParent();
				}
				else
					itemObj.grandparent = grandparentNode;

				if ( database )
					CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );
				baseArray.push( itemObj );

				for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount ; j++ )
				{
					child = listItem.getChild( j );
					if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] )
						// Note the recursion here, it pushes inner list items with
						// +1 indentation in the correct order.
						CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent );
					else
						itemObj.contents.push( child );
				}
			}
			return baseArray;
		},

		// Convert our internal representation of a list back to a DOM forest.
		arrayToList : function( listArray, database, baseIndex, paragraphMode, dir )
		{
			if ( !baseIndex )
				baseIndex = 0;
			if ( !listArray || listArray.length < baseIndex + 1 )
				return null;
			var i,
				doc = listArray[ baseIndex ].parent.getDocument(),
				retval = new CKEDITOR.dom.documentFragment( doc ),
				rootNode = null,
				currentIndex = baseIndex,
				indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),
				currentListItem = null,
				orgDir,
				block,
				paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
			while ( 1 )
			{
				var item = listArray[ currentIndex ],
					itemGrandParent = item.grandparent;

				orgDir = item.element.getDirection( 1 );

				if ( item.indent == indentLevel )
				{
					if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() )
					{
						rootNode = listArray[ currentIndex ].parent.clone( false, 1 );
						dir && rootNode.setAttribute( 'dir', dir );
						retval.append( rootNode );
					}
					currentListItem = rootNode.append( item.element.clone( 0, 1 ) );

					if ( orgDir != rootNode.getDirection( 1 ) )
						currentListItem.setAttribute( 'dir', orgDir );

					for ( i = 0 ; i < item.contents.length ; i++ )
						currentListItem.append( item.contents[i].clone( 1, 1 ) );
					currentIndex++;
				}
				else if ( item.indent == Math.max( indentLevel, 0 ) + 1 )
				{
					// Maintain original direction (#6861).
					var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ),
						listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode,
						currDir != orgDir ? orgDir: null );

					// If the next block is an <li> with another list tree as the first
					// child, we'll need to append a filler (<br>/NBSP) or the list item
					// wouldn't be editable. (#6724)
					if ( !currentListItem.getChildCount() && CKEDITOR.env.ie && !( doc.$.documentMode > 7 ))
						currentListItem.append( doc.createText( '\xa0' ) );
					currentListItem.append( listData.listNode );
					currentIndex = listData.nextIndex;
				}
				else if ( item.indent == -1 && !baseIndex && itemGrandParent )
				{
					if ( listNodeNames[ itemGrandParent.getName() ] )
					{
						currentListItem = item.element.clone( false, true );
						if ( orgDir != itemGrandParent.getDirection( 1 ) )
							currentListItem.setAttribute( 'dir', orgDir );
					}
					else
						currentListItem = new CKEDITOR.dom.documentFragment( doc );

					// Migrate all children to the new container,
					// apply the proper text direction.
					var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir,
						li = item.element,
						className = li.getAttribute( 'class' ),
						style = li.getAttribute( 'style' );

					var needsBlock = currentListItem.type ==
					                 CKEDITOR.NODE_DOCUMENT_FRAGMENT &&
					                 ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose ||
					                   style || className );

					var child, count = item.contents.length;
					for ( i = 0 ; i < count; i++ )
					{
						child = item.contents[ i ];

						if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() )
						{
							// Apply direction on content blocks.
							if ( dirLoose && !child.getDirection() )
								child.setAttribute( 'dir', orgDir );

							inheirtInlineStyles( li, child );

							className && child.addClass( className );
						}
						else if ( needsBlock )
						{
							// Establish new block to hold text direction and styles.
							if ( !block )
							{
								block = doc.createElement( paragraphName );
								dirLoose && block.setAttribute( 'dir', orgDir );
							}

							// Copy over styles to new block;
							style && block.setAttribute( 'style', style );
							className && block.setAttribute( 'class', className );

							block.append( child.clone( 1, 1 ) );
						}

						currentListItem.append( block || child.clone( 1, 1 ) );
					}

					if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT
						 && currentIndex != listArray.length - 1 )
					{
						var last = currentListItem.getLast();
						if ( last && last.type == CKEDITOR.NODE_ELEMENT
								&& last.getAttribute( 'type' ) == '_moz' )
						{
							last.remove();
						}

						if ( !( last = currentListItem.getLast( nonEmpty )
							&& last.type == CKEDITOR.NODE_ELEMENT
							&& last.getName() in CKEDITOR.dtd.$block ) )
						{
							currentListItem.append( doc.createElement( 'br' ) );
						}
					}

					var currentListItemName = currentListItem.$.nodeName.toLowerCase();
					if ( !CKEDITOR.env.ie && ( currentListItemName == 'div' || currentListItemName == 'p' ) )
						currentListItem.appendBogus();
					retval.append( currentListItem );
					rootNode = null;
					currentIndex++;
				}
				else
					return null;

				block = null;

				if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )
					break;
			}

			if ( database )
			{
				var currentNode = retval.getFirst(),
					listRoot = listArray[ 0 ].parent;

				while ( currentNode )
				{
					if ( currentNode.type == CKEDITOR.NODE_ELEMENT )
					{
						// Clear marker attributes for the new list tree made of cloned nodes, if any.
						CKEDITOR.dom.element.clearMarkers( database, currentNode );

						// Clear redundant direction attribute specified on list items.
						if ( currentNode.getName() in CKEDITOR.dtd.$listItem )
							cleanUpDirection( currentNode );
					}

					currentNode = currentNode.getNextSourceNode();
				}
			}

			return { listNode : retval, nextIndex : currentIndex };
		}
	};

	function onSelectionChange( evt )
	{
		if ( evt.editor.readOnly )
			return null;

		var path = evt.data.path,
			blockLimit = path.blockLimit,
			elements = path.elements,
			element,
			i;

		// Grouping should only happen under blockLimit.(#3940).
		for ( i = 0 ; i < elements.length && ( element = elements[ i ] )
			  && !element.equals( blockLimit ); i++ )
		{
			if ( listNodeNames[ elements[ i ].getName() ] )
				return this.setState( this.type == elements[ i ].getName() ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
		}

		return this.setState( CKEDITOR.TRISTATE_OFF );
	}

	function changeListType( editor, groupObj, database, 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 = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
			selectedListItems = [];

		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
		{
			var itemNode = groupObj.contents[i];
			itemNode = itemNode.getAscendant( 'li', true );
			if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
				continue;
			selectedListItems.push( itemNode );
			CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
		}

		var root = groupObj.root,
			doc = root.getDocument(),
			listNode,
			newListNode;

		for ( i = 0 ; i < selectedListItems.length ; i++ )
		{
			var listIndex = selectedListItems[i].getCustomData( 'listarray_index' );
			listNode = listArray[ listIndex ].parent;

			// Switch to new list node for this particular item.
			if ( !listNode.is( this.type ) )
			{
				newListNode = doc.createElement( this.type );
				// Copy all attributes, except from 'start' and 'type'.
				listNode.copyAttributes( newListNode, { start : 1, type : 1 } );
				// The list-style-type property should be ignored.
				newListNode.removeStyle( 'list-style-type' );
				listArray[ listIndex ].parent = newListNode;
			}
		}

		var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );
		var child, length = newList.listNode.getChildCount();
		for ( i = 0 ; i < length && ( child = newList.listNode.getChild( i ) ) ; i++ )
		{
			if ( child.getName() == this.type )
				listsCreated.push( child );
		}
		newList.listNode.replace( groupObj.root );
	}

	var headerTagRegex = /^h[1-6]$/;

	function createList( editor, groupObj, listsCreated )
	{
		var contents = groupObj.contents,
			doc = groupObj.root.getDocument(),
			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].equals( groupObj.root ) )
		{
			var divBlock = doc.createElement( 'div' );
			contents[0].moveChildren && contents[0].moveChildren( divBlock );
			contents[0].append( divBlock );
			contents[0] = divBlock;
		}

		// Calculate the common parent node of all content blocks.
		var commonParent = groupObj.contents[0].getParent();
		for ( var i = 0 ; i < contents.length ; i++ )
			commonParent = commonParent.getCommonAncestor( contents[i].getParent() );

		var useComputedState = editor.config.useComputedState,
			listDir, explicitDirection;

		useComputedState = useComputedState === undefined || useComputedState;

		// 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 ( i = 0 ; i < contents.length ; i++ )
		{
			var contentNode = contents[i],
				parentNode;
			while ( ( parentNode = contentNode.getParent() ) )
			{
				if ( parentNode.equals( commonParent ) )
				{
					listContents.push( contentNode );

					// Determine the lists's direction.
					if ( !explicitDirection && contentNode.getDirection() )
						explicitDirection = 1;

					var itemDir = contentNode.getDirection( useComputedState );

					if ( listDir !== null )
					{
						// If at least one LI have a different direction than current listDir, we can't have listDir.
						if ( listDir && listDir != itemDir )
							listDir = null;
						else
							listDir = itemDir;
					}

					break;
				}
				contentNode = parentNode;
			}
		}

		if ( listContents.length < 1 )
			return;

		// Insert the list to the DOM tree.
		var insertAnchor = listContents[ listContents.length - 1 ].getNext(),
			listNode = doc.createElement( this.type );

		listsCreated.push( listNode );

		var contentBlock, listItem;

		while ( listContents.length )
		{
			contentBlock = listContents.shift();
			listItem = doc.createElement( 'li' );

			// Preserve preformat block and heading structure when converting to list item. (#5335) (#5271)
			if ( contentBlock.is( 'pre' ) || headerTagRegex.test( contentBlock.getName() ) )
				contentBlock.appendTo( listItem );
			else
			{
				contentBlock.copyAttributes( listItem );
				// Remove direction attribute after it was merged into list root. (#7657)
				if ( listDir && contentBlock.getDirection() )
				{
					listItem.removeStyle( 'direction' );
					listItem.removeAttribute( 'dir' );
				}
				contentBlock.moveChildren( listItem );
				contentBlock.remove();
			}

			listItem.appendTo( listNode );
		}

		// Apply list root dir only if it has been explicitly declared.
		if ( listDir && explicitDirection )
			listNode.setAttribute( 'dir', listDir );

		if ( insertAnchor )
			listNode.insertBefore( insertAnchor );
		else
			listNode.appendTo( commonParent );
	}

	function removeList( editor, groupObj, database )
	{
		// 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 = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
			selectedListItems = [];

		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
		{
			var itemNode = groupObj.contents[i];
			itemNode = itemNode.getAscendant( 'li', true );
			if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
				continue;
			selectedListItems.push( itemNode );
			CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
		}

		var lastListIndex = null;
		for ( i = 0 ; i < selectedListItems.length ; i++ )
		{
			var listIndex = selectedListItems[i].getCustomData( '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 ( 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 = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode,
			groupObj.root.getAttribute( 'dir' ) );

		// Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)
		var docFragment = newList.listNode, boundaryNode, siblingNode;
		function compensateBrs( isStart )
		{
			if ( ( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() )
				 && !( boundaryNode.is && boundaryNode.isBlockBoundary() )
				 && ( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]
				      ( CKEDITOR.dom.walker.whitespaces( true ) ) )
				 && !( siblingNode.is && siblingNode.isBlockBoundary( { br : 1 } ) ) )
				editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );
		}
		compensateBrs( true );
		compensateBrs();

		docFragment.replace( groupObj.root );
	}

	function listCommand( name, type )
	{
		this.name = name;
		this.type = type;
	}

	var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT );

	// Merge child nodes with direction preserved. (#7448)
	function mergeChildren( from, into, refNode, forward )
	{
		var child, itemDir;
		while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) )
		{
			if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) )
				child.setAttribute( 'dir', itemDir );

			child.remove();

			refNode ?
				child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) :
				into.append( child, forward  );
		}
	}

	listCommand.prototype = {
		exec : function( editor )
		{
			var doc = editor.document,
				config = editor.config,
				selection = editor.getSelection(),
				ranges = selection && selection.getRanges( true );

			// There should be at least one selected range.
			if ( !ranges || ranges.length < 1 )
				return;

			// Midas lists rule #1 says we can create a list even in an empty document.
			// But DOM iterator 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 ( this.state == CKEDITOR.TRISTATE_OFF )
			{
				var body = doc.getBody();
				if ( !body.getFirst( nonEmpty ) )
				{
					config.enterMode == CKEDITOR.ENTER_BR ?
						body.appendBogus() :
						ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );

					selection.selectRanges( ranges );
				}
				// Maybe a single range there enclosing the whole list,
				// turn on the list state manually(#4129).
				else
				{
					var range = ranges.length == 1 && ranges[ 0 ],
						enclosedNode = range && range.getEnclosedNode();
					if ( enclosedNode && enclosedNode.is
						&& this.type == enclosedNode.getName() )
							this.setState( CKEDITOR.TRISTATE_ON );
				}
			}

			var bookmarks = selection.createBookmarks( true );

			// 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 = [],
				database = {},
				rangeIterator = ranges.createIterator(),
				index = 0;

			while ( ( range = rangeIterator.getNextRange() ) && ++index )
			{
				var boundaryNodes = range.getBoundaryNodes(),
					startNode = boundaryNodes.startNode,
					endNode = boundaryNodes.endNode;

				if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )
					range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );

				if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )
					range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );

				var iterator = range.createIterator(),
					block;

				iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );

				while ( ( block = iterator.getNextParagraph() ) )
				{
					// Avoid duplicate blocks get processed across ranges.
					if( block.getCustomData( 'list_block' ) )
						continue;
					else
						CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );

					var path = new CKEDITOR.dom.elementPath( block ),
						pathElements = path.elements,
						pathElementsCount = pathElements.length,
						listNode = null,
						processedFlag = 0,
						blockLimit = path.blockLimit,
						element;

					// First, try to group by a list ancestor.
					for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- )
					{
						if ( listNodeNames[ element.getName() ]
							 && blockLimit.contains( element ) )     // Don't leak outside block limit (#3940).
						{
							// 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)
							blockLimit.removeCustomData( 'list_group_object_' + index );

							var groupObj = element.getCustomData( 'list_group_object' );
							if ( groupObj )
								groupObj.contents.push( block );
							else
							{
								groupObj = { root : element, contents : [ block ] };
								listGroups.push( groupObj );
								CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );
							}
							processedFlag = 1;
							break;
						}
					}

					if ( processedFlag )
						continue;

					// No list ancestor? Group by block limit, but don't mix contents from different ranges.
					var root = blockLimit;
					if ( root.getCustomData( 'list_group_object_' + index ) )
						root.getCustomData( 'list_group_object_' + index ).contents.push( block );
					else
					{
						groupObj = { root : root, contents : [ block ] };
						CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );
						listGroups.push( groupObj );
					}
				}
			}

			// 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 )
			{
				groupObj = listGroups.shift();
				if ( this.state == CKEDITOR.TRISTATE_OFF )
				{
					if ( listNodeNames[ groupObj.root.getName() ] )
						changeListType.call( this, editor, groupObj, database, listsCreated );
					else
						createList.call( this, editor, groupObj, listsCreated );
				}
				else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] )
					removeList.call( this, editor, groupObj, database );
			}

			// For all new lists created, merge into adjacent, same type lists.
			for ( i = 0 ; i < listsCreated.length ; i++ )
				mergeListSiblings( listsCreated[ i ] );

			// Clean up, restore selection and update toolbar button states.
			CKEDITOR.dom.element.clearAllMarkers( database );
			selection.selectBookmarks( bookmarks );
			editor.focus();
		}
	};

	// Merge list adjacent, of same type lists.
	function mergeListSiblings( listNode )
	{
		var mergeSibling;
		( mergeSibling = function( rtl )
		{
			var sibling = listNode[ rtl ? 'getPrevious' : 'getNext' ]( nonEmpty );
			if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT &&
			     sibling.is( listNode.getName() ) )
			{
				// Move children order by merge direction.(#3820)
				mergeChildren( listNode, sibling, null, !rtl );

				listNode.remove();
				listNode = sibling;
			}
		} )();
		mergeSibling( 1 );
	}

	var dtd = CKEDITOR.dtd;
	var tailNbspRegex = /[\t\r\n ]*(?:&nbsp;|\xa0)$/;

	function indexOfFirstChildElement( element, tagNameList )
	{
		var child,
			children = element.children,
			length = children.length;

		for ( var i = 0 ; i < length ; i++ )
		{
			child = children[ i ];
			if ( child.name && ( child.name in tagNameList ) )
				return i;
		}

		return length;
	}

	function getExtendNestedListFilter( isHtmlFilter )
	{
		// An element filter function that corrects nested list start in an empty
		// list item for better displaying/outputting. (#3165)
		return function( listItem )
		{
			var children = listItem.children,
				firstNestedListIndex = indexOfFirstChildElement( listItem, dtd.$list ),
				firstNestedList = children[ firstNestedListIndex ],
				nodeBefore = firstNestedList && firstNestedList.previous,
				tailNbspmatch;

			if ( nodeBefore
				&& ( nodeBefore.name && nodeBefore.name == 'br'
					|| nodeBefore.value && ( tailNbspmatch = nodeBefore.value.match( tailNbspRegex ) ) ) )
			{
				var fillerNode = nodeBefore;

				// Always use 'nbsp' as filler node if we found a nested list appear
				// in front of a list item.
				if ( !( tailNbspmatch && tailNbspmatch.index ) && fillerNode == children[ 0 ] )
					children[ 0 ] = ( isHtmlFilter || CKEDITOR.env.ie ) ?
					                 new CKEDITOR.htmlParser.text( '\xa0' ) :
									 new CKEDITOR.htmlParser.element( 'br', {} );

				// Otherwise the filler is not needed anymore.
				else if ( fillerNode.name == 'br' )
					children.splice( firstNestedListIndex - 1, 1 );
				else
					fillerNode.value = fillerNode.value.replace( tailNbspRegex, '' );
			}

		};
	}

	var defaultListDataFilterRules = { elements : {} };
	for ( var i in dtd.$listItem )
		defaultListDataFilterRules.elements[ i ] = getExtendNestedListFilter();

	var defaultListHtmlFilterRules = { elements : {} };
	for ( i in dtd.$listItem )
		defaultListHtmlFilterRules.elements[ i ] = getExtendNestedListFilter( true );

	// Check if node is block element that recieves text.
	function isTextBlock( node )
	{
		return node.type == CKEDITOR.NODE_ELEMENT &&
			   ( node.getName() in CKEDITOR.dtd.$block ||
				 node.getName() in CKEDITOR.dtd.$listItem ) &&
			   CKEDITOR.dtd[ node.getName() ][ '#' ];
	}

	// Join visually two block lines.
	function joinNextLineToCursor( editor, cursor, nextCursor )
	{
		editor.fire( 'saveSnapshot' );

		// Merge with previous block's content.
		nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS );
		var frag = nextCursor.extractContents();

		cursor.trim( false, true );
		var bm = cursor.createBookmark();

		// Kill original bogus;
		var currentPath = new CKEDITOR.dom.elementPath( cursor.startContainer ),
				pathBlock = currentPath.block,
				currentBlock = currentPath.lastElement.getAscendant( 'li', 1 ) || pathBlock,
				nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer ),
				nextLi = nextPath.contains( CKEDITOR.dtd.$listItem ),
				nextList = nextPath.contains( CKEDITOR.dtd.$list ),
				last;

		// Remove bogus node the current block/pseudo block.
		if ( pathBlock )
		{
			var bogus = pathBlock.getBogus();
			bogus && bogus.remove();
		}
		else if ( nextList )
		{
			last = nextList.getPrevious( nonEmpty );
			if ( last && blockBogus( last ) )
				last.remove();
		}

		// Kill the tail br in extracted.
		last = frag.getLast();
		if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )
			last.remove();

		// Insert fragment at the range position.
		var nextNode = cursor.startContainer.getChild( cursor.startOffset );
		if ( nextNode )
			frag.insertBefore( nextNode );
		else
			cursor.startContainer.append( frag );

		// Move the sub list nested in the next list item.
		if ( nextLi )
		{
			var sublist = getSubList( nextLi );
			if ( sublist )
			{
				// If next line is in the sub list of the current list item.
				if ( currentBlock.contains( nextLi ) )
				{
					mergeChildren( sublist, nextLi.getParent(), nextLi );
					sublist.remove();
				}
				// Migrate the sub list to current list item.
				else
					currentBlock.append( sublist );
			}
		}

		// Remove any remaining empty path blocks at next line after merging.
		while ( nextCursor.checkStartOfBlock() &&
			 nextCursor.checkEndOfBlock() )
		{
			nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer );
			var nextBlock = nextPath.block, parent;

			// Check if also to remove empty list.
			if ( nextBlock.is( 'li' ) )
			{
				parent = nextBlock.getParent();
				if ( nextBlock.equals( parent.getLast( nonEmpty ) )
						&& nextBlock.equals( parent.getFirst( nonEmpty ) ) )
					nextBlock = parent;
			}

			nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START );
			nextBlock.remove();
		}

		// Check if need to further merge with the list resides after the merged block. (#9080)
		var walkerRng = nextCursor.clone(), body = editor.document.getBody();
		walkerRng.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
		var walker = new CKEDITOR.dom.walker( walkerRng );
		walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); };
		var next = walker.next();
		if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list )
			mergeListSiblings( next );

		cursor.moveToBookmark( bm );

		// Make fresh selection.
		cursor.select();

		editor.selectionChange( 1 );
		editor.fire( 'saveSnapshot' );
	}

	function getSubList( li )
	{
		var last = li.getLast( nonEmpty );
		return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null;
	}

	CKEDITOR.plugins.add( 'list',
	{
		init : function( editor )
		{
			// Register commands.
			var numberedListCommand = editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) ),
				bulletedListCommand = editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );

			// Register the toolbar button.
			editor.ui.addButton( 'NumberedList',
				{
					label : editor.lang.numberedlist,
					command : 'numberedlist'
				} );
			editor.ui.addButton( 'BulletedList',
				{
					label : editor.lang.bulletedlist,
					command : 'bulletedlist'
				} );

			// Register the state changing handlers.
			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, numberedListCommand ) );
			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, bulletedListCommand ) );

				// Handled backspace/del key to join list items. (#8248,#9080)
				editor.on( 'key', function( evt )
				{
					var key = evt.data.keyCode;

					// DEl/BACKSPACE
					if ( editor.mode == 'wysiwyg' && key in { 8 : 1, 46 : 1 } )
					{
						var sel = editor.getSelection(),
						range = sel.getRanges()[ 0 ];

						if ( !range.collapsed )
							return;

						var path = new CKEDITOR.dom.elementPath( range.startContainer );
						var isBackspace = key == 8;
						var body = editor.document.getBody();
						var walker = new CKEDITOR.dom.walker( range.clone() );
						walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); };

						// Backspace/Del behavior at the start/end of table is handled in core.
						walker.guard = function( node, isOut ) { return !( isOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'table' ) ); };

						var cursor = range.clone();

						if ( isBackspace )
						{
							var previous, joinWith;

							// Join a sub list's first line, with the previous visual line in parent.
							if ( ( previous = path.contains( listNodeNames ) ) &&
							     range.checkBoundaryOfElement( previous, CKEDITOR.START ) &&
							     ( previous = previous.getParent() ) && previous.is( 'li' ) &&
							     ( previous = getSubList( previous ) ) )
							{
								joinWith = previous;
								previous = previous.getPrevious( nonEmpty );
								// Place cursor before the nested list.
								cursor.moveToPosition(
									previous && blockBogus( previous ) ? previous : joinWith,
									CKEDITOR.POSITION_BEFORE_START );
							}
							// Join any line following a list, with the last visual line of the list.
							else
							{
								walker.range.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
								walker.range.setEnd( range.startContainer, range.startOffset );
								previous = walker.previous();

								if ( previous && previous.type == CKEDITOR.NODE_ELEMENT &&
								   ( previous.getName() in listNodeNames || previous.is( 'li' ) ) )
								{
									if ( !previous.is( 'li' ) )
									{
										walker.range.selectNodeContents( previous );
										walker.reset();
										walker.evaluator = isTextBlock;
										previous = walker.previous();
									}

									joinWith = previous;
									// Place cursor at the end of previous block.
									cursor.moveToElementEditEnd( joinWith );
								}
							}

							if ( joinWith )
							{
								joinNextLineToCursor( editor, cursor, range );
								evt.cancel();
							}
							else
							{
								var list = path.contains( listNodeNames ), li;
								// Backspace pressed at the start of list outdents the first list item. (#9129)
								if ( list && range.checkBoundaryOfElement( list, CKEDITOR.START ) )
								{
									li = list.getFirst( nonEmpty );

									if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) )
									{
										previous = list.getPrevious( nonEmpty );

										// Only if the list item contains a sub list, do nothing but
										// simply move cursor backward one character.
										if ( getSubList( li ) )
										{
											if ( previous ) {
												range.moveToElementEditEnd( previous );
												range.select();
											}

											evt.cancel();
										}
										else
										{
											editor.execCommand( 'outdent' );
											evt.cancel();
										}
									}
								}
							}
						}
						else
						{
							var next, nextLine;
							li = range.startContainer.getAscendant( 'li', 1 );

							if ( li )
							{
								walker.range.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );

								var last = li.getLast( nonEmpty );
								var block = last && isTextBlock( last ) ? last : li;

								// Indicate cursor at the visual end of an list item.
								var isAtEnd = 0;

								next = walker.next();

								// When list item contains a sub list.
								if ( next && next.type == CKEDITOR.NODE_ELEMENT &&
									 next.getName() in listNodeNames
									 && next.equals( last ) )
								{
									isAtEnd = 1;

									// Move to the first item in sub list.
									next = walker.next();
								}
								// Right at the end of list item.
								else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) )
									isAtEnd = 1;


								if ( isAtEnd && next )
								{
									// Put cursor range there.
									nextLine = range.clone();
									nextLine.moveToElementEditStart( next );

									joinNextLineToCursor( editor, cursor, nextLine );
									evt.cancel();
								}
							}
							else
							{
								// Handle Del key pressed before the list.
								walker.range.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
								next = walker.next();

								if ( next && next.type == CKEDITOR.NODE_ELEMENT &&
								     next.getName() in listNodeNames )
								{
									// The start <li>
									next = next.getFirst( nonEmpty );

									// Simply remove the current empty block, move cursor to the
									// subsequent list.
									if ( path.block &&
									     range.checkStartOfBlock() &&
									     range.checkEndOfBlock() )
									{
										path.block.remove();
										range.moveToElementEditStart( next );
										range.select();
										evt.cancel();
									}

									// Preventing the default (merge behavior), but simply move
									// the cursor one character forward if subsequent list item
									// contains sub list.
									else if ( getSubList( next )  )
									{
										range.moveToElementEditStart( next );
										range.select();
										evt.cancel();
									}
									// Merge the first list item with the current line.
									else
									{
										nextLine = range.clone();
										nextLine.moveToElementEditStart( next );
										joinNextLineToCursor( editor, cursor, nextLine );
										evt.cancel();
									}
								}
							}
						}

						// The backspace/del could potentially put cursor at a bad position,
						// being it handled or not, check immediately the selection to have it fixed.
						setTimeout( function() { editor.selectionChange( 1 ); } );
					}
				} );
		},

		afterInit : function ( editor )
		{
			var dataProcessor = editor.dataProcessor;
			if ( dataProcessor )
			{
				dataProcessor.dataFilter.addRules( defaultListDataFilterRules );
				dataProcessor.htmlFilter.addRules( defaultListHtmlFilterRules );
			}
		},

		requires : [ 'domiterator' ]
	} );
})();

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 Cefalo
Bangladesh Bangladesh
Hi,

I am Palash Debnath. I have been working on windows technologies since 2008. I started with ASP.NET. Then I moved to Windows Form and from the last year I have been working with Windows 8 app development. Work with Windows 10 apps development as well. Now I have been working with Microsoft Azure. I have completed my Undergraduate from Khulna University of Engineering in Computer Science & Engineering. Currently working as a Senior Software Engineer at Cefalo.

Comments and Discussions