1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 /**
  7  * @file Insert and remove numbered and bulleted lists.
  8  */
  9 
 10 (function()
 11 {
 12 	var listNodeNames = { ol : 1, ul : 1 },
 13 		emptyTextRegex = /^[\n\r\t ]*$/;
 14 
 15 	var whitespaces = CKEDITOR.dom.walker.whitespaces(),
 16 		bookmarks = CKEDITOR.dom.walker.bookmark(),
 17 		nonEmpty = function( node ){ return !( whitespaces( node ) || bookmarks( node ) );},
 18 		blockBogus = CKEDITOR.dom.walker.bogus();
 19 
 20 	function cleanUpDirection( element )
 21 	{
 22 		var dir, parent, parentDir;
 23 		if ( ( dir = element.getDirection() ) )
 24 		{
 25 			parent = element.getParent();
 26 			while ( parent && !( parentDir = parent.getDirection() ) )
 27 				parent = parent.getParent();
 28 
 29 			if ( dir == parentDir )
 30 				element.removeAttribute( 'dir' );
 31 		}
 32 	}
 33 
 34 	// Inheirt inline styles from another element.
 35 	function inheirtInlineStyles( parent, el )
 36 	{
 37 		var style = parent.getAttribute( 'style' );
 38 
 39 		// Put parent styles before child styles.
 40 		style && el.setAttribute( 'style',
 41 			style.replace( /([^;])$/, '$1;' ) +
 42 			( el.getAttribute( 'style' ) || '' ) );
 43 	}
 44 
 45 	CKEDITOR.plugins.list = {
 46 		/*
 47 		 * Convert a DOM list tree into a data structure that is easier to
 48 		 * manipulate. This operation should be non-intrusive in the sense that it
 49 		 * does not change the DOM tree, with the exception that it may add some
 50 		 * markers to the list item nodes when database is specified.
 51 		 */
 52 		listToArray : function( listNode, database, baseArray, baseIndentLevel, grandparentNode )
 53 		{
 54 			if ( !listNodeNames[ listNode.getName() ] )
 55 				return [];
 56 
 57 			if ( !baseIndentLevel )
 58 				baseIndentLevel = 0;
 59 			if ( !baseArray )
 60 				baseArray = [];
 61 
 62 			// Iterate over all list items to and look for inner lists.
 63 			for ( var i = 0, count = listNode.getChildCount() ; i < count ; i++ )
 64 			{
 65 				var listItem = listNode.getChild( i );
 66 
 67 				// Fixing malformed nested lists by moving it into a previous list item. (#6236)
 68 				if( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list )
 69 					CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 );
 70 
 71 				// It may be a text node or some funny stuff.
 72 				if ( listItem.$.nodeName.toLowerCase() != 'li' )
 73 					continue;
 74 
 75 				var itemObj = { 'parent' : listNode, indent : baseIndentLevel, element : listItem, contents : [] };
 76 				if ( !grandparentNode )
 77 				{
 78 					itemObj.grandparent = listNode.getParent();
 79 					if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' )
 80 						itemObj.grandparent = itemObj.grandparent.getParent();
 81 				}
 82 				else
 83 					itemObj.grandparent = grandparentNode;
 84 
 85 				if ( database )
 86 					CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );
 87 				baseArray.push( itemObj );
 88 
 89 				for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount ; j++ )
 90 				{
 91 					child = listItem.getChild( j );
 92 					if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] )
 93 						// Note the recursion here, it pushes inner list items with
 94 						// +1 indentation in the correct order.
 95 						CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent );
 96 					else
 97 						itemObj.contents.push( child );
 98 				}
 99 			}
