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