initial commit
[namibia] / public / scripts / ckeditor / _source / plugins / list / plugin.js
1 /*
2 Copyright (c) 2003-2011, 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
19         function cleanUpDirection( element )
20         {
21                 var dir, parent, parentDir;
22                 if ( ( dir = element.getDirection() ) )
23                 {
24                         parent = element.getParent();
25                         while ( parent && !( parentDir = parent.getDirection() ) )
26                                 parent = parent.getParent();
27
28                         if ( dir == parentDir )
29                                 element.removeAttribute( 'dir' );
30                 }
31         }
32
33         CKEDITOR.plugins.list = {
34                 /*
35                  * Convert a DOM list tree into a data structure that is easier to
36                  * manipulate. This operation should be non-intrusive in the sense that it
37                  * does not change the DOM tree, with the exception that it may add some
38                  * markers to the list item nodes when database is specified.
39                  */
40                 listToArray : function( listNode, database, baseArray, baseIndentLevel, grandparentNode )
41                 {
42                         if ( !listNodeNames[ listNode.getName() ] )
43                                 return [];
44
45                         if ( !baseIndentLevel )
46                                 baseIndentLevel = 0;
47                         if ( !baseArray )
48                                 baseArray = [];
49
50                         // Iterate over all list items to and look for inner lists.
51                         for ( var i = 0, count = listNode.getChildCount() ; i < count ; i++ )
52                         {
53                                 var listItem = listNode.getChild( i );
54
55                                 // Fixing malformed nested lists by moving it into a previous list item. (#6236)
56                                 if( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list )
57                                         CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 );
58
59                                 // It may be a text node or some funny stuff.
60                                 if ( listItem.$.nodeName.toLowerCase() != 'li' )
61                                         continue;
62
63                                 var itemObj = { 'parent' : listNode, indent : baseIndentLevel, element : listItem, contents : [] };
64                                 if ( !grandparentNode )
65                                 {
66                                         itemObj.grandparent = listNode.getParent();
67                                         if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' )
68                                                 itemObj.grandparent = itemObj.grandparent.getParent();
69                                 }
70                                 else
71                                         itemObj.grandparent = grandparentNode;
72
73                                 if ( database )
74                                         CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );
75                                 baseArray.push( itemObj );
76
77                                 for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount ; j++ )
78                                 {
79                                         child = listItem.getChild( j );
80                                         if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] )
81                                                 // Note the recursion here, it pushes inner list items with
82                                                 // +1 indentation in the correct order.
83                                                 CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent );
84                                         else
85                                                 itemObj.contents.push( child );
86                                 }
87                         }
88                         return baseArray;
89                 },
90
91                 // Convert our internal representation of a list back to a DOM forest.
92                 arrayToList : function( listArray, database, baseIndex, paragraphMode, dir )
93                 {
94                         if ( !baseIndex )
95                                 baseIndex = 0;
96                         if ( !listArray || listArray.length < baseIndex + 1 )
97                                 return null;
98                         var doc = listArray[ baseIndex ].parent.getDocument(),
99                                 retval = new CKEDITOR.dom.documentFragment( doc ),
100                                 rootNode = null,
101                                 currentIndex = baseIndex,
102                                 indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),
103                                 currentListItem = null,
104                                 orgDir,
105                                 paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
106                         while ( 1 )
107                         {
108                                 var item = listArray[ currentIndex ];
109
110                                 orgDir = item.element.getDirection( 1 );
111
112                                 if ( item.indent == indentLevel )
113                                 {
114                                         if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() )
115                                         {
116                                                 rootNode = listArray[ currentIndex ].parent.clone( false, 1 );
117                                                 dir && rootNode.setAttribute( 'dir', dir );
118                                                 retval.append( rootNode );
119                                         }
120                                         currentListItem = rootNode.append( item.element.clone( 0, 1 ) );
121
122                                         if ( orgDir != rootNode.getDirection( 1 ) )
123                                                 currentListItem.setAttribute( 'dir', orgDir );
124
125                                         for ( var i = 0 ; i < item.contents.length ; i++ )
126                                                 currentListItem.append( item.contents[i].clone( 1, 1 ) );
127                                         currentIndex++;
128                                 }
129                                 else if ( item.indent == Math.max( indentLevel, 0 ) + 1 )
130                                 {
131                                         // Maintain original direction (#6861).
132                                         var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ),
133                                                 listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode,
134                                                 currDir != orgDir ? orgDir: null );
135
136                                         // If the next block is an <li> with another list tree as the first
137                                         // child, we'll need to append a filler (<br>/NBSP) or the list item
138                                         // wouldn't be editable. (#6724)
139                                         if ( !currentListItem.getChildCount() && CKEDITOR.env.ie && !( doc.$.documentMode > 7 ))
140                                                 currentListItem.append( doc.createText( '\xa0' ) );
141                                         currentListItem.append( listData.listNode );
142                                         currentIndex = listData.nextIndex;
143                                 }
144                                 else if ( item.indent == -1 && !baseIndex && item.grandparent )
145                                 {
146                                         if ( listNodeNames[ item.grandparent.getName() ] )
147                                                 currentListItem = item.element.clone( false, true );
148                                         else
149                                         {
150                                                 // Create completely new blocks here.
151                                                 if ( dir || item.element.hasAttributes() || paragraphMode != CKEDITOR.ENTER_BR )
152                                                 {
153                                                         currentListItem = doc.createElement( paragraphName );
154                                                         item.element.copyAttributes( currentListItem, { type:1, value:1 } );
155
156                                                         // There might be a case where there are no attributes in the element after all
157                                                         // (i.e. when "type" or "value" are the only attributes set). In this case, if enterMode = BR,
158                                                         // the current item should be a fragment.
159                                                         if ( !dir && paragraphMode == CKEDITOR.ENTER_BR && !currentListItem.hasAttributes() )
160                                                                 currentListItem = new CKEDITOR.dom.documentFragment( doc );
161                                                 }
162                                                 else
163                                                         currentListItem = new CKEDITOR.dom.documentFragment( doc );
164                                         }
165
166                                         if ( currentListItem.type == CKEDITOR.NODE_ELEMENT )
167                                         {
168                                                 if ( item.grandparent.getDirection( 1 ) != orgDir )
169                                                         currentListItem.setAttribute( 'dir', orgDir );
170                                         }
171
172                                         for ( i = 0 ; i < item.contents.length ; i++ )
173                                                 currentListItem.append( item.contents[i].clone( 1, 1 ) );
174
175                                         if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT
176                                                  && currentIndex != listArray.length - 1 )
177                                         {
178                                                 var last = currentListItem.getLast();
179                                                 if ( last && last.type == CKEDITOR.NODE_ELEMENT
180                                                                 && last.getAttribute( 'type' ) == '_moz' )
181                                                 {
182                                                         last.remove();
183                                                 }
184
185                                                 if ( !( last = currentListItem.getLast( nonEmpty )
186                                                         && last.type == CKEDITOR.NODE_ELEMENT
187                                                         && last.getName() in CKEDITOR.dtd.$block ) )
188                                                 {
189                                                         currentListItem.append( doc.createElement( 'br' ) );
190                                                 }
191                                         }
192
193                                         if ( currentListItem.type == CKEDITOR.NODE_ELEMENT &&
194                                                         currentListItem.getName() == paragraphName &&
195                                                         currentListItem.$.firstChild )
196                                         {
197                                                 currentListItem.trim();
198                                                 var firstChild = currentListItem.getFirst();
199                                                 if ( firstChild.type == CKEDITOR.NODE_ELEMENT && firstChild.isBlockBoundary() )
200                                                 {
201                                                         var tmp = new CKEDITOR.dom.documentFragment( doc );
202                                                         currentListItem.moveChildren( tmp );
203                                                         currentListItem = tmp;
204                                                 }
205                                         }
206
207                                         var currentListItemName = currentListItem.$.nodeName.toLowerCase();
208                                         if ( !CKEDITOR.env.ie && ( currentListItemName == 'div' || currentListItemName == 'p' ) )
209                                                 currentListItem.appendBogus();
210                                         retval.append( currentListItem );
211                                         rootNode = null;
212                                         currentIndex++;
213                                 }
214                                 else
215                                         return null;
216
217                                 if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )
218                                         break;
219                         }
220
221                         if ( database )
222                         {
223                                 var currentNode = retval.getFirst(),
224                                         listRoot = listArray[ 0 ].parent;
225
226                                 while ( currentNode )
227                                 {
228                                         if ( currentNode.type == CKEDITOR.NODE_ELEMENT )
229                                         {
230                                                 // Clear marker attributes for the new list tree made of cloned nodes, if any.
231                                                 CKEDITOR.dom.element.clearMarkers( database, currentNode );
232
233                                                 // Clear redundant direction attribute specified on list items.
234                                                 if ( currentNode.getName() in CKEDITOR.dtd.$listItem )
235                                                         cleanUpDirection( currentNode );
236                                         }
237
238                                         currentNode = currentNode.getNextSourceNode();
239                                 }
240                         }
241
242                         return { listNode : retval, nextIndex : currentIndex };
243                 }
244         };
245
246         function onSelectionChange( evt )
247         {
248                 if ( evt.editor.readOnly )
249                         return null;
250
251                 var path = evt.data.path,
252                         blockLimit = path.blockLimit,
253                         elements = path.elements,
254                         element,
255                         i;
256
257                 // Grouping should only happen under blockLimit.(#3940).
258                 for ( i = 0 ; i < elements.length && ( element = elements[ i ] )
259                           && !element.equals( blockLimit ); i++ )
260                 {
261                         if ( listNodeNames[ elements[ i ].getName() ] )
262                                 return this.setState( this.type == elements[ i ].getName() ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
263                 }
264
265                 return this.setState( CKEDITOR.TRISTATE_OFF );
266         }
267
268         function changeListType( editor, groupObj, database, listsCreated )
269         {
270                 // This case is easy...
271                 // 1. Convert the whole list into a one-dimensional array.
272                 // 2. Change the list type by modifying the array.
273                 // 3. Recreate the whole list by converting the array to a list.
274                 // 4. Replace the original list with the recreated list.
275                 var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
276                         selectedListItems = [];
277
278                 for ( var i = 0 ; i < groupObj.contents.length ; i++ )
279                 {
280                         var itemNode = groupObj.contents[i];
281                         itemNode = itemNode.getAscendant( 'li', true );
282                         if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
283                                 continue;
284                         selectedListItems.push( itemNode );
285                         CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
286                 }
287
288                 var root = groupObj.root,
289                         fakeParent = root.getDocument().createElement( this.type );
290                 // Copy all attributes, except from 'start' and 'type'.
291                 root.copyAttributes( fakeParent, { start : 1, type : 1 } );
292                 // The list-style-type property should be ignored.
293                 fakeParent.removeStyle( 'list-style-type' );
294
295                 for ( i = 0 ; i < selectedListItems.length ; i++ )
296                 {
297                         var listIndex = selectedListItems[i].getCustomData( 'listarray_index' );
298                         listArray[listIndex].parent = fakeParent;
299                 }
300                 var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );
301                 var child, length = newList.listNode.getChildCount();
302                 for ( i = 0 ; i < length && ( child = newList.listNode.getChild( i ) ) ; i++ )
303                 {
304                         if ( child.getName() == this.type )
305                                 listsCreated.push( child );
306                 }
307                 newList.listNode.replace( groupObj.root );
308         }
309
310         var headerTagRegex = /^h[1-6]$/;
311
312         function createList( editor, groupObj, listsCreated )
313         {
314                 var contents = groupObj.contents,
315                         doc = groupObj.root.getDocument(),
316                         listContents = [];
317
318                 // It is possible to have the contents returned by DomRangeIterator to be the same as the root.
319                 // e.g. when we're running into table cells.
320                 // In such a case, enclose the childNodes of contents[0] into a <div>.
321                 if ( contents.length == 1 && contents[0].equals( groupObj.root ) )
322                 {
323                         var divBlock = doc.createElement( 'div' );
324                         contents[0].moveChildren && contents[0].moveChildren( divBlock );
325                         contents[0].append( divBlock );
326                         contents[0] = divBlock;
327                 }
328
329                 // Calculate the common parent node of all content blocks.
330                 var commonParent = groupObj.contents[0].getParent();
331                 for ( var i = 0 ; i < contents.length ; i++ )
332                         commonParent = commonParent.getCommonAncestor( contents[i].getParent() );
333
334                 var useComputedState = editor.config.useComputedState,
335                         listDir, explicitDirection;
336
337                 useComputedState = useComputedState === undefined || useComputedState;
338
339                 // We want to insert things that are in the same tree level only, so calculate the contents again
340                 // by expanding the selected blocks to the same tree level.
341                 for ( i = 0 ; i < contents.length ; i++ )
342                 {
343                         var contentNode = contents[i],
344                                 parentNode;
345                         while ( ( parentNode = contentNode.getParent() ) )
346                         {
347                                 if ( parentNode.equals( commonParent ) )
348                                 {
349                                         listContents.push( contentNode );
350
351                                         // Determine the lists's direction.
352                                         if ( !explicitDirection && contentNode.getDirection() )
353                                                 explicitDirection = 1;
354
355                                         var itemDir = contentNode.getDirection( useComputedState );
356
357                                         if ( listDir !== null )
358                                         {
359                                                 // If at least one LI have a different direction than current listDir, we can't have listDir.
360                                                 if ( listDir && listDir != itemDir )
361                                                         listDir = null;
362                                                 else
363                                                         listDir = itemDir;
364                                         }
365
366                                         break;
367                                 }
368                                 contentNode = parentNode;
369                         }
370                 }
371
372                 if ( listContents.length < 1 )
373                         return;
374
375                 // Insert the list to the DOM tree.
376                 var insertAnchor = listContents[ listContents.length - 1 ].getNext(),
377                         listNode = doc.createElement( this.type );
378
379                 listsCreated.push( listNode );
380
381                 var contentBlock, listItem;
382
383                 while ( listContents.length )
384                 {
385                         contentBlock = listContents.shift();
386                         listItem = doc.createElement( 'li' );
387
388                         // Preserve preformat block and heading structure when converting to list item. (#5335) (#5271)
389                         if ( contentBlock.is( 'pre' ) || headerTagRegex.test( contentBlock.getName() ) )
390                                 contentBlock.appendTo( listItem );
391                         else
392                         {
393                                 contentBlock.copyAttributes( listItem );
394                                 // Remove direction attribute after it was merged into list root. (#7657)
395                                 if ( listDir && contentBlock.getDirection() )
396                                 {
397                                         listItem.removeStyle( 'direction' );
398                                         listItem.removeAttribute( 'dir' );
399                                 }
400                                 contentBlock.moveChildren( listItem );
401                                 contentBlock.remove();
402                         }
403
404                         listItem.appendTo( listNode );
405                 }
406
407                 // Apply list root dir only if it has been explicitly declared.
408                 if ( listDir && explicitDirection )
409                         listNode.setAttribute( 'dir', listDir );
410
411                 if ( insertAnchor )
412                         listNode.insertBefore( insertAnchor );
413                 else
414                         listNode.appendTo( commonParent );
415         }
416
417         function removeList( editor, groupObj, database )
418         {
419                 // This is very much like the change list type operation.
420                 // Except that we're changing the selected items' indent to -1 in the list array.
421                 var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),
422                         selectedListItems = [];
423
424                 for ( var i = 0 ; i < groupObj.contents.length ; i++ )
425                 {
426                         var itemNode = groupObj.contents[i];
427                         itemNode = itemNode.getAscendant( 'li', true );
428                         if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )
429                                 continue;
430                         selectedListItems.push( itemNode );
431                         CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
432                 }
433
434                 var lastListIndex = null;
435                 for ( i = 0 ; i < selectedListItems.length ; i++ )
436                 {
437                         var listIndex = selectedListItems[i].getCustomData( 'listarray_index' );
438                         listArray[listIndex].indent = -1;
439                         lastListIndex = listIndex;
440                 }
441
442                 // After cutting parts of the list out with indent=-1, we still have to maintain the array list
443                 // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
444                 // list cannot be converted back to a real DOM list.
445                 for ( i = lastListIndex + 1 ; i < listArray.length ; i++ )
446                 {
447                         if ( listArray[i].indent > listArray[i-1].indent + 1 )
448                         {
449                                 var indentOffset = listArray[i-1].indent + 1 - listArray[i].indent;
450                                 var oldIndent = listArray[i].indent;
451                                 while ( listArray[i] && listArray[i].indent >= oldIndent )
452                                 {
453                                         listArray[i].indent += indentOffset;
454                                         i++;
455                                 }
456                                 i--;
457                         }
458                 }
459
460                 var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode,
461                         groupObj.root.getAttribute( 'dir' ) );
462
463                 // Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)
464                 var docFragment = newList.listNode, boundaryNode, siblingNode;
465                 function compensateBrs( isStart )
466                 {
467                         if ( ( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() )
468                                  && !( boundaryNode.is && boundaryNode.isBlockBoundary() )
469                                  && ( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]
470                                       ( CKEDITOR.dom.walker.whitespaces( true ) ) )
471                                  && !( siblingNode.is && siblingNode.isBlockBoundary( { br : 1 } ) ) )
472                                 editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );
473                 }
474                 compensateBrs( true );
475                 compensateBrs();
476
477                 docFragment.replace( groupObj.root );
478         }
479
480         function listCommand( name, type )
481         {
482                 this.name = name;
483                 this.type = type;
484         }
485
486         // Move direction attribute from root to list items.
487         function dirToListItems( list )
488         {
489                 var dir = list.getDirection();
490                 if ( dir )
491                 {
492                         for ( var i = 0, children = list.getChildren(), child; child = children.getItem( i ), i < children.count(); i++ )
493                         {
494                                 if ( child.type == CKEDITOR.NODE_ELEMENT && child.is( 'li' ) && !child.getDirection() )
495                                         child.setAttribute( 'dir', dir );
496                         }
497
498                         list.removeAttribute( 'dir' );
499                 }
500         }
501
502         listCommand.prototype = {
503                 exec : function( editor )
504                 {
505                         var doc = editor.document,
506                                 config = editor.config,
507                                 selection = editor.getSelection(),
508                                 ranges = selection && selection.getRanges( true );
509
510                         // There should be at least one selected range.
511                         if ( !ranges || ranges.length < 1 )
512                                 return;
513
514                         // Midas lists rule #1 says we can create a list even in an empty document.
515                         // But DOM iterator wouldn't run if the document is really empty.
516                         // So create a paragraph if the document is empty and we're going to create a list.
517                         if ( this.state == CKEDITOR.TRISTATE_OFF )
518                         {
519                                 var body = doc.getBody();
520                                 if ( !body.getFirst( nonEmpty ) )
521                                 {
522                                         config.enterMode == CKEDITOR.ENTER_BR ?
523                                                 body.appendBogus() :
524                                                 ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
525
526                                         selection.selectRanges( ranges );
527                                 }
528                                 // Maybe a single range there enclosing the whole list,
529                                 // turn on the list state manually(#4129).
530                                 else
531                                 {
532                                         var range = ranges.length == 1 && ranges[ 0 ],
533                                                 enclosedNode = range && range.getEnclosedNode();
534                                         if ( enclosedNode && enclosedNode.is
535                                                 && this.type == enclosedNode.getName() )
536                                                         this.setState( CKEDITOR.TRISTATE_ON );
537                                 }
538                         }
539
540                         var bookmarks = selection.createBookmarks( true );
541
542                         // Group the blocks up because there are many cases where multiple lists have to be created,
543                         // or multiple lists have to be cancelled.
544                         var listGroups = [],
545                                 database = {},
546                                 rangeIterator = ranges.createIterator(),
547                                 index = 0;
548
549                         while ( ( range = rangeIterator.getNextRange() ) && ++index )
550                         {
551                                 var boundaryNodes = range.getBoundaryNodes(),
552                                         startNode = boundaryNodes.startNode,
553                                         endNode = boundaryNodes.endNode;
554
555                                 if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )
556                                         range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );
557
558                                 if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )
559                                         range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );
560
561                                 var iterator = range.createIterator(),
562                                         block;
563
564                                 iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );
565
566                                 while ( ( block = iterator.getNextParagraph() ) )
567                                 {
568                                         // Avoid duplicate blocks get processed across ranges.
569                                         if( block.getCustomData( 'list_block' ) )
570                                                 continue;
571                                         else
572                                                 CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );
573
574                                         var path = new CKEDITOR.dom.elementPath( block ),
575                                                 pathElements = path.elements,
576                                                 pathElementsCount = pathElements.length,
577                                                 listNode = null,
578                                                 processedFlag = 0,
579                                                 blockLimit = path.blockLimit,
580                                                 element;
581
582                                         // First, try to group by a list ancestor.
583                                         for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- )
584                                         {
585                                                 if ( listNodeNames[ element.getName() ]
586                                                          && blockLimit.contains( element ) )     // Don't leak outside block limit (#3940).
587                                                 {
588                                                         // If we've encountered a list inside a block limit
589                                                         // The last group object of the block limit element should
590                                                         // no longer be valid. Since paragraphs after the list
591                                                         // should belong to a different group of paragraphs before
592                                                         // the list. (Bug #1309)
593                                                         blockLimit.removeCustomData( 'list_group_object_' + index );
594
595                                                         var groupObj = element.getCustomData( 'list_group_object' );
596                                                         if ( groupObj )
597                                                                 groupObj.contents.push( block );
598                                                         else
599                                                         {
600                                                                 groupObj = { root : element, contents : [ block ] };
601                                                                 listGroups.push( groupObj );
602                                                                 CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );
603                                                         }
604                                                         processedFlag = 1;
605                                                         break;
606                                                 }
607                                         }
608
609                                         if ( processedFlag )
610                                                 continue;
611
612                                         // No list ancestor? Group by block limit, but don't mix contents from different ranges.
613                                         var root = blockLimit;
614                                         if ( root.getCustomData( 'list_group_object_' + index ) )
615                                                 root.getCustomData( 'list_group_object_' + index ).contents.push( block );
616                                         else
617                                         {
618                                                 groupObj = { root : root, contents : [ block ] };
619                                                 CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );
620                                                 listGroups.push( groupObj );
621                                         }
622                                 }
623                         }
624
625                         // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
626                         // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
627                         // at the group that's not rooted at lists. So we have three cases to handle.
628                         var listsCreated = [];
629                         while ( listGroups.length > 0 )
630                         {
631                                 groupObj = listGroups.shift();
632                                 if ( this.state == CKEDITOR.TRISTATE_OFF )
633                                 {
634                                         if ( listNodeNames[ groupObj.root.getName() ] )
635                                                 changeListType.call( this, editor, groupObj, database, listsCreated );
636                                         else
637                                                 createList.call( this, editor, groupObj, listsCreated );
638                                 }
639                                 else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] )
640                                         removeList.call( this, editor, groupObj, database );
641                         }
642
643                         // For all new lists created, merge adjacent, same type lists.
644                         for ( i = 0 ; i < listsCreated.length ; i++ )
645                         {
646                                 listNode = listsCreated[i];
647                                 var mergeSibling, listCommand = this;
648                                 ( mergeSibling = function( rtl ){
649
650                                         var sibling = listNode[ rtl ?
651                                                 'getPrevious' : 'getNext' ]( CKEDITOR.dom.walker.whitespaces( true ) );
652                                         if ( sibling && sibling.getName &&
653                                                  sibling.getName() == listCommand.type )
654                                         {
655
656                                                 // In case to be merged lists have difference directions. (#7448)
657                                                 if ( sibling.getDirection( 1 ) != listNode.getDirection( 1 ) )
658                                                         dirToListItems( listNode.getDirection() ? listNode : sibling );
659
660                                                 sibling.remove();
661                                                 // Move children order by merge direction.(#3820)
662                                                 sibling.moveChildren( listNode, rtl );
663                                         }
664                                 } )();
665                                 mergeSibling( 1 );
666                         }
667
668                         // Clean up, restore selection and update toolbar button states.
669                         CKEDITOR.dom.element.clearAllMarkers( database );
670                         selection.selectBookmarks( bookmarks );
671                         editor.focus();
672                 }
673         };
674
675         var dtd = CKEDITOR.dtd;
676         var tailNbspRegex = /[\t\r\n ]*(?:&nbsp;|\xa0)$/;
677
678         function indexOfFirstChildElement( element, tagNameList )
679         {
680                 var child,
681                         children = element.children,
682                         length = children.length;
683
684                 for ( var i = 0 ; i < length ; i++ )
685                 {
686                         child = children[ i ];
687                         if ( child.name && ( child.name in tagNameList ) )
688                                 return i;
689                 }
690
691                 return length;
692         }
693
694         function getExtendNestedListFilter( isHtmlFilter )
695         {
696                 // An element filter function that corrects nested list start in an empty
697                 // list item for better displaying/outputting. (#3165)
698                 return function( listItem )
699                 {
700                         var children = listItem.children,
701                                 firstNestedListIndex = indexOfFirstChildElement( listItem, dtd.$list ),
702                                 firstNestedList = children[ firstNestedListIndex ],
703                                 nodeBefore = firstNestedList && firstNestedList.previous,
704                                 tailNbspmatch;
705
706                         if ( nodeBefore
707                                 && ( nodeBefore.name && nodeBefore.name == 'br'
708                                         || nodeBefore.value && ( tailNbspmatch = nodeBefore.value.match( tailNbspRegex ) ) ) )
709                         {
710                                 var fillerNode = nodeBefore;
711
712                                 // Always use 'nbsp' as filler node if we found a nested list appear
713                                 // in front of a list item.
714                                 if ( !( tailNbspmatch && tailNbspmatch.index ) && fillerNode == children[ 0 ] )
715                                         children[ 0 ] = ( isHtmlFilter || CKEDITOR.env.ie ) ?
716                                                          new CKEDITOR.htmlParser.text( '\xa0' ) :
717                                                                          new CKEDITOR.htmlParser.element( 'br', {} );
718
719                                 // Otherwise the filler is not needed anymore.
720                                 else if ( fillerNode.name == 'br' )
721                                         children.splice( firstNestedListIndex - 1, 1 );
722                                 else
723                                         fillerNode.value = fillerNode.value.replace( tailNbspRegex, '' );
724                         }
725
726                 };
727         }
728
729         var defaultListDataFilterRules = { elements : {} };
730         for ( var i in dtd.$listItem )
731                 defaultListDataFilterRules.elements[ i ] = getExtendNestedListFilter();
732
733         var defaultListHtmlFilterRules = { elements : {} };
734         for ( i in dtd.$listItem )
735                 defaultListHtmlFilterRules.elements[ i ] = getExtendNestedListFilter( true );
736
737         CKEDITOR.plugins.add( 'list',
738         {
739                 init : function( editor )
740                 {
741                         // Register commands.
742                         var numberedListCommand = editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) ),
743                                 bulletedListCommand = editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );
744
745                         // Register the toolbar button.
746                         editor.ui.addButton( 'NumberedList',
747                                 {
748                                         label : editor.lang.numberedlist,
749                                         command : 'numberedlist'
750                                 } );
751                         editor.ui.addButton( 'BulletedList',
752                                 {
753                                         label : editor.lang.bulletedlist,
754                                         command : 'bulletedlist'
755                                 } );
756
757                         // Register the state changing handlers.
758                         editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, numberedListCommand ) );
759                         editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, bulletedListCommand ) );
760                 },
761
762                 afterInit : function ( editor )
763                 {
764                         var dataProcessor = editor.dataProcessor;
765                         if ( dataProcessor )
766                         {
767                                 dataProcessor.dataFilter.addRules( defaultListDataFilterRules );
768                                 dataProcessor.htmlFilter.addRules( defaultListHtmlFilterRules );
769                         }
770                 },
771
772                 requires : [ 'domiterator' ]
773         } );
774 })();