2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview Insert and remove numbered and bulleted lists.
11 var listNodeNames
= { ol: 1, ul: 1 };
13 var whitespaces
= CKEDITOR
.dom
.walker
.whitespaces(),
14 bookmarks
= CKEDITOR
.dom
.walker
.bookmark(),
15 nonEmpty = function( node
) {
16 return !( whitespaces( node
) || bookmarks( node
) );
18 blockBogus
= CKEDITOR
.dom
.walker
.bogus();
20 function cleanUpDirection( element
) {
21 var dir
, parent
, parentDir
;
22 if ( ( dir
= element
.getDirection() ) ) {
23 parent
= element
.getParent();
24 while ( parent
&& !( parentDir
= parent
.getDirection() ) )
25 parent
= parent
.getParent();
27 if ( dir
== parentDir
)
28 element
.removeAttribute( 'dir' );
32 // Inherit inline styles from another element.
33 function inheritInlineStyles( parent
, el
) {
34 var style
= parent
.getAttribute( 'style' );
36 // Put parent styles before child styles.
37 style
&& el
.setAttribute( 'style', style
.replace( /([^;])$/, '$1;' ) + ( el
.getAttribute( 'style' ) || '' ) );
40 CKEDITOR
.plugins
.list
= {
42 * Convert a DOM list tree into a data structure that is easier to
43 * manipulate. This operation should be non-intrusive in the sense that it
44 * does not change the DOM tree, with the exception that it may add some
45 * markers to the list item nodes when database is specified.
47 * @member CKEDITOR.plugins.list
50 listToArray: function( listNode
, database
, baseArray
, baseIndentLevel
, grandparentNode
) {
51 if ( !listNodeNames
[ listNode
.getName() ] )
54 if ( !baseIndentLevel
)
59 // Iterate over all list items to and look for inner lists.
60 for ( var i
= 0, count
= listNode
.getChildCount(); i
< count
; i
++ ) {
61 var listItem
= listNode
.getChild( i
);
63 // Fixing malformed nested lists by moving it into a previous list item. (http://dev.ckeditor.com/ticket/6236)
64 if ( listItem
.type
== CKEDITOR
.NODE_ELEMENT
&& listItem
.getName() in CKEDITOR
.dtd
.$list
)
65 CKEDITOR
.plugins
.list
.listToArray( listItem
, database
, baseArray
, baseIndentLevel
+ 1 );
67 // It may be a text node or some funny stuff.
68 if ( listItem
.$.nodeName
.toLowerCase() != 'li' )
71 var itemObj
= { 'parent': listNode
, indent: baseIndentLevel
, element: listItem
, contents: [] };
72 if ( !grandparentNode
) {
73 itemObj
.grandparent
= listNode
.getParent();
74 if ( itemObj
.grandparent
&& itemObj
.grandparent
.$.nodeName
.toLowerCase() == 'li' )
75 itemObj
.grandparent
= itemObj
.grandparent
.getParent();
77 itemObj
.grandparent
= grandparentNode
;
81 CKEDITOR
.dom
.element
.setMarker( database
, listItem
, 'listarray_index', baseArray
.length
);
82 baseArray
.push( itemObj
);
84 for ( var j
= 0, itemChildCount
= listItem
.getChildCount(), child
; j
< itemChildCount
; j
++ ) {
85 child
= listItem
.getChild( j
);
86 if ( child
.type
== CKEDITOR
.NODE_ELEMENT
&& listNodeNames
[ child
.getName() ] )
87 // Note the recursion here, it pushes inner list items with
88 // +1 indentation in the correct order.
89 CKEDITOR
.plugins
.list
.listToArray( child
, database
, baseArray
, baseIndentLevel
+ 1, itemObj
.grandparent
);
91 itemObj
.contents
.push( child
);
98 * Convert our internal representation of a list back to a DOM forest.
100 * @member CKEDITOR.plugins.list
103 arrayToList: function( listArray
, database
, baseIndex
, paragraphMode
, dir
) {
106 if ( !listArray
|| listArray
.length
< baseIndex
+ 1 )
110 doc
= listArray
[ baseIndex
].parent
.getDocument(),
111 retval
= new CKEDITOR
.dom
.documentFragment( doc
),
113 currentIndex
= baseIndex
,
114 indentLevel
= Math
.max( listArray
[ baseIndex
].indent
, 0 ),
115 currentListItem
= null,
117 paragraphName
= ( paragraphMode
== CKEDITOR
.ENTER_P
? 'p' : 'div' );
120 var item
= listArray
[ currentIndex
],
121 itemGrandParent
= item
.grandparent
;
123 orgDir
= item
.element
.getDirection( 1 );
125 if ( item
.indent
== indentLevel
) {
126 if ( !rootNode
|| listArray
[ currentIndex
].parent
.getName() != rootNode
.getName() ) {
127 rootNode
= listArray
[ currentIndex
].parent
.clone( false, 1 );
128 dir
&& rootNode
.setAttribute( 'dir', dir
);
129 retval
.append( rootNode
);
131 currentListItem
= rootNode
.append( item
.element
.clone( 0, 1 ) );
133 if ( orgDir
!= rootNode
.getDirection( 1 ) )
134 currentListItem
.setAttribute( 'dir', orgDir
);
136 for ( i
= 0; i
< item
.contents
.length
; i
++ )
137 currentListItem
.append( item
.contents
[ i
].clone( 1, 1 ) );
139 } else if ( item
.indent
== Math
.max( indentLevel
, 0 ) + 1 ) {
140 // Maintain original direction (http://dev.ckeditor.com/ticket/6861).
141 var currDir
= listArray
[ currentIndex
- 1 ].element
.getDirection( 1 ),
142 listData
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, null, currentIndex
, paragraphMode
, currDir
!= orgDir
? orgDir : null );
144 // If the next block is an <li> with another list tree as the first
145 // child, we'll need to append a filler (<br>/NBSP) or the list item
146 // wouldn't be editable. (http://dev.ckeditor.com/ticket/6724)
147 if ( !currentListItem
.getChildCount() && CKEDITOR
.env
.needsNbspFiller
&& doc
.$.documentMode
<= 7 )
148 currentListItem
.append( doc
.createText( '\xa0' ) );
149 currentListItem
.append( listData
.listNode
);
150 currentIndex
= listData
.nextIndex
;
151 } else if ( item
.indent
== -1 && !baseIndex
&& itemGrandParent
) {
152 if ( listNodeNames
[ itemGrandParent
.getName() ] ) {
153 currentListItem
= item
.element
.clone( false, true );
154 if ( orgDir
!= itemGrandParent
.getDirection( 1 ) )
155 currentListItem
.setAttribute( 'dir', orgDir
);
157 currentListItem
= new CKEDITOR
.dom
.documentFragment( doc
);
160 // Migrate all children to the new container,
161 // apply the proper text direction.
162 var dirLoose
= itemGrandParent
.getDirection( 1 ) != orgDir
,
164 className
= li
.getAttribute( 'class' ),
165 style
= li
.getAttribute( 'style' );
167 var needsBlock
= currentListItem
.type
== CKEDITOR
.NODE_DOCUMENT_FRAGMENT
&& ( paragraphMode
!= CKEDITOR
.ENTER_BR
|| dirLoose
|| style
|| className
);
170 count
= item
.contents
.length
,
173 for ( i
= 0; i
< count
; i
++ ) {
174 child
= item
.contents
[ i
];
176 // Append bookmark if we can, or cache it and append it when we'll know
177 // what to do with it. Generally - we want to keep it next to its original neighbour.
178 // Exception: if bookmark is the only child it hasn't got any neighbour, so handle it normally
179 // (wrap with block if needed).
180 if ( bookmarks( child
) && count
> 1 ) {
181 // If we don't need block, it's simple - append bookmark directly to the current list item.
183 currentListItem
.append( child
.clone( 1, 1 ) );
185 cachedBookmark
= child
.clone( 1, 1 );
187 // Block content goes directly to the current list item, without wrapping.
188 else if ( child
.type
== CKEDITOR
.NODE_ELEMENT
&& child
.isBlockBoundary() ) {
189 // Apply direction on content blocks.
190 if ( dirLoose
&& !child
.getDirection() )
191 child
.setAttribute( 'dir', orgDir
);
193 inheritInlineStyles( li
, child
);
195 className
&& child
.addClass( className
);
197 // Close the block which we started for inline content.
199 // Append bookmark directly before current child.
200 if ( cachedBookmark
) {
201 currentListItem
.append( cachedBookmark
);
202 cachedBookmark
= null;
204 // Append this block element to the list item.
205 currentListItem
.append( child
.clone( 1, 1 ) );
207 // Some inline content was found - wrap it with block and append that
208 // block to the current list item or append it to the block previously created.
209 else if ( needsBlock
) {
210 // Establish new block to hold text direction and styles.
212 block
= doc
.createElement( paragraphName
);
213 currentListItem
.append( block
);
214 dirLoose
&& block
.setAttribute( 'dir', orgDir
);
217 // Copy over styles to new block;
218 style
&& block
.setAttribute( 'style', style
);
219 className
&& block
.setAttribute( 'class', className
);
221 // Append bookmark directly before current child.
222 if ( cachedBookmark
) {
223 block
.append( cachedBookmark
);
224 cachedBookmark
= null;
226 block
.append( child
.clone( 1, 1 ) );
228 // E.g. BR mode - inline content appended directly to the list item.
230 currentListItem
.append( child
.clone( 1, 1 ) );
234 // No content after bookmark - append it to the block if we had one
235 // or directly to the current list item if we finished directly in the current list item.
236 if ( cachedBookmark
) {
237 ( block
|| currentListItem
).append( cachedBookmark
);
238 cachedBookmark
= null;
241 if ( currentListItem
.type
== CKEDITOR
.NODE_DOCUMENT_FRAGMENT
&& currentIndex
!= listArray
.length
- 1 ) {
244 // Remove bogus <br> if this browser uses them.
245 if ( CKEDITOR
.env
.needsBrFiller
) {
246 last
= currentListItem
.getLast();
247 if ( last
&& last
.type
== CKEDITOR
.NODE_ELEMENT
&& last
.is( 'br' ) )
251 // If the last element is not a block, append <br> to separate merged list items.
252 last
= currentListItem
.getLast( nonEmpty
);
253 if ( !( last
&& last
.type
== CKEDITOR
.NODE_ELEMENT
&& last
.is( CKEDITOR
.dtd
.$block
) ) )
254 currentListItem
.append( doc
.createElement( 'br' ) );
257 var currentListItemName
= currentListItem
.$.nodeName
.toLowerCase();
258 if ( currentListItemName
== 'div' || currentListItemName
== 'p' ) {
259 currentListItem
.appendBogus();
261 retval
.append( currentListItem
);
270 if ( listArray
.length
<= currentIndex
|| Math
.max( listArray
[ currentIndex
].indent
, 0 ) < indentLevel
)
275 var currentNode
= retval
.getFirst();
277 while ( currentNode
) {
278 if ( currentNode
.type
== CKEDITOR
.NODE_ELEMENT
) {
279 // Clear marker attributes for the new list tree made of cloned nodes, if any.
280 CKEDITOR
.dom
.element
.clearMarkers( database
, currentNode
);
282 // Clear redundant direction attribute specified on list items.
283 if ( currentNode
.getName() in CKEDITOR
.dtd
.$listItem
)
284 cleanUpDirection( currentNode
);
287 currentNode
= currentNode
.getNextSourceNode();
291 return { listNode: retval
, nextIndex: currentIndex
};
295 function changeListType( editor
, groupObj
, database
, listsCreated
) {
296 // This case is easy...
297 // 1. Convert the whole list into a one-dimensional array.
298 // 2. Change the list type by modifying the array.
299 // 3. Recreate the whole list by converting the array to a list.
300 // 4. Replace the original list with the recreated list.
301 var listArray
= CKEDITOR
.plugins
.list
.listToArray( groupObj
.root
, database
),
302 selectedListItems
= [];
304 for ( var i
= 0; i
< groupObj
.contents
.length
; i
++ ) {
305 var itemNode
= groupObj
.contents
[ i
];
306 itemNode
= itemNode
.getAscendant( 'li', true );
307 if ( !itemNode
|| itemNode
.getCustomData( 'list_item_processed' ) )
309 selectedListItems
.push( itemNode
);
310 CKEDITOR
.dom
.element
.setMarker( database
, itemNode
, 'list_item_processed', true );
313 var root
= groupObj
.root
,
314 doc
= root
.getDocument(),
315 listNode
, newListNode
;
317 for ( i
= 0; i
< selectedListItems
.length
; i
++ ) {
318 var listIndex
= selectedListItems
[ i
].getCustomData( 'listarray_index' );
319 listNode
= listArray
[ listIndex
].parent
;
321 // Switch to new list node for this particular item.
322 if ( !listNode
.is( this.type
) ) {
323 newListNode
= doc
.createElement( this.type
);
324 // Copy all attributes, except from 'start' and 'type'.
325 listNode
.copyAttributes( newListNode
, { start: 1, type: 1 } );
326 // The list-style-type property should be ignored.
327 newListNode
.removeStyle( 'list-style-type' );
328 listArray
[ listIndex
].parent
= newListNode
;
332 var newList
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, database
, null, editor
.config
.enterMode
);
334 length
= newList
.listNode
.getChildCount();
335 for ( i
= 0; i
< length
&& ( child
= newList
.listNode
.getChild( i
) ); i
++ ) {
336 if ( child
.getName() == this.type
)
337 listsCreated
.push( child
);
339 newList
.listNode
.replace( groupObj
.root
);
341 editor
.fire( 'contentDomInvalidated' );
344 function createList( editor
, groupObj
, listsCreated
) {
345 var contents
= groupObj
.contents
,
346 doc
= groupObj
.root
.getDocument(),
349 // It is possible to have the contents returned by DomRangeIterator to be the same as the root.
350 // e.g. when we're running into table cells.
351 // In such a case, enclose the childNodes of contents[0] into a <div>.
352 if ( contents
.length
== 1 && contents
[ 0 ].equals( groupObj
.root
) ) {
353 var divBlock
= doc
.createElement( 'div' );
354 contents
[ 0 ].moveChildren
&& contents
[ 0 ].moveChildren( divBlock
);
355 contents
[ 0 ].append( divBlock
);
356 contents
[ 0 ] = divBlock
;
359 // Calculate the common parent node of all content blocks.
360 var commonParent
= groupObj
.contents
[ 0 ].getParent();
361 for ( var i
= 0; i
< contents
.length
; i
++ )
362 commonParent
= commonParent
.getCommonAncestor( contents
[ i
].getParent() );
364 var useComputedState
= editor
.config
.useComputedState
,
365 listDir
, explicitDirection
;
367 useComputedState
= useComputedState
=== undefined || useComputedState
;
369 // We want to insert things that are in the same tree level only, so calculate the contents again
370 // by expanding the selected blocks to the same tree level.
371 for ( i
= 0; i
< contents
.length
; i
++ ) {
372 var contentNode
= contents
[ i
],
374 while ( ( parentNode
= contentNode
.getParent() ) ) {
375 if ( parentNode
.equals( commonParent
) ) {
376 listContents
.push( contentNode
);
378 // Determine the lists's direction.
379 if ( !explicitDirection
&& contentNode
.getDirection() )
380 explicitDirection
= 1;
382 var itemDir
= contentNode
.getDirection( useComputedState
);
384 if ( listDir
!== null ) {
385 // If at least one LI have a different direction than current listDir, we can't have listDir.
386 if ( listDir
&& listDir
!= itemDir
)
394 contentNode
= parentNode
;
398 if ( listContents
.length
< 1 )
401 // Insert the list to the DOM tree.
402 var insertAnchor
= listContents
[ listContents
.length
- 1 ].getNext(),
403 listNode
= doc
.createElement( this.type
);
405 listsCreated
.push( listNode
);
407 var contentBlock
, listItem
;
409 while ( listContents
.length
) {
410 contentBlock
= listContents
.shift();
411 listItem
= doc
.createElement( 'li' );
413 // If current block should be preserved, append it to list item instead of
414 // transforming it to <li> element.
415 if ( shouldPreserveBlock( contentBlock
) )
416 contentBlock
.appendTo( listItem
);
418 contentBlock
.copyAttributes( listItem
);
419 // Remove direction attribute after it was merged into list root. (http://dev.ckeditor.com/ticket/7657)
420 if ( listDir
&& contentBlock
.getDirection() ) {
421 listItem
.removeStyle( 'direction' );
422 listItem
.removeAttribute( 'dir' );
424 contentBlock
.moveChildren( listItem
);
425 contentBlock
.remove();
428 listItem
.appendTo( listNode
);
431 // Apply list root dir only if it has been explicitly declared.
432 if ( listDir
&& explicitDirection
)
433 listNode
.setAttribute( 'dir', listDir
);
436 listNode
.insertBefore( insertAnchor
);
438 listNode
.appendTo( commonParent
);
441 function removeList( editor
, groupObj
, database
) {
442 // This is very much like the change list type operation.
443 // Except that we're changing the selected items' indent to -1 in the list array.
444 var listArray
= CKEDITOR
.plugins
.list
.listToArray( groupObj
.root
, database
),
445 selectedListItems
= [];
447 for ( var i
= 0; i
< groupObj
.contents
.length
; i
++ ) {
448 var itemNode
= groupObj
.contents
[ i
];
449 itemNode
= itemNode
.getAscendant( 'li', true );
450 if ( !itemNode
|| itemNode
.getCustomData( 'list_item_processed' ) )
452 selectedListItems
.push( itemNode
);
453 CKEDITOR
.dom
.element
.setMarker( database
, itemNode
, 'list_item_processed', true );
456 var lastListIndex
= null;
457 for ( i
= 0; i
< selectedListItems
.length
; i
++ ) {
458 var listIndex
= selectedListItems
[ i
].getCustomData( 'listarray_index' );
459 listArray
[ listIndex
].indent
= -1;
460 lastListIndex
= listIndex
;
463 // After cutting parts of the list out with indent=-1, we still have to maintain the array list
464 // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
465 // list cannot be converted back to a real DOM list.
466 for ( i
= lastListIndex
+ 1; i
< listArray
.length
; i
++ ) {
467 if ( listArray
[ i
].indent
> listArray
[ i
- 1 ].indent
+ 1 ) {
468 var indentOffset
= listArray
[ i
- 1 ].indent
+ 1 - listArray
[ i
].indent
;
469 var oldIndent
= listArray
[ i
].indent
;
470 while ( listArray
[ i
] && listArray
[ i
].indent
>= oldIndent
) {
471 listArray
[ i
].indent
+= indentOffset
;
478 var newList
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, database
, null, editor
.config
.enterMode
, groupObj
.root
.getAttribute( 'dir' ) );
480 // Compensate <br> before/after the list node if the surrounds are non-blocks.(http://dev.ckeditor.com/ticket/3836)
481 var docFragment
= newList
.listNode
,
482 boundaryNode
, siblingNode
;
484 function compensateBrs( isStart
) {
486 ( boundaryNode
= docFragment
[ isStart
? 'getFirst' : 'getLast' ]() ) &&
487 !( boundaryNode
.is
&& boundaryNode
.isBlockBoundary() ) &&
488 ( siblingNode
= groupObj
.root
[ isStart
? 'getPrevious' : 'getNext' ]( CKEDITOR
.dom
.walker
.invisible( true ) ) ) &&
489 !( siblingNode
.is
&& siblingNode
.isBlockBoundary( { br: 1 } ) )
491 editor
.document
.createElement( 'br' )[ isStart
? 'insertBefore' : 'insertAfter' ]( boundaryNode
);
494 compensateBrs( true );
497 docFragment
.replace( groupObj
.root
);
499 editor
.fire( 'contentDomInvalidated' );
502 var headerTagRegex
= /^h[1-6]$/;
504 // Checks wheather this block should be element preserved (not transformed to <li>) when creating list.
505 function shouldPreserveBlock( block
) {
507 // http://dev.ckeditor.com/ticket/5335
509 // http://dev.ckeditor.com/ticket/5271 - this is a header.
510 headerTagRegex
.test( block
.getName() ) ||
511 // 11083 - this is a non-editable element.
512 block
.getAttribute( 'contenteditable' ) == 'false'
516 function listCommand( name
, type
) {
520 this.allowedContent
= type
+ ' li';
521 this.requiredContent
= type
;
524 var elementType
= CKEDITOR
.dom
.walker
.nodeType( CKEDITOR
.NODE_ELEMENT
);
526 // Merge child nodes with direction preserved. (http://dev.ckeditor.com/ticket/7448)
527 function mergeChildren( from, into
, refNode
, forward
) {
529 while ( ( child
= from[ forward
? 'getLast' : 'getFirst' ]( elementType
) ) ) {
530 if ( ( itemDir
= child
.getDirection( 1 ) ) !== into
.getDirection( 1 ) )
531 child
.setAttribute( 'dir', itemDir
);
535 refNode
? child
[ forward
? 'insertBefore' : 'insertAfter' ]( refNode
) : into
.append( child
, forward
);
539 listCommand
.prototype = {
540 exec: function( editor
) {
541 // Run state check first of all.
542 this.refresh( editor
, editor
.elementPath() );
544 var config
= editor
.config
,
545 selection
= editor
.getSelection(),
546 ranges
= selection
&& selection
.getRanges();
548 // Midas lists rule #1 says we can create a list even in an empty document.
549 // But DOM iterator wouldn't run if the document is really empty.
550 // So create a paragraph if the document is empty and we're going to create a list.
551 if ( this.state
== CKEDITOR
.TRISTATE_OFF
) {
552 var editable
= editor
.editable();
553 if ( !editable
.getFirst( nonEmpty
) ) {
554 config
.enterMode
== CKEDITOR
.ENTER_BR
? editable
.appendBogus() : ranges
[ 0 ].fixBlock( 1, config
.enterMode
== CKEDITOR
.ENTER_P
? 'p' : 'div' );
556 selection
.selectRanges( ranges
);
558 // Maybe a single range there enclosing the whole list,
559 // turn on the list state manually(http://dev.ckeditor.com/ticket/4129).
561 var range
= ranges
.length
== 1 && ranges
[ 0 ],
562 enclosedNode
= range
&& range
.getEnclosedNode();
563 if ( enclosedNode
&& enclosedNode
.is
&& this.type
== enclosedNode
.getName() )
564 this.setState( CKEDITOR
.TRISTATE_ON
);
568 var bookmarks
= selection
.createBookmarks( true );
570 // Group the blocks up because there are many cases where multiple lists have to be created,
571 // or multiple lists have to be cancelled.
574 rangeIterator
= ranges
.createIterator(),
577 while ( ( range
= rangeIterator
.getNextRange() ) && ++index
) {
578 var boundaryNodes
= range
.getBoundaryNodes(),
579 startNode
= boundaryNodes
.startNode
,
580 endNode
= boundaryNodes
.endNode
;
582 if ( startNode
.type
== CKEDITOR
.NODE_ELEMENT
&& startNode
.getName() == 'td' )
583 range
.setStartAt( boundaryNodes
.startNode
, CKEDITOR
.POSITION_AFTER_START
);
585 if ( endNode
.type
== CKEDITOR
.NODE_ELEMENT
&& endNode
.getName() == 'td' )
586 range
.setEndAt( boundaryNodes
.endNode
, CKEDITOR
.POSITION_BEFORE_END
);
588 var iterator
= range
.createIterator(),
591 iterator
.forceBrBreak
= ( this.state
== CKEDITOR
.TRISTATE_OFF
);
593 while ( ( block
= iterator
.getNextParagraph() ) ) {
594 // Avoid duplicate blocks get processed across ranges.
595 if ( block
.getCustomData( 'list_block' ) )
598 CKEDITOR
.dom
.element
.setMarker( database
, block
, 'list_block', 1 );
600 var path
= editor
.elementPath( block
),
601 pathElements
= path
.elements
,
602 pathElementsCount
= pathElements
.length
,
604 blockLimit
= path
.blockLimit
,
607 // First, try to group by a list ancestor.
608 for ( var i
= pathElementsCount
- 1; i
>= 0 && ( element
= pathElements
[ i
] ); i
-- ) {
609 // Don't leak outside block limit (http://dev.ckeditor.com/ticket/3940).
610 if ( listNodeNames
[ element
.getName() ] && blockLimit
.contains( element
) ) {
611 // If we've encountered a list inside a block limit
612 // The last group object of the block limit element should
613 // no longer be valid. Since paragraphs after the list
614 // should belong to a different group of paragraphs before
615 // the list. (Bug http://dev.ckeditor.com/ticket/1309)
616 blockLimit
.removeCustomData( 'list_group_object_' + index
);
618 var groupObj
= element
.getCustomData( 'list_group_object' );
620 groupObj
.contents
.push( block
);
622 groupObj
= { root: element
, contents: [ block
] };
623 listGroups
.push( groupObj
);
624 CKEDITOR
.dom
.element
.setMarker( database
, element
, 'list_group_object', groupObj
);
634 // No list ancestor? Group by block limit, but don't mix contents from different ranges.
635 var root
= blockLimit
;
636 if ( root
.getCustomData( 'list_group_object_' + index
) )
637 root
.getCustomData( 'list_group_object_' + index
).contents
.push( block
);
639 groupObj
= { root: root
, contents: [ block
] };
640 CKEDITOR
.dom
.element
.setMarker( database
, root
, 'list_group_object_' + index
, groupObj
);
641 listGroups
.push( groupObj
);
646 // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
647 // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
648 // at the group that's not rooted at lists. So we have three cases to handle.
649 var listsCreated
= [];
650 while ( listGroups
.length
> 0 ) {
651 groupObj
= listGroups
.shift();
652 if ( this.state
== CKEDITOR
.TRISTATE_OFF
) {
653 if ( listNodeNames
[ groupObj
.root
.getName() ] )
654 changeListType
.call( this, editor
, groupObj
, database
, listsCreated
);
656 createList
.call( this, editor
, groupObj
, listsCreated
);
657 } else if ( this.state
== CKEDITOR
.TRISTATE_ON
&& listNodeNames
[ groupObj
.root
.getName() ] ) {
658 removeList
.call( this, editor
, groupObj
, database
);
662 // For all new lists created, merge into adjacent, same type lists.
663 for ( i
= 0; i
< listsCreated
.length
; i
++ )
664 mergeListSiblings( listsCreated
[ i
] );
666 // Clean up, restore selection and update toolbar button states.
667 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
668 selection
.selectBookmarks( bookmarks
);
672 refresh: function( editor
, path
) {
673 var list
= path
.contains( listNodeNames
, 1 ),
674 limit
= path
.blockLimit
|| path
.root
;
676 // 1. Only a single type of list activate.
677 // 2. Do not show list outside of block limit.
678 if ( list
&& limit
.contains( list
) )
679 this.setState( list
.is( this.type
) ? CKEDITOR
.TRISTATE_ON : CKEDITOR
.TRISTATE_OFF
);
681 this.setState( CKEDITOR
.TRISTATE_OFF
);
685 // Merge list adjacent, of same type lists.
686 function mergeListSiblings( listNode
) {
688 function mergeSibling( rtl
) {
689 var sibling
= listNode
[ rtl
? 'getPrevious' : 'getNext' ]( nonEmpty
);
690 if ( sibling
&& sibling
.type
== CKEDITOR
.NODE_ELEMENT
&& sibling
.is( listNode
.getName() ) ) {
691 // Move children order by merge direction.(http://dev.ckeditor.com/ticket/3820)
692 mergeChildren( listNode
, sibling
, null, !rtl
);
703 // Check if node is block element that recieves text.
704 function isTextBlock( node
) {
705 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& ( node
.getName() in CKEDITOR
.dtd
.$block
|| node
.getName() in CKEDITOR
.dtd
.$listItem
) && CKEDITOR
.dtd
[ node
.getName() ][ '#' ];
708 // Join visually two block lines.
709 function joinNextLineToCursor( editor
, cursor
, nextCursor
) {
710 editor
.fire( 'saveSnapshot' );
712 // Merge with previous block's content.
713 nextCursor
.enlarge( CKEDITOR
.ENLARGE_LIST_ITEM_CONTENTS
);
714 var frag
= nextCursor
.extractContents();
716 cursor
.trim( false, true );
717 var bm
= cursor
.createBookmark();
719 // Kill original bogus;
720 var currentPath
= new CKEDITOR
.dom
.elementPath( cursor
.startContainer
),
721 pathBlock
= currentPath
.block
,
722 currentBlock
= currentPath
.lastElement
.getAscendant( 'li', 1 ) || pathBlock
,
723 nextPath
= new CKEDITOR
.dom
.elementPath( nextCursor
.startContainer
),
724 nextLi
= nextPath
.contains( CKEDITOR
.dtd
.$listItem
),
725 nextList
= nextPath
.contains( CKEDITOR
.dtd
.$list
),
728 // Remove bogus node the current block/pseudo block.
730 var bogus
= pathBlock
.getBogus();
731 bogus
&& bogus
.remove();
733 else if ( nextList
) {
734 last
= nextList
.getPrevious( nonEmpty
);
735 if ( last
&& blockBogus( last
) )
739 // Kill the tail br in extracted.
740 last
= frag
.getLast();
741 if ( last
&& last
.type
== CKEDITOR
.NODE_ELEMENT
&& last
.is( 'br' ) )
744 // Insert fragment at the range position.
745 var nextNode
= cursor
.startContainer
.getChild( cursor
.startOffset
);
747 frag
.insertBefore( nextNode
);
749 cursor
.startContainer
.append( frag
);
751 // Move the sub list nested in the next list item.
753 var sublist
= getSubList( nextLi
);
755 // If next line is in the sub list of the current list item.
756 if ( currentBlock
.contains( nextLi
) ) {
757 mergeChildren( sublist
, nextLi
.getParent(), nextLi
);
760 // Migrate the sub list to current list item.
762 currentBlock
.append( sublist
);
767 var nextBlock
, parent
;
768 // Remove any remaining zombies path blocks at the end after line merged.
769 while ( nextCursor
.checkStartOfBlock() && nextCursor
.checkEndOfBlock() ) {
770 nextPath
= nextCursor
.startPath();
771 nextBlock
= nextPath
.block
;
773 // Abort when nothing to be removed (http://dev.ckeditor.com/ticket/10890).
777 // Check if also to remove empty list.
778 if ( nextBlock
.is( 'li' ) ) {
779 parent
= nextBlock
.getParent();
780 if ( nextBlock
.equals( parent
.getLast( nonEmpty
) ) && nextBlock
.equals( parent
.getFirst( nonEmpty
) ) )
784 nextCursor
.moveToPosition( nextBlock
, CKEDITOR
.POSITION_BEFORE_START
);
788 // Check if need to further merge with the list resides after the merged block. (http://dev.ckeditor.com/ticket/9080)
789 var walkerRng
= nextCursor
.clone(), editable
= editor
.editable();
790 walkerRng
.setEndAt( editable
, CKEDITOR
.POSITION_BEFORE_END
);
791 var walker
= new CKEDITOR
.dom
.walker( walkerRng
);
792 walker
.evaluator = function( node
) {
793 return nonEmpty( node
) && !blockBogus( node
);
795 var next
= walker
.next();
796 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.getName() in CKEDITOR
.dtd
.$list
)
797 mergeListSiblings( next
);
799 cursor
.moveToBookmark( bm
);
801 // Make fresh selection.
804 editor
.fire( 'saveSnapshot' );
807 function getSubList( li
) {
808 var last
= li
.getLast( nonEmpty
);
809 return last
&& last
.type
== CKEDITOR
.NODE_ELEMENT
&& last
.getName() in listNodeNames
? last : null;
812 CKEDITOR
.plugins
.add( 'list', {
813 // jscs:disable maximumLineLength
814 lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
815 // jscs:enable maximumLineLength
816 icons: 'bulletedlist,bulletedlist-rtl,numberedlist,numberedlist-rtl', // %REMOVE_LINE_CORE%
817 hidpi: true, // %REMOVE_LINE_CORE%
818 requires: 'indentlist',
819 init: function( editor
) {
820 if ( editor
.blockless
)
823 // Register commands.
824 editor
.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) );
825 editor
.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );
827 // Register the toolbar button.
828 if ( editor
.ui
.addButton
) {
829 editor
.ui
.addButton( 'NumberedList', {
830 label: editor
.lang
.list
.numberedlist
,
831 command: 'numberedlist',
835 editor
.ui
.addButton( 'BulletedList', {
836 label: editor
.lang
.list
.bulletedlist
,
837 command: 'bulletedlist',
843 // Handled backspace/del key to join list items. (http://dev.ckeditor.com/ticket/8248,http://dev.ckeditor.com/ticket/9080)
844 editor
.on( 'key', function( evt
) {
845 // Use getKey directly in order to ignore modifiers.
846 // Justification: http://dev.ckeditor.com/ticket/11861#comment:13
847 var key
= evt
.data
.domEvent
.getKey(), li
;
850 if ( editor
.mode
== 'wysiwyg' && key
in { 8: 1, 46: 1 } ) {
851 var sel
= editor
.getSelection(),
852 range
= sel
.getRanges()[ 0 ],
853 path
= range
&& range
.startPath();
855 if ( !range
|| !range
.collapsed
)
858 var isBackspace
= key
== 8;
859 var editable
= editor
.editable();
860 var walker
= new CKEDITOR
.dom
.walker( range
.clone() );
861 walker
.evaluator = function( node
) {
862 return nonEmpty( node
) && !blockBogus( node
);
864 // Backspace/Del behavior at the start/end of table is handled in core.
865 walker
.guard = function( node
, isOut
) {
866 return !( isOut
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( 'table' ) );
869 var cursor
= range
.clone();
872 var previous
, joinWith
;
874 // Join a sub list's first line, with the previous visual line in parent.
876 ( previous
= path
.contains( listNodeNames
) ) &&
877 range
.checkBoundaryOfElement( previous
, CKEDITOR
.START
) &&
878 ( previous
= previous
.getParent() ) && previous
.is( 'li' ) &&
879 ( previous
= getSubList( previous
) )
882 previous
= previous
.getPrevious( nonEmpty
);
883 // Place cursor before the nested list.
884 cursor
.moveToPosition(
885 previous
&& blockBogus( previous
) ? previous : joinWith
,
886 CKEDITOR
.POSITION_BEFORE_START
);
888 // Join any line following a list, with the last visual line of the list.
890 walker
.range
.setStartAt( editable
, CKEDITOR
.POSITION_AFTER_START
);
891 walker
.range
.setEnd( range
.startContainer
, range
.startOffset
);
893 previous
= walker
.previous();
896 previous
&& previous
.type
== CKEDITOR
.NODE_ELEMENT
&&
897 ( previous
.getName() in listNodeNames
||
898 previous
.is( 'li' ) )
900 if ( !previous
.is( 'li' ) ) {
901 walker
.range
.selectNodeContents( previous
);
903 walker
.evaluator
= isTextBlock
;
904 previous
= walker
.previous();
908 // Place cursor at the end of previous block.
909 cursor
.moveToElementEditEnd( joinWith
);
911 // And then just before end of closest block element (http://dev.ckeditor.com/ticket/12729).
912 cursor
.moveToPosition( cursor
.endPath().block
, CKEDITOR
.POSITION_BEFORE_END
);
917 joinNextLineToCursor( editor
, cursor
, range
);
921 var list
= path
.contains( listNodeNames
);
922 // Backspace pressed at the start of list outdents the first list item. (http://dev.ckeditor.com/ticket/9129)
923 if ( list
&& range
.checkBoundaryOfElement( list
, CKEDITOR
.START
) ) {
924 li
= list
.getFirst( nonEmpty
);
926 if ( range
.checkBoundaryOfElement( li
, CKEDITOR
.START
) ) {
927 previous
= list
.getPrevious( nonEmpty
);
929 // Only if the list item contains a sub list, do nothing but
930 // simply move cursor backward one character.
931 if ( getSubList( li
) ) {
933 range
.moveToElementEditEnd( previous
);
940 editor
.execCommand( 'outdent' );
950 li
= path
.contains( 'li' );
953 walker
.range
.setEndAt( editable
, CKEDITOR
.POSITION_BEFORE_END
);
955 var last
= li
.getLast( nonEmpty
);
956 var block
= last
&& isTextBlock( last
) ? last : li
;
958 // Indicate cursor at the visual end of an list item.
961 next
= walker
.next();
963 // When list item contains a sub list.
965 next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&&
966 next
.getName() in listNodeNames
&&
971 // Move to the first item in sub list.
972 next
= walker
.next();
974 // Right at the end of list item.
975 else if ( range
.checkBoundaryOfElement( block
, CKEDITOR
.END
) ) {
979 if ( isAtEnd
&& next
) {
980 // Put cursor range there.
981 nextLine
= range
.clone();
982 nextLine
.moveToElementEditStart( next
);
984 // http://dev.ckeditor.com/ticket/13409
985 // For the following case and similar
989 // <p><a href="#one"><em>x^</em></a></p>
991 // <li><span>y</span></li>
995 if ( isAtEnd
== 1 ) {
996 // Move the cursor to <em> if attached to "x" text node.
999 // Abort if the range is attached directly in <li>, like
1005 // <li><span>y</span></li>
1009 if ( !cursor
.startContainer
.equals( li
) ) {
1010 var node
= cursor
.startContainer
,
1011 farthestInlineAscendant
;
1013 // Find <a>, which is farthest from <em> but still inline element.
1014 while ( node
.is( CKEDITOR
.dtd
.$inline
) ) {
1015 farthestInlineAscendant
= node
;
1016 node
= node
.getParent();
1019 // Move the range so it does not contain inline elements.
1020 // It prevents <span> from being included in <em>.
1024 // <p><a href="#one"><em>x</em></a>^</p>
1026 // <li><span>y</span></li>
1035 // <p><a href="#one"><em>x^<span>y</span></em></a></p>
1039 // pressing DELETE produces
1043 // <p><a href="#one"><em>x</em></a>^<span>y</span></p>
1046 if ( farthestInlineAscendant
) {
1047 cursor
.moveToPosition( farthestInlineAscendant
, CKEDITOR
.POSITION_AFTER_END
);
1052 // Moving `cursor` and `next line` only when at the end literally (http://dev.ckeditor.com/ticket/12729).
1053 if ( isAtEnd
== 2 ) {
1054 cursor
.moveToPosition( cursor
.endPath().block
, CKEDITOR
.POSITION_BEFORE_END
);
1056 // Next line might be text node not wrapped in block element.
1057 if ( nextLine
.endPath().block
) {
1058 nextLine
.moveToPosition( nextLine
.endPath().block
, CKEDITOR
.POSITION_AFTER_START
);
1062 joinNextLineToCursor( editor
, cursor
, nextLine
);
1066 // Handle Del key pressed before the list.
1067 walker
.range
.setEndAt( editable
, CKEDITOR
.POSITION_BEFORE_END
);
1068 next
= walker
.next();
1070 if ( next
&& next
.type
== CKEDITOR
.NODE_ELEMENT
&& next
.is( listNodeNames
) ) {
1072 next
= next
.getFirst( nonEmpty
);
1074 // Simply remove the current empty block, move cursor to the
1076 if ( path
.block
&& range
.checkStartOfBlock() && range
.checkEndOfBlock() ) {
1077 path
.block
.remove();
1078 range
.moveToElementEditStart( next
);
1082 // Preventing the default (merge behavior), but simply move
1083 // the cursor one character forward if subsequent list item
1084 // contains sub list.
1085 else if ( getSubList( next
) ) {
1086 range
.moveToElementEditStart( next
);
1090 // Merge the first list item with the current line.
1092 nextLine
= range
.clone();
1093 nextLine
.moveToElementEditStart( next
);
1094 joinNextLineToCursor( editor
, cursor
, nextLine
);
1102 // The backspace/del could potentially put cursor at a bad position,
1103 // being it handled or not, check immediately the selection to have it fixed.
1104 setTimeout( function() {
1105 editor
.selectionChange( 1 );