diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 17:45:33 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 18:00:33 +0100 |
commit | 7adcb81e4f83f98c468889aaa5a85558ba88c770 (patch) | |
tree | 0d6ede733777b29060b48df4afaa2c64bfbae276 /sources/plugins/list/plugin.js | |
download | connexionswing-ckeditor-component-4.5.6.tar.gz connexionswing-ckeditor-component-4.5.6.tar.zst connexionswing-ckeditor-component-4.5.6.zip |
Initial commit4.5.6
Diffstat (limited to 'sources/plugins/list/plugin.js')
-rw-r--r-- | sources/plugins/list/plugin.js | 1111 |
1 files changed, 1111 insertions, 0 deletions
diff --git a/sources/plugins/list/plugin.js b/sources/plugins/list/plugin.js new file mode 100644 index 00000000..267bd3b0 --- /dev/null +++ b/sources/plugins/list/plugin.js | |||
@@ -0,0 +1,1111 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, 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. (#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 (#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. (#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. (#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.(#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 | // #5335 | ||
508 | block.is( 'pre' ) || | ||
509 | // #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. (#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(#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 (#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 #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.(#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 (#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. (#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,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,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,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. (#8248,#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 (#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. (#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 | // #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 (#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 | } )(); | ||