100 			return baseArray;
101 		},
102 
103 		// Convert our internal representation of a list back to a DOM forest.
104 		arrayToList : function( listArray, database, baseIndex, paragraphMode, dir )
105 		{
106 			if ( !baseIndex )
107 				baseIndex = 0;
108 			if ( !listArray || listArray.length < baseIndex + 1 )
109 				return null;
110 			var i,
111 				doc = listArray[ baseIndex ].parent.getDocument(),
112 				retval = new CKEDITOR.dom.documentFragment( doc ),
113 				rootNode = null,
114 				currentIndex = baseIndex,
115 				indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),
116 				currentListItem = null,
117 				orgDir,
118 				block,
119 				paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
120 			while ( 1 )
121 			{
122 				var item = listArray[ currentIndex ],
123 					itemGrandParent = item.grandparent;
124 
125 				orgDir = item.element.getDirection( 1 );
126 
127 				if ( item.indent == indentLevel )
128 				{
129 					if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() )
130 					{
131 						rootNode = listArray[ currentIndex ].parent.clone( false, 1 );
132 						dir && rootNode.setAttribute( 'dir', dir );
133 						retval.append( rootNode );
134 					}
135 					currentListItem = rootNode.append( item.element.clone( 0, 1 ) );
136 
137 					if ( orgDir != rootNode.getDirection( 1 ) )
138 						currentListItem.setAttribute( 'dir', orgDir );
139 
140 					for ( i = 0 ; i < item.contents.length ; i++ )
141 						currentListItem.append( item.contents[i].clone( 1, 1 ) );
142 					currentIndex++;
143 				}
144 				else if ( item.indent == Math.max( indentLevel, 0 ) + 1 )
145 				{
146 					// Maintain original direction (#6861).
147 					var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ),
148 						listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode,
149 						currDir != orgDir ? orgDir: null );
150 
151 					// If the next block is an <li> with another list tree as the first
152 					// child, we'll need to append a filler (<br>/NBSP) or the list item
153 					// wouldn't be editable. (#6724)
154 					if ( !currentListItem.getChildCount() && CKEDITOR.env.ie && !( doc.$.documentMode > 7 ))
155 						currentListItem.append( doc.createText( '\xa0' ) );
156 					currentListItem.append( listData.listNode );
157 					currentIndex = listData.nextIndex;
158 				}
159 				else if ( item.indent == -1 && !baseIndex && itemGrandParent )
160 				{
161 					if ( listNodeNames[ itemGrandParent.getName() ] )
162 					{
163 						currentListItem = item.element.clone( false, true );
164 						if ( orgDir != itemGrandParent.getDirection( 1 ) )
165 							currentListItem.setAttribute( 'dir', orgDir );
166 					}
167 					else
168 						currentListItem = new CKEDITOR.dom.documentFragment( doc );
169 
170 					// Migrate all children to the new container,
171 					// apply the proper text direction.
172 					var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir,
173 						li = item.element,
174 						className = li.getAttribute( 'class' ),
175 						style = li.getAttribute( 'style' );
176 
177 					var needsBlock = currentListItem.type ==
178 					                 CKEDITOR.NODE_DOCUMENT_FRAGMENT &&
179 					                 ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose ||
180 					                   style || className );
181 
182 					var child, count = item.contents.length;
183 					for ( i = 0 ; i < count; i++ )
184 					{
185 						child = item.contents[ i ];
186 
187 						if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() )
188 						{
189 							// Apply direction on content blocks.
190 							if ( dirLoose && !child.getDirection() )
191 								child.setAttribute( 'dir', orgDir );
192 
193 							inheirtInlineStyles( li, child );
194 
195 							className && child.addClass( className );
196 						}
197 						else if ( needsBlock )
198 						{
199 							// Establish new block to hold text direction and styles.
200 							if ( !block )
201 							{
202 								block = doc.createElement( paragraphName );
203 								dirLoose && block.setAttribute( 'dir', orgDir );
204 							}
205 
206 							// Copy over styles to new block;
207 							style && block.setAttribute( 'style', style );
208 							className && block.setAttribute( 'class', className );
209 
210 							block.append( child.clone( 1, 1 ) );
211 						}
212 
213 						currentListItem.append( block || child.clone( 1, 1 ) );
214 					}
215 
216 					if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT
217 						 && currentIndex != listArray.length - 1 )
218 					{
219 						var last = currentListItem.getLast();
220 						if ( last && last.type == CKEDITOR.NODE_ELEMENT
221 								&& last.getAttribute( 'type' ) == '_moz' )
222 						{
223 							last.remove();
224 						}
225 
226 						if ( !( last = currentListItem.getLast( nonEmpty )
227 							&& last.type == CKEDITOR.NODE_ELEMENT
228 							&& last.getName() in CKEDITOR.dtd.$block ) )
229 						{
230 							currentListItem.append( doc.createElement( 'br' ) );
231 						}
232 					}
233 
234 					var currentListItemName = currentListItem.$.nodeName.toLowerCase();
235 					if ( !CKEDITOR.env.ie && ( currentListItemName == 'div' || currentListItemName == 'p' ) )
236 						currentListItem.appendBogus();
237 					retval.append( currentListItem );
238 					rootNode = null;
239 					currentIndex++;
240 				}
241 				else
242 					return null;
243 
244 				block = null;
245 
246 				if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )
247 					break;
248 			}
249 
250 			if ( database )
251 			{
252 				var currentNode = retval.getFirst(),
253 					listRoot = listArray[ 0 ].parent;
254 
255 				while ( currentNode )
256 				{
257 					if ( currentNode.type == CKEDITOR.NODE_ELEMENT )
258 					{
259 						// Clear marker attributes for the new list tree made of cloned nodes, if any.
260 						CKEDITOR.dom.element.clearMarkers( database, currentNode );
261 
262 						// Clear redundant direction attribute specified on list items.
263 						if ( currentNode.getName() in CKEDITOR.dtd.$listItem )
264 							cleanUpDirection( currentNode );
265 					}
266 
267 					currentNode = currentNode.getNextSourceNode();
268 				}
269 			}
270 
271 			return { listNode : retval, nextIndex : currentIndex };
272 		}
273 	};
274 
275 	function onSelectionChange( evt )
276 	{
277 		if ( evt.editor.readOnly )
278 			return null;
279 
280 		var path = evt.data.path,
281 			blockLimit = path.blockLimit,
282 			elements = path.elements,
283 			element,
284 			i;
285 
286 		// Grouping should only happen under blockLimit.(#3940).
287 		for ( i = 0 ; i < elements.length && ( element = elements[ i ] )
288 			  && !element.equals( blockLimit ); i++ )
289 		{
290 			if ( listNodeNames[ elements[ i ].getName() ] )
291 				return this.setState( this.type == elements[ i ].getName() ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
292 		}
293 
294 		return this.setState( CKEDITOR.TRISTATE_OFF );
295 	}
296 
297 	function changeListType( editor, groupObj, database, listsCreated )
298 	{
299 		// This case is easy...
300 		// 1. Convert the whole list into a one-dimensional array.
301 		// 2. Change the list type by modifying the array.
302 		// 3. Recreate the whole list by converting the array to a list.
303 		// 4. Replace the original list with the recreated list.
304 		var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
305 			selectedListItems = [];
306 
307 		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
308 		{
309 			var itemNode = groupObj.contents[i];
310 			itemNode = itemNode.getAscendant( 'li', true );
311 			if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
312 				continue;
313 			selectedListItems.push( itemNode );
314 			CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
315 		}
316 
317 		var root = groupObj.root,
318 			doc = root.getDocument(),
319 			listNode,
320 			newListNode;
321 
322 		for ( i = 0 ; i < selectedListItems.length ; i++ )
323 		{
324 			var listIndex = selectedListItems[i].getCustomData( 'listarray_index' );
325 			listNode = listArray[ listIndex ].parent;
326 
327 			// Switch to new list node for this particular item.
328 			if ( !listNode.is( this.type ) )
329 			{
330 				newListNode = doc.createElement( this.type );
331 				// Copy all attributes, except from 'start' and 'type'.
332 				listNode.copyAttributes( newListNode, { start : 1, type : 1 } );
333 				// The list-style-type property should be ignored.
334 				newListNode.removeStyle( 'list-style-type' );
335 				listArray[ listIndex ].parent = newListNode;
336 			}
337 		}
338 
339 		var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );
340 		var child, length = newList.listNode.getChildCount();
341 		for ( i = 0 ; i < length && ( child = newList.listNode.getChild( i ) ) ; i++ )
342 		{
343 			if ( child.getName() == this.type )
344 				listsCreated.push( child );
345 		}
346 		newList.listNode.replace( groupObj.root );
347 	}
348 
349 	var headerTagRegex = /^h[1-6]$/;
350 
351 	function createList( editor, groupObj, listsCreated )
352 	{
353 		var contents = groupObj.contents,
354 			doc = groupObj.root.getDocument(),
355 			listContents = [];
356 
357 		// It is possible to have the contents returned by DomRangeIterator to be the same as the root.
358 		// e.g. when we're running into table cells.
359 		// In such a case, enclose the childNodes of contents[0] into a <div>.
360 		if ( contents.length == 1 && contents[0].equals( groupObj.root ) )
361 		{
362 			var divBlock = doc.createElement( 'div' );
363 			contents[0].moveChildren && contents[0].moveChildren( divBlock );
364 			contents[0].append( divBlock );
365 			contents[0] = divBlock;
366 		}
367 
368 		// Calculate the common parent node of all content blocks.
369 		var commonParent = groupObj.contents[0].getParent();
370 		for ( var i = 0 ; i < contents.length ; i++ )
371 			commonParent = commonParent.getCommonAncestor( contents[i].getParent() );
372 
373 		var useComputedState = editor.config.useComputedState,
374 			listDir, explicitDirection;
375 
376 		useComputedState = useComputedState === undefined || useComputedState;
377 
378 		// We want to insert things that are in the same tree level only, so calculate the contents again
379 		// by expanding the selected blocks to the same tree level.
380 		for ( i = 0 ; i < contents.length ; i++ )
381 		{
382 			var contentNode = contents[i],
383 				parentNode;
384 			while ( ( parentNode = contentNode.getParent() ) )
385 			{
386 				if ( parentNode.equals( commonParent ) )
387 				{
388 					listContents.push( contentNode );
389 
390 					// Determine the lists's direction.
391 					if ( !explicitDirection && contentNode.getDirection() )
392 						explicitDirection = 1;
393 
394 					var itemDir = contentNode.getDirection( useComputedState );
395 
396 					if ( listDir !== null )
397 					{
398 						// If at least one LI have a different direction than current listDir, we can't have listDir.
399 						if ( listDir && listDir != itemDir )
400 							listDir = null;
401 						else
402 							listDir = itemDir;
403 					}
404 
405 					break;
406 				}
407 				contentNode = parentNode;
408 			}
409 		}
410 
411 		if ( listContents.length < 1 )
412 			return;
413 
414 		// Insert the list to the DOM tree.
415 		var insertAnchor = listContents[ listContents.length - 1 ].getNext(),
416 			listNode = doc.createElement( this.type );
417 
418 		listsCreated.push( listNode );
419 
420 		var contentBlock, listItem;
421 
422 		while ( listContents.length )
423 		{
424 			contentBlock = listContents.shift();
425 			listItem = doc.createElement( 'li' );
426 
427 			// Preserve preformat block and heading structure when converting to list item. (#5335) (#5271)
428 			if ( contentBlock.is( 'pre' ) || headerTagRegex.test( contentBlock.getName() ) )
429 				contentBlock.appendTo( listItem );
430 			else
431 			{
432 				contentBlock.copyAttributes( listItem );
433 				// Remove direction attribute after it was merged into list root. (#7657)
434 				if ( listDir && contentBlock.getDirection() )
435 				{
436 					listItem.removeStyle( 'direction' );
437 					listItem.removeAttribute( 'dir' );
438 				}
439 				contentBlock.moveChildren( listItem );
440 				contentBlock.remove();
441 			}
442 
443 			listItem.appendTo( listNode );
444 		}
445 
446 		// Apply list root dir only if it has been explicitly declared.
447 		if ( listDir && explicitDirection )
448 			listNode.setAttribute( 'dir', listDir );
449 
450 		if ( insertAnchor )
451 			listNode.insertBefore( insertAnchor );
452 		else
453 			listNode.appendTo( commonParent );
454 	}
455 
456 	function removeList( editor, groupObj, database )
457 	{
458 		// This is very much like the change list type operation.
459 		// Except that we're changing the selected items' indent to -1 in the list array.
460 		var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
461 			selectedListItems = [];
462 
463 		for ( var i = 0 ; i < groupObj.contents.length ; i++ )
464 		{
465 			var itemNode = groupObj.contents[i];
466 			itemNode = itemNode.getAscendant( 'li', true );
467 			if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
468 				continue;
469 			selectedListItems.push( itemNode );
470 			CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
471 		}
472 
473 		var lastListIndex = null;
474 		for ( i = 0 ; i < selectedListItems.length ; i++ )
475 		{
476 			var listIndex = selectedListItems[i].getCustomData( 'listarray_index' );
477 			listArray[listIndex].indent = -1;
478 			lastListIndex = listIndex;
479 		}
480 
481 		// After cutting parts of the list out with indent=-1, we still have to maintain the array list
482 		// model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
483 		// list cannot be converted back to a real DOM list.
484 		for ( i = lastListIndex + 1 ; i < listArray.length ; i++ )
485 		{
486 			if ( listArray[i].indent > listArray[i-1].indent + 1 )
487 			{
488 				var indentOffset = listArray[i-1].indent + 1 - listArray[i].indent;
489 				var oldIndent = listArray[i].indent;
490 				while ( listArray[i] && listArray[i].indent >= oldIndent )
491 				{
492 					listArray[i].indent += indentOffset;
493 					i++;
494 				}
495 				i--;
496 			}
497 		}
498 
499 		var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode,
500 			groupObj.root.getAttribute( 'dir' ) );
501 
502 		// Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)
503 		var docFragment = newList.listNode, boundaryNode, siblingNode;
504 		function compensateBrs( isStart )
505 		{
506 			if ( ( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() )
507 				 && !( boundaryNode.is && boundaryNode.isBlockBoundary() )
508 				 && ( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]
509 				      ( CKEDITOR.dom.walker.whitespaces( true ) ) )
510 				 && !( siblingNode.is && siblingNode.isBlockBoundary( { br : 1 } ) ) )
511 				editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );
512 		}
513 		compensateBrs( true );
514 		compensateBrs();
515 
516 		docFragment.replace( groupObj.root );
517 	}
518 
519 	function listCommand( name, type )
520 	{
521 		this.name = name;
522 		this.type = type;
523 	}
524 
525 	var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT );
526 
527 	// Merge child nodes with direction preserved. (#7448)
528 	function mergeChildren( from, into, refNode, forward )
529 	{
530 		var child, itemDir;
531 		while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) )
532 		{
533 			if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) )
534 				child.setAttribute( 'dir', itemDir );
535 
536 			child.remove();
537 
538 			refNode ?
539 				child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) :
540 				into.append( child, forward  );
541 		}
542 	}
543 
544 	listCommand.prototype = {
545 		exec : function( editor )
546 		{
547 			var doc = editor.document,
548 				config = editor.config,
549 				selection = editor.getSelection(),
550 				ranges = selection && selection.getRanges( true );
551 
552 			// There should be at least one selected range.
553 			if ( !ranges || ranges.length < 1 )
554 				return;
555 
556 			// Midas lists rule #1 says we can create a list even in an empty document.
557 			// But DOM iterator wouldn't run if the document is really empty.
558 			// So create a paragraph if the document is empty and we're going to create a list.
559 			if ( this.state == CKEDITOR.TRISTATE_OFF )
560 			{
561 				var body = doc.getBody();
562 				if ( !body.getFirst( nonEmpty ) )
563 				{
564 					config.enterMode == CKEDITOR.ENTER_BR ?
565 						body.appendBogus() :
566 						ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
567 
568 					selection.selectRanges( ranges );
569 				}
570 				// Maybe a single range there enclosing the whole list,
571 				// turn on the list state manually(#4129).
572 				else
573 				{
574 					var range = ranges.length == 1 && ranges[ 0 ],
575 						enclosedNode = range && range.getEnclosedNode();
576 					if ( enclosedNode && enclosedNode.is
577 						&& this.type == enclosedNode.getName() )
578 							this.setState( CKEDITOR.TRISTATE_ON );
579 				}
580 			}
581 
582 			var bookmarks = selection.createBookmarks( true );
583 
584 			// Group the blocks up because there are many cases where multiple lists have to be created,
585 			// or multiple lists have to be cancelled.
586 			var listGroups = [],
587 				database = {},
588 				rangeIterator = ranges.createIterator(),
589 				index = 0;
590 
591 			while ( ( range = rangeIterator.getNextRange() ) && ++index )
592 			{
593 				var boundaryNodes = range.getBoundaryNodes(),
594 					startNode = boundaryNodes.startNode,
595 					endNode = boundaryNodes.endNode;
596 
597 				if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )
598 					range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );
599 
600 				if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )
601 					range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );
602 
603 				var iterator = range.createIterator(),
604 					block;
605 
606 				iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );
607 
608 				while ( ( block = iterator.getNextParagraph() ) )
609 				{
610 					// Avoid duplicate blocks get processed across ranges.
611 					if( block.getCustomData( 'list_block' ) )
612 						continue;
613 					else
614 						CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );
615 
616 					var path = new CKEDITOR.dom.elementPath( block ),
617 						pathElements = path.elements,
618 						pathElementsCount = pathElements.length,
619 						listNode = null,
620 						processedFlag = 0,
621 						blockLimit = path.blockLimit,
622 						element;
623 
624 					// First, try to group by a list ancestor.
625 					for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- )
626 					{
627 						if ( listNodeNames[ element.getName() ]
628 							 && blockLimit.contains( element ) )     // Don't leak outside block limit (#3940).
629 						{
630 							// If we've encountered a list inside a block limit
631 							// The last group object of the block limit element should
632 							// no longer be valid. Since paragraphs after the list
633 							// should belong to a different group of paragraphs before
634 							// the list. (Bug #1309)
635 							blockLimit.removeCustomData( 'list_group_object_' + index );
636 
637 							var groupObj = element.getCustomData( 'list_group_object' );
638 							if ( groupObj )
639 								groupObj.contents.push( block );
640 							else
641 							{
642 								groupObj = { root : element, contents : [ block ] };
643 								listGroups.push( groupObj );
644 								CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );
645 							}
646 							processedFlag = 1;
647 							break;
648 						}
649 					}
650 
651 					if ( processedFlag )
652 						continue;
653 
654 					// No list ancestor? Group by block limit, but don't mix contents from different ranges.
655 					var root = blockLimit;
656 					if ( root.getCustomData( 'list_group_object_' + index ) )
657 						root.getCustomData( 'list_group_object_' + index ).contents.push( block );
658 					else
659 					{
660 						groupObj = { root : root, contents : [ block ] };
661 						CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );
662 						listGroups.push( groupObj );
663 					}
664 				}
665 			}
666 
667 			// Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
668 			// We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
669 			// at the group that's not rooted at lists. So we have three cases to handle.
670 			var listsCreated = [];
671 			while ( listGroups.length > 0 )
672 			{
673 				groupObj = listGroups.shift();
674 				if ( this.state == CKEDITOR.TRISTATE_OFF )
675 				{
676 					if ( listNodeNames[ groupObj.root.getName() ] )
677 						changeListType.call( this, editor, groupObj, database, listsCreated );
678 					else
679 						createList.call( this, editor, groupObj, listsCreated );
680 				}
681 				else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] )
682 					removeList.call( this, editor, groupObj, database );
683 			}
684 
685 			// For all new lists created, merge into adjacent, same type lists.
686 			for ( i = 0 ; i < listsCreated.length ; i++ )
687 				mergeListSiblings( listsCreated[ i ] );
688 
689 			// Clean up, restore selection and update toolbar button states.
690 			CKEDITOR.dom.element.clearAllMarkers( database );
691 			selection.selectBookmarks( bookmarks );
692 			editor.focus();
693 		}
694 	};
695 
696 	// Merge list adjacent, of same type lists.
697 	function mergeListSiblings( listNode )
698 	{
699 		var mergeSibling;
700 		( mergeSibling = function( rtl )
701 		{
702 			var sibling = listNode[ rtl ? 'getPrevious' : 'getNext' ]( nonEmpty );
703 			if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT &&
704 			     sibling.is( listNode.getName() ) )
705 			{
706 				// Move children order by merge direction.(#3820)
707 				mergeChildren( listNode, sibling, null, !rtl );
708 
709 				listNode.remove();
710 				listNode = sibling;
711 			}
712 		} )();
713 		mergeSibling( 1 );
714 	}
715 
716 	var dtd = CKEDITOR.dtd;
717 	var tailNbspRegex = /[\t\r\n ]*(?: |\xa0)$/;
718 
719 	function indexOfFirstChildElement( element, tagNameList )
720 	{
721 		var child,
722 			children = element.children,
723 			length = children.length;
724 
725 		for ( var i = 0 ; i < length ; i++ )
726 		{
727 			child = children[ i ];
728 			if ( child.name && ( child.name in tagNameList ) )
729 				return i;
730 		}
731 
732 		return length;
733 	}
734 
735 	function getExtendNestedListFilter( isHtmlFilter )
736 	{
737 		// An element filter function that corrects nested list start in an empty
738 		// list item for better displaying/outputting. (#3165)
739 		return function( listItem )
740 		{
741 			var children = listItem.children,
742 				firstNestedListIndex = indexOfFirstChildElement( listItem, dtd.$list ),
743 				firstNestedList = children[ firstNestedListIndex ],
744 				nodeBefore = firstNestedList && firstNestedList.previous,
745 				tailNbspmatch;
746 
747 			if ( nodeBefore
748 				&& ( nodeBefore.name && nodeBefore.name == 'br'
749 					|| nodeBefore.value && ( tailNbspmatch = nodeBefore.value.match( tailNbspRegex ) ) ) )
750 			{
751 				var fillerNode = nodeBefore;
752 
753 				// Always use 'nbsp' as filler node if we found a nested list appear
754 				// in front of a list item.
755 				if ( !( tailNbspmatch && tailNbspmatch.index ) && fillerNode == children[ 0 ] )
756 					children[ 0 ] = ( isHtmlFilter || CKEDITOR.env.ie ) ?
757 					                 new CKEDITOR.htmlParser.text( '\xa0' ) :
758 									 new CKEDITOR.htmlParser.element( 'br', {} );
759 
760 				// Otherwise the filler is not needed anymore.
761 				else if ( fillerNode.name == 'br' )
762 					children.splice( firstNestedListIndex - 1, 1 );
763 				else
764 					fillerNode.value = fillerNode.value.replace( tailNbspRegex, '' );
765 			}
766 
767 		};
768 	}
769 
770 	var defaultListDataFilterRules = { elements : {} };
771 	for ( var i in dtd.$listItem )
772 		defaultListDataFilterRules.elements[ i ] = getExtendNestedListFilter();
773 
774 	var defaultListHtmlFilterRules = { elements : {} };
775 	for ( i in dtd.$listItem )
776 		defaultListHtmlFilterRules.elements[ i ] = getExtendNestedListFilter( true );
777 
778 	// Check if node is block element that recieves text.
779 	function isTextBlock( node )
780 	{
781 		return node.type == CKEDITOR.NODE_ELEMENT &&
782 			   ( node.getName() in CKEDITOR.dtd.$block ||
783 				 node.getName() in CKEDITOR.dtd.$listItem ) &&
784 			   CKEDITOR.dtd[ node.getName() ][ '#' ];
785 	}
786 
787 	// Join visually two block lines.
788 	function joinNextLineToCursor( editor, cursor, nextCursor )
789 	{
790 		editor.fire( 'saveSnapshot' );
791 
792 		// Merge with previous block's content.
793 		nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS );
794 		var frag = nextCursor.extractContents();
795 
796 		cursor.trim( false, true );
797 		var bm = cursor.createBookmark();
798 
799 		// Kill original bogus;
800 		var currentPath = new CKEDITOR.dom.elementPath( cursor.startContainer ),
801 				pathBlock = currentPath.block,
802 				currentBlock = currentPath.lastElement.getAscendant( 'li', 1 ) || pathBlock,
803 				nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer ),
804 				nextLi = nextPath.contains( CKEDITOR.dtd.$listItem ),
805 				nextList = nextPath.contains( CKEDITOR.dtd.$list ),
806 				last;
807 
808 		// Remove bogus node the current block/pseudo block.
809 		if ( pathBlock )
810 		{
811 			var bogus = pathBlock.getBogus();
812 			bogus && bogus.remove();
813 		}
814 		else if ( nextList )
815 		{
816 			last = nextList.getPrevious( nonEmpty );
817 			if ( last && blockBogus( last ) )
818 				last.remove();
819 		}
820 
821 		// Kill the tail br in extracted.
822 		last = frag.getLast();
823 		if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )
824 			last.remove();
825 
826 		// Insert fragment at the range position.
827 		var nextNode = cursor.startContainer.getChild( cursor.startOffset );
828 		if ( nextNode )
829 			frag.insertBefore( nextNode );
830 		else
831 			cursor.startContainer.append( frag );
832 
833 		// Move the sub list nested in the next list item.
834 		if ( nextLi )
835 		{
836 			var sublist = getSubList( nextLi );
837 			if ( sublist )
838 			{
839 				// If next line is in the sub list of the current list item.
840 				if ( currentBlock.contains( nextLi ) )
841 				{
842 					mergeChildren( sublist, nextLi.getParent(), nextLi );
843 					sublist.remove();
844 				}
845 				// Migrate the sub list to current list item.
846 				else
847 					currentBlock.append( sublist );
848 			}
849 		}
850 
851 		// Remove any remaining empty path blocks at next line after merging.
852 		while ( nextCursor.checkStartOfBlock() &&
853 			 nextCursor.checkEndOfBlock() )
854 		{
855 			nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer );
856 			var nextBlock = nextPath.block, parent;
857 
858 			// Check if also to remove empty list.
859 			if ( nextBlock.is( 'li' ) )
860 			{
861 				parent = nextBlock.getParent();
862 				if ( nextBlock.equals( parent.getLast( nonEmpty ) )
863 						&& nextBlock.equals( parent.getFirst( nonEmpty ) ) )
864 					nextBlock = parent;
865 			}
866 
867 			nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START );
868 			nextBlock.remove();
869 		}
870 
871 		// Check if need to further merge with the list resides after the merged block. (#9080)
872 		var walkerRng = nextCursor.clone(), body = editor.document.getBody();
873 		walkerRng.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
874 		var walker = new CKEDITOR.dom.walker( walkerRng );
875 		walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); };
876 		var next = walker.next();
877 		if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list )
878 			mergeListSiblings( next );
879 
880 		cursor.moveToBookmark( bm );
881 
882 		// Make fresh selection.
883 		cursor.select();
884 
885 		editor.selectionChange( 1 );
886 		editor.fire( 'saveSnapshot' );
887 	}
888 
889 	function getSubList( li )
890 	{
891 		var last = li.getLast( nonEmpty );
892 		return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null;
893 	}
894 
895 	CKEDITOR.plugins.add( 'list',
896 	{
897 		init : function( editor )
898 		{
899 			// Register commands.
900 			var numberedListCommand = editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) ),
901 				bulletedListCommand = editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );
902 
903 			// Register the toolbar button.
904 			editor.ui.addButton( 'NumberedList',
905 				{
906 					label : editor.lang.numberedlist,
907 					command : 'numberedlist'
908 				} );
909 			editor.ui.addButton( 'BulletedList',
910 				{
911 					label : editor.lang.bulletedlist,
912 					command : 'bulletedlist'
913 				} );
914 
915 			// Register the state changing handlers.
916 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, numberedListCommand ) );
917 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, bulletedListCommand ) );
918 
919 				// Handled backspace/del key to join list items. (#8248,#9080)
920 				editor.on( 'key', function( evt )
921 				{
922 					var key = evt.data.keyCode;
923 
924 					// DEl/BACKSPACE
925 					if ( editor.mode == 'wysiwyg' && key in { 8 : 1, 46 : 1 } )
926 					{
927 						var sel = editor.getSelection(),
928 						range = sel.getRanges()[ 0 ];
929 
930 						if ( !range.collapsed )
931 							return;
932 
933 						var path = new CKEDITOR.dom.elementPath( range.startContainer );
934 						var isBackspace = key == 8;
935 						var body = editor.document.getBody();
936 						var walker = new CKEDITOR.dom.walker( range.clone() );
937 						walker.evaluator = function( node ) { return nonEmpty( node ) && !blockBogus( node ); };
938 
939 						// Backspace/Del behavior at the start/end of table is handled in core.
940 						walker.guard = function( node, isOut ) { return !( isOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'table' ) ); };
941 
942 						var cursor = range.clone();
943 
944 						if ( isBackspace )
945 						{
946 							var previous, joinWith;
947 
948 							// Join a sub list's first line, with the previous visual line in parent.
949 							if ( ( previous = path.contains( listNodeNames ) ) &&
950 							     range.checkBoundaryOfElement( previous, CKEDITOR.START ) &&
951 							     ( previous = previous.getParent() ) && previous.is( 'li' ) &&
952 							     ( previous = getSubList( previous ) ) )
953 							{
954 								joinWith = previous;
955 								previous = previous.getPrevious( nonEmpty );
956 								// Place cursor before the nested list.
957 								cursor.moveToPosition(
958 									previous && blockBogus( previous ) ? previous : joinWith,
959 									CKEDITOR.POSITION_BEFORE_START );
960 							}
961 							// Join any line following a list, with the last visual line of the list.
962 							else
963 							{
964 								walker.range.setStartAt( body, CKEDITOR.POSITION_AFTER_START );
965 								walker.range.setEnd( range.startContainer, range.startOffset );
966 								previous = walker.previous();
967 
968 								if ( previous && previous.type == CKEDITOR.NODE_ELEMENT &&
969 								   ( previous.getName() in listNodeNames || previous.is( 'li' ) ) )
970 								{
971 									if ( !previous.is( 'li' ) )
972 									{
973 										walker.range.selectNodeContents( previous );
974 										walker.reset();
975 										walker.evaluator = isTextBlock;
976 										previous = walker.previous();
977 									}
978 
979 									joinWith = previous;
980 									// Place cursor at the end of previous block.
981 									cursor.moveToElementEditEnd( joinWith );
982 								}
983 							}
984 
985 							if ( joinWith )
986 							{
987 								joinNextLineToCursor( editor, cursor, range );
988 								evt.cancel();
989 							}
990 							else
991 							{
992 								var list = path.contains( listNodeNames ), li;
993 								// Backspace pressed at the start of list outdents the first list item. (#9129)
994 								if ( list && range.checkBoundaryOfElement( list, CKEDITOR.START ) )
995 								{
996 									li = list.getFirst( nonEmpty );
997 
998 									if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) )
999 									{
1000 										previous = list.getPrevious( nonEmpty );
1001 
1002 										// Only if the list item contains a sub list, do nothing but
1003 										// simply move cursor backward one character.
1004 										if ( getSubList( li ) )
1005 										{
1006 											if ( previous ) {
1007 												range.moveToElementEditEnd( previous );
1008 												range.select();
1009 											}
1010 
1011 											evt.cancel();
1012 										}
1013 										else
1014 										{
1015 											editor.execCommand( 'outdent' );
1016 											evt.cancel();
1017 										}
1018 									}
1019 								}
1020 							}
1021 						}
1022 						else
1023 						{
1024 							var next, nextLine;
1025 							li = range.startContainer.getAscendant( 'li', 1 );
1026 
1027 							if ( li )
1028 							{
1029 								walker.range.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
1030 
1031 								var last = li.getLast( nonEmpty );
1032 								var block = last && isTextBlock( last ) ? last : li;
1033 
1034 								// Indicate cursor at the visual end of an list item.
1035 								var isAtEnd = 0;
1036 
1037 								next = walker.next();
1038 
1039 								// When list item contains a sub list.
1040 								if ( next && next.type == CKEDITOR.NODE_ELEMENT &&
1041 									 next.getName() in listNodeNames
1042 									 && next.equals( last ) )
1043 								{
1044 									isAtEnd = 1;
1045 
1046 									// Move to the first item in sub list.
1047 									next = walker.next();
1048 								}
1049 								// Right at the end of list item.
1050 								else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) )
1051 									isAtEnd = 1;
1052 
1053 
1054 								if ( isAtEnd && next )
1055 								{
1056 									// Put cursor range there.
1057 									nextLine = range.clone();
1058 									nextLine.moveToElementEditStart( next );
1059 
1060 									joinNextLineToCursor( editor, cursor, nextLine );
1061 									evt.cancel();
1062 								}
1063 							}
1064 							else
1065 							{
1066 								// Handle Del key pressed before the list.
1067 								walker.range.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );
1068 								next = walker.next();
1069 
1070 								if ( next && next.type == CKEDITOR.NODE_ELEMENT &&
1071 								     next.getName() in listNodeNames )
1072 								{
1073 									// The start <li>
1074 									next = next.getFirst( nonEmpty );
1075 
1076 									// Simply remove the current empty block, move cursor to the
1077 									// subsequent list.
1078 									if ( path.block &&
1079 									     range.checkStartOfBlock() &&
1080 									     range.checkEndOfBlock() )
1081 									{
1082 										path.block.remove();
1083 										range.moveToElementEditStart( next );
1084 										range.select();
1085 										evt.cancel();
1086 									}
1087 
1088 									// Preventing the default (merge behavior), but simply move
1089 									// the cursor one character forward if subsequent list item
1090 									// contains sub list.
1091 									else if ( getSubList( next )  )
1092 									{
1093 										range.moveToElementEditStart( next );
1094 										range.select();
1095 										evt.cancel();
1096 									}
1097 									// Merge the first list item with the current line.
1098 									else
1099 									{
1100 										nextLine = range.clone();
1101 										nextLine.moveToElementEditStart( next );
1102 										joinNextLineToCursor( editor, cursor, nextLine );
1103 										evt.cancel();
1104 									}
1105 								}
1106 							}
1107 						}
1108 
1109 						// The backspace/del could potentially put cursor at a bad position,
1110 						// being it handled or not, check immediately the selection to have it fixed.
1111 						setTimeout( function() { editor.selectionChange( 1 ); } );
1112 					}
1113 				} );
1114 		},
1115 
1116 		afterInit : function ( editor )
1117 		{
1118 			var dataProcessor = editor.dataProcessor;
1119 			if ( dataProcessor )
1120 			{
1121 				dataProcessor.dataFilter.addRules( defaultListDataFilterRules );
1122 				dataProcessor.htmlFilter.addRules( defaultListHtmlFilterRules );
1123 			}
1124 		},
1125 
1126 		requires : [ 'domiterator' ]
1127 	} );
1128 })();
1129