]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/list/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / list / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /**
7 * @fileOverview Insert and remove numbered and bulleted lists.
8 */
9
10 ( function() {
11 var listNodeNames = { ol: 1, ul: 1 };
12
13 var whitespaces = CKEDITOR.dom.walker.whitespaces(),
14 bookmarks = CKEDITOR.dom.walker.bookmark(),
15 nonEmpty = function( node ) {
16 return !( whitespaces( node ) || bookmarks( node ) );
17 },
18 blockBogus = CKEDITOR.dom.walker.bogus();
19
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();
26
27 if ( dir == parentDir )
28 element.removeAttribute( 'dir' );
29 }
30 }
31
32 // Inherit inline styles from another element.
33 function inheritInlineStyles( parent, el ) {
34 var style = parent.getAttribute( 'style' );
35
36 // Put parent styles before child styles.
37 style && el.setAttribute( 'style', style.replace( /([^;])$/, '$1;' ) + ( el.getAttribute( 'style' ) || '' ) );
38 }
39
40 CKEDITOR.plugins.list = {
41 /**
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.
46 *
47 * @member CKEDITOR.plugins.list
48 * @todo params
49 */
50 listToArray: function( listNode, database, baseArray, baseIndentLevel, grandparentNode ) {
51 if ( !listNodeNames[ listNode.getName() ] )
52 return [];
53
54 if ( !baseIndentLevel )
55 baseIndentLevel = 0;
56 if ( !baseArray )
57 baseArray = [];
58
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 );
62
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 );
66
67 // It may be a text node or some funny stuff.
68 if ( listItem.$.nodeName.toLowerCase() != 'li' )
69 continue;
70
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();
76 } else {
77 itemObj.grandparent = grandparentNode;
78 }
79
80 if ( database )
81 CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );
82 baseArray.push( itemObj );
83
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 );
90 else
91 itemObj.contents.push( child );
92 }
93 }
94 return baseArray;
95 },
96
97 /**
98 * Convert our internal representation of a list back to a DOM forest.
99 *
100 * @member CKEDITOR.plugins.list
101 * @todo params
102 */
103 arrayToList: function( listArray, database, baseIndex, paragraphMode, dir ) {
104 if ( !baseIndex )
105 baseIndex = 0;
106 if ( !listArray || listArray.length < baseIndex + 1 )
107 return null;
108
109 var i,
110 doc = listArray[ baseIndex ].parent.getDocument(),
111 retval = new CKEDITOR.dom.documentFragment( doc ),
112 rootNode = null,
113 currentIndex = baseIndex,
114 indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),
115 currentListItem = null,
116 orgDir, block,
117 paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );
118
119 while ( 1 ) {
120 var item = listArray[ currentIndex ],
121 itemGrandParent = item.grandparent;
122
123 orgDir = item.element.getDirection( 1 );
124
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 );
130 }
131 currentListItem = rootNode.append( item.element.clone( 0, 1 ) );
132
133 if ( orgDir != rootNode.getDirection( 1 ) )
134 currentListItem.setAttribute( 'dir', orgDir );
135
136 for ( i = 0; i < item.contents.length; i++ )
137 currentListItem.append( item.contents[ i ].clone( 1, 1 ) );
138 currentIndex++;
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 );
143
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 );
156 } else {
157 currentListItem = new CKEDITOR.dom.documentFragment( doc );
158 }
159
160 // Migrate all children to the new container,
161 // apply the proper text direction.
162 var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir,
163 li = item.element,
164 className = li.getAttribute( 'class' ),
165 style = li.getAttribute( 'style' );
166
167 var needsBlock = currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose || style || className );
168
169 var child,
170 count = item.contents.length,
171 cachedBookmark;
172
173 for ( i = 0; i < count; i++ ) {
174 child = item.contents[ i ];
175
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.
182 if ( !needsBlock )
183 currentListItem.append( child.clone( 1, 1 ) );
184 else
185 cachedBookmark = child.clone( 1, 1 );
186 }
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 );
192
193 inheritInlineStyles( li, child );
194
195 className && child.addClass( className );
196
197 // Close the block which we started for inline content.
198 block = null;
199 // Append bookmark directly before current child.
200 if ( cachedBookmark ) {
201 currentListItem.append( cachedBookmark );
202 cachedBookmark = null;
203 }
204 // Append this block element to the list item.
205 currentListItem.append( child.clone( 1, 1 ) );
206 }
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.
211 if ( !block ) {
212 block = doc.createElement( paragraphName );
213 currentListItem.append( block );
214 dirLoose && block.setAttribute( 'dir', orgDir );
215 }
216
217 // Copy over styles to new block;
218 style && block.setAttribute( 'style', style );
219 className && block.setAttribute( 'class', className );
220
221 // Append bookmark directly before current child.
222 if ( cachedBookmark ) {
223 block.append( cachedBookmark );
224 cachedBookmark = null;
225 }
226 block.append( child.clone( 1, 1 ) );
227 }
228 // E.g. BR mode - inline content appended directly to the list item.
229 else {
230 currentListItem.append( child.clone( 1, 1 ) );
231 }
232 }
233
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;
239 }
240
241 if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && currentIndex != listArray.length - 1 ) {
242 var last;
243
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' ) )
248 last.remove();
249 }
250
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' ) );
255 }
256
257 var currentListItemName = currentListItem.$.nodeName.toLowerCase();
258 if ( currentListItemName == 'div' || currentListItemName == 'p' ) {
259 currentListItem.appendBogus();
260 }
261 retval.append( currentListItem );
262 rootNode = null;
263 currentIndex++;
264 } else {
265 return null;
266 }
267
268 block = null;
269
270 if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )
271 break;
272 }
273
274 if ( database ) {
275 var currentNode = retval.getFirst();
276
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 );
281
282 // Clear redundant direction attribute specified on list items.
283 if ( currentNode.getName() in CKEDITOR.dtd.$listItem )
284 cleanUpDirection( currentNode );
285 }
286
287 currentNode = currentNode.getNextSourceNode();
288 }
289 }
290
291 return { listNode: retval, nextIndex: currentIndex };
292 }
293 };
294
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 = [];
303
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' ) )
308 continue;
309 selectedListItems.push( itemNode );
310 CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
311 }
312
313 var root = groupObj.root,
314 doc = root.getDocument(),
315 listNode, newListNode;
316
317 for ( i = 0; i < selectedListItems.length; i++ ) {
318 var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' );
319 listNode = listArray[ listIndex ].parent;
320
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;
329 }
330 }
331
332 var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );
333 var child,
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 );
338 }
339 newList.listNode.replace( groupObj.root );
340
341 editor.fire( 'contentDomInvalidated' );
342 }
343
344 function createList( editor, groupObj, listsCreated ) {
345 var contents = groupObj.contents,
346 doc = groupObj.root.getDocument(),
347 listContents = [];
348
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;
357 }
358
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() );
363
364 var useComputedState = editor.config.useComputedState,
365 listDir, explicitDirection;
366
367 useComputedState = useComputedState === undefined || useComputedState;
368
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 ],
373 parentNode;
374 while ( ( parentNode = contentNode.getParent() ) ) {
375 if ( parentNode.equals( commonParent ) ) {
376 listContents.push( contentNode );
377
378 // Determine the lists's direction.
379 if ( !explicitDirection && contentNode.getDirection() )
380 explicitDirection = 1;
381
382 var itemDir = contentNode.getDirection( useComputedState );
383
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 )
387 listDir = null;
388 else
389 listDir = itemDir;
390 }
391
392 break;
393 }
394 contentNode = parentNode;
395 }
396 }
397
398 if ( listContents.length < 1 )
399 return;
400
401 // Insert the list to the DOM tree.
402 var insertAnchor = listContents[ listContents.length - 1 ].getNext(),
403 listNode = doc.createElement( this.type );
404
405 listsCreated.push( listNode );
406
407 var contentBlock, listItem;
408
409 while ( listContents.length ) {
410 contentBlock = listContents.shift();
411 listItem = doc.createElement( 'li' );
412
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 );
417 else {
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' );
423 }
424 contentBlock.moveChildren( listItem );
425 contentBlock.remove();
426 }
427
428 listItem.appendTo( listNode );
429 }
430
431 // Apply list root dir only if it has been explicitly declared.
432 if ( listDir && explicitDirection )
433 listNode.setAttribute( 'dir', listDir );
434
435 if ( insertAnchor )
436 listNode.insertBefore( insertAnchor );
437 else
438 listNode.appendTo( commonParent );
439 }
440
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 = [];
446
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' ) )
451 continue;
452 selectedListItems.push( itemNode );
453 CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );
454 }
455
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;
461 }
462
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;
472 i++;
473 }
474 i--;
475 }
476 }
477
478 var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, groupObj.root.getAttribute( 'dir' ) );
479
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;
483
484 function compensateBrs( isStart ) {
485 if (
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 } ) )
490 ) {
491 editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );
492 }
493 }
494 compensateBrs( true );
495 compensateBrs();
496
497 docFragment.replace( groupObj.root );
498
499 editor.fire( 'contentDomInvalidated' );
500 }
501
502 var headerTagRegex = /^h[1-6]$/;
503
504 // Checks wheather this block should be element preserved (not transformed to <li>) when creating list.
505 function shouldPreserveBlock( block ) {
506 return (
507 // http://dev.ckeditor.com/ticket/5335
508 block.is( 'pre' ) ||
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'
513 );
514 }
515
516 function listCommand( name, type ) {
517 this.name = name;
518 this.type = type;
519 this.context = type;
520 this.allowedContent = type + ' li';
521 this.requiredContent = type;
522 }
523
524 var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT );
525
526 // Merge child nodes with direction preserved. (http://dev.ckeditor.com/ticket/7448)
527 function mergeChildren( from, into, refNode, forward ) {
528 var child, itemDir;
529 while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) ) {
530 if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) )
531 child.setAttribute( 'dir', itemDir );
532
533 child.remove();
534
535 refNode ? child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) : into.append( child, forward );
536 }
537 }
538
539 listCommand.prototype = {
540 exec: function( editor ) {
541 // Run state check first of all.
542 this.refresh( editor, editor.elementPath() );
543
544 var config = editor.config,
545 selection = editor.getSelection(),
546 ranges = selection && selection.getRanges();
547
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' );
555
556 selection.selectRanges( ranges );
557 }
558 // Maybe a single range there enclosing the whole list,
559 // turn on the list state manually(http://dev.ckeditor.com/ticket/4129).
560 else {
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 );
565 }
566 }
567
568 var bookmarks = selection.createBookmarks( true );
569
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.
572 var listGroups = [],
573 database = {},
574 rangeIterator = ranges.createIterator(),
575 index = 0;
576
577 while ( ( range = rangeIterator.getNextRange() ) && ++index ) {
578 var boundaryNodes = range.getBoundaryNodes(),
579 startNode = boundaryNodes.startNode,
580 endNode = boundaryNodes.endNode;
581
582 if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )
583 range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );
584
585 if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )
586 range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );
587
588 var iterator = range.createIterator(),
589 block;
590
591 iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );
592
593 while ( ( block = iterator.getNextParagraph() ) ) {
594 // Avoid duplicate blocks get processed across ranges.
595 if ( block.getCustomData( 'list_block' ) )
596 continue;
597 else
598 CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );
599
600 var path = editor.elementPath( block ),
601 pathElements = path.elements,
602 pathElementsCount = pathElements.length,
603 processedFlag = 0,
604 blockLimit = path.blockLimit,
605 element;
606
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 );
617
618 var groupObj = element.getCustomData( 'list_group_object' );
619 if ( groupObj )
620 groupObj.contents.push( block );
621 else {
622 groupObj = { root: element, contents: [ block ] };
623 listGroups.push( groupObj );
624 CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );
625 }
626 processedFlag = 1;
627 break;
628 }
629 }
630
631 if ( processedFlag )
632 continue;
633
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 );
638 else {
639 groupObj = { root: root, contents: [ block ] };
640 CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );
641 listGroups.push( groupObj );
642 }
643 }
644 }
645
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 );
655 else
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 );
659 }
660 }
661
662 // For all new lists created, merge into adjacent, same type lists.
663 for ( i = 0; i < listsCreated.length; i++ )
664 mergeListSiblings( listsCreated[ i ] );
665
666 // Clean up, restore selection and update toolbar button states.
667 CKEDITOR.dom.element.clearAllMarkers( database );
668 selection.selectBookmarks( bookmarks );
669 editor.focus();
670 },
671
672 refresh: function( editor, path ) {
673 var list = path.contains( listNodeNames, 1 ),
674 limit = path.blockLimit || path.root;
675
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 );
680 else
681 this.setState( CKEDITOR.TRISTATE_OFF );
682 }
683 };
684
685 // Merge list adjacent, of same type lists.
686 function mergeListSiblings( listNode ) {
687
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 );
693
694 listNode.remove();
695 listNode = sibling;
696 }
697 }
698
699 mergeSibling();
700 mergeSibling( 1 );
701 }
702
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() ][ '#' ];
706 }
707
708 // Join visually two block lines.
709 function joinNextLineToCursor( editor, cursor, nextCursor ) {
710 editor.fire( 'saveSnapshot' );
711
712 // Merge with previous block's content.
713 nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS );
714 var frag = nextCursor.extractContents();
715
716 cursor.trim( false, true );
717 var bm = cursor.createBookmark();
718
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 ),
726 last;
727
728 // Remove bogus node the current block/pseudo block.
729 if ( pathBlock ) {
730 var bogus = pathBlock.getBogus();
731 bogus && bogus.remove();
732 }
733 else if ( nextList ) {
734 last = nextList.getPrevious( nonEmpty );
735 if ( last && blockBogus( last ) )
736 last.remove();
737 }
738
739 // Kill the tail br in extracted.
740 last = frag.getLast();
741 if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )
742 last.remove();
743
744 // Insert fragment at the range position.
745 var nextNode = cursor.startContainer.getChild( cursor.startOffset );
746 if ( nextNode )
747 frag.insertBefore( nextNode );
748 else
749 cursor.startContainer.append( frag );
750
751 // Move the sub list nested in the next list item.
752 if ( nextLi ) {
753 var sublist = getSubList( nextLi );
754 if ( sublist ) {
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 );
758 sublist.remove();
759 }
760 // Migrate the sub list to current list item.
761 else {
762 currentBlock.append( sublist );
763 }
764 }
765 }
766
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;
772
773 // Abort when nothing to be removed (http://dev.ckeditor.com/ticket/10890).
774 if ( !nextBlock )
775 break;
776
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 ) ) )
781 nextBlock = parent;
782 }
783
784 nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START );
785 nextBlock.remove();
786 }
787
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 );
794 };
795 var next = walker.next();
796 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list )
797 mergeListSiblings( next );
798
799 cursor.moveToBookmark( bm );
800
801 // Make fresh selection.
802 cursor.select();
803
804 editor.fire( 'saveSnapshot' );
805 }
806
807 function getSubList( li ) {
808 var last = li.getLast( nonEmpty );
809 return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null;
810 }
811
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 )
821 return;
822
823 // Register commands.
824 editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) );
825 editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );
826
827 // Register the toolbar button.
828 if ( editor.ui.addButton ) {
829 editor.ui.addButton( 'NumberedList', {
830 label: editor.lang.list.numberedlist,
831 command: 'numberedlist',
832 directional: true,
833 toolbar: 'list,10'
834 } );
835 editor.ui.addButton( 'BulletedList', {
836 label: editor.lang.list.bulletedlist,
837 command: 'bulletedlist',
838 directional: true,
839 toolbar: 'list,20'
840 } );
841 }
842
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;
848
849 // DEl/BACKSPACE
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();
854
855 if ( !range || !range.collapsed )
856 return;
857
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 );
863 };
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' ) );
867 };
868
869 var cursor = range.clone();
870
871 if ( isBackspace ) {
872 var previous, joinWith;
873
874 // Join a sub list's first line, with the previous visual line in parent.
875 if (
876 ( previous = path.contains( listNodeNames ) ) &&
877 range.checkBoundaryOfElement( previous, CKEDITOR.START ) &&
878 ( previous = previous.getParent() ) && previous.is( 'li' ) &&
879 ( previous = getSubList( previous ) )
880 ) {
881 joinWith = 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 );
887 }
888 // Join any line following a list, with the last visual line of the list.
889 else {
890 walker.range.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );
891 walker.range.setEnd( range.startContainer, range.startOffset );
892
893 previous = walker.previous();
894
895 if (
896 previous && previous.type == CKEDITOR.NODE_ELEMENT &&
897 ( previous.getName() in listNodeNames ||
898 previous.is( 'li' ) )
899 ) {
900 if ( !previous.is( 'li' ) ) {
901 walker.range.selectNodeContents( previous );
902 walker.reset();
903 walker.evaluator = isTextBlock;
904 previous = walker.previous();
905 }
906
907 joinWith = previous;
908 // Place cursor at the end of previous block.
909 cursor.moveToElementEditEnd( joinWith );
910
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 );
913 }
914 }
915
916 if ( joinWith ) {
917 joinNextLineToCursor( editor, cursor, range );
918 evt.cancel();
919 }
920 else {
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 );
925
926 if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) ) {
927 previous = list.getPrevious( nonEmpty );
928
929 // Only if the list item contains a sub list, do nothing but
930 // simply move cursor backward one character.
931 if ( getSubList( li ) ) {
932 if ( previous ) {
933 range.moveToElementEditEnd( previous );
934 range.select();
935 }
936
937 evt.cancel();
938 }
939 else {
940 editor.execCommand( 'outdent' );
941 evt.cancel();
942 }
943 }
944 }
945 }
946
947 } else {
948 var next, nextLine;
949
950 li = path.contains( 'li' );
951
952 if ( li ) {
953 walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
954
955 var last = li.getLast( nonEmpty );
956 var block = last && isTextBlock( last ) ? last : li;
957
958 // Indicate cursor at the visual end of an list item.
959 var isAtEnd = 0;
960
961 next = walker.next();
962
963 // When list item contains a sub list.
964 if (
965 next && next.type == CKEDITOR.NODE_ELEMENT &&
966 next.getName() in listNodeNames &&
967 next.equals( last )
968 ) {
969 isAtEnd = 1;
970
971 // Move to the first item in sub list.
972 next = walker.next();
973 }
974 // Right at the end of list item.
975 else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) ) {
976 isAtEnd = 2;
977 }
978
979 if ( isAtEnd && next ) {
980 // Put cursor range there.
981 nextLine = range.clone();
982 nextLine.moveToElementEditStart( next );
983
984 // http://dev.ckeditor.com/ticket/13409
985 // For the following case and similar
986 //
987 // <ul>
988 // <li>
989 // <p><a href="#one"><em>x^</em></a></p>
990 // <ul>
991 // <li><span>y</span></li>
992 // </ul>
993 // </li>
994 // </ul>
995 if ( isAtEnd == 1 ) {
996 // Move the cursor to <em> if attached to "x" text node.
997 cursor.optimize();
998
999 // Abort if the range is attached directly in <li>, like
1000 //
1001 // <ul>
1002 // <li>
1003 // x^
1004 // <ul>
1005 // <li><span>y</span></li>
1006 // </ul>
1007 // </li>
1008 // </ul>
1009 if ( !cursor.startContainer.equals( li ) ) {
1010 var node = cursor.startContainer,
1011 farthestInlineAscendant;
1012
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();
1017 }
1018
1019 // Move the range so it does not contain inline elements.
1020 // It prevents <span> from being included in <em>.
1021 //
1022 // <ul>
1023 // <li>
1024 // <p><a href="#one"><em>x</em></a>^</p>
1025 // <ul>
1026 // <li><span>y</span></li>
1027 // </ul>
1028 // </li>
1029 // </ul>
1030 //
1031 // so instead of
1032 //
1033 // <ul>
1034 // <li>
1035 // <p><a href="#one"><em>x^<span>y</span></em></a></p>
1036 // </li>
1037 // </ul>
1038 //
1039 // pressing DELETE produces
1040 //
1041 // <ul>
1042 // <li>
1043 // <p><a href="#one"><em>x</em></a>^<span>y</span></p>
1044 // </li>
1045 // </ul>
1046 if ( farthestInlineAscendant ) {
1047 cursor.moveToPosition( farthestInlineAscendant, CKEDITOR.POSITION_AFTER_END );
1048 }
1049 }
1050 }
1051
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 );
1055
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 );
1059 }
1060 }
1061
1062 joinNextLineToCursor( editor, cursor, nextLine );
1063 evt.cancel();
1064 }
1065 } else {
1066 // Handle Del key pressed before the list.
1067 walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );
1068 next = walker.next();
1069
1070 if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( listNodeNames ) ) {
1071 // The start <li>
1072 next = next.getFirst( nonEmpty );
1073
1074 // Simply remove the current empty block, move cursor to the
1075 // subsequent list.
1076 if ( path.block && range.checkStartOfBlock() && range.checkEndOfBlock() ) {
1077 path.block.remove();
1078 range.moveToElementEditStart( next );
1079 range.select();
1080 evt.cancel();
1081 }
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 );
1087 range.select();
1088 evt.cancel();
1089 }
1090 // Merge the first list item with the current line.
1091 else {
1092 nextLine = range.clone();
1093 nextLine.moveToElementEditStart( next );
1094 joinNextLineToCursor( editor, cursor, nextLine );
1095 evt.cancel();
1096 }
1097 }
1098 }
1099
1100 }
1101
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 );
1106 } );
1107 }
1108 } );
1109 }
1110 } );
1111 } )();