]>
Commit | Line | Data |
---|---|---|
1 | /**\r | |
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.\r | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license\r | |
4 | */\r | |
5 | \r | |
6 | /**\r | |
7 | * @fileOverview Insert and remove numbered and bulleted lists.\r | |
8 | */\r | |
9 | \r | |
10 | ( function() {\r | |
11 | var listNodeNames = { ol: 1, ul: 1 };\r | |
12 | \r | |
13 | var whitespaces = CKEDITOR.dom.walker.whitespaces(),\r | |
14 | bookmarks = CKEDITOR.dom.walker.bookmark(),\r | |
15 | nonEmpty = function( node ) {\r | |
16 | return !( whitespaces( node ) || bookmarks( node ) );\r | |
17 | },\r | |
18 | blockBogus = CKEDITOR.dom.walker.bogus();\r | |
19 | \r | |
20 | function cleanUpDirection( element ) {\r | |
21 | var dir, parent, parentDir;\r | |
22 | if ( ( dir = element.getDirection() ) ) {\r | |
23 | parent = element.getParent();\r | |
24 | while ( parent && !( parentDir = parent.getDirection() ) )\r | |
25 | parent = parent.getParent();\r | |
26 | \r | |
27 | if ( dir == parentDir )\r | |
28 | element.removeAttribute( 'dir' );\r | |
29 | }\r | |
30 | }\r | |
31 | \r | |
32 | // Inherit inline styles from another element.\r | |
33 | function inheritInlineStyles( parent, el ) {\r | |
34 | var style = parent.getAttribute( 'style' );\r | |
35 | \r | |
36 | // Put parent styles before child styles.\r | |
37 | style && el.setAttribute( 'style', style.replace( /([^;])$/, '$1;' ) + ( el.getAttribute( 'style' ) || '' ) );\r | |
38 | }\r | |
39 | \r | |
40 | CKEDITOR.plugins.list = {\r | |
41 | /**\r | |
42 | * Convert a DOM list tree into a data structure that is easier to\r | |
43 | * manipulate. This operation should be non-intrusive in the sense that it\r | |
44 | * does not change the DOM tree, with the exception that it may add some\r | |
45 | * markers to the list item nodes when database is specified.\r | |
46 | *\r | |
47 | * @member CKEDITOR.plugins.list\r | |
48 | * @todo params\r | |
49 | */\r | |
50 | listToArray: function( listNode, database, baseArray, baseIndentLevel, grandparentNode ) {\r | |
51 | if ( !listNodeNames[ listNode.getName() ] )\r | |
52 | return [];\r | |
53 | \r | |
54 | if ( !baseIndentLevel )\r | |
55 | baseIndentLevel = 0;\r | |
56 | if ( !baseArray )\r | |
57 | baseArray = [];\r | |
58 | \r | |
59 | // Iterate over all list items to and look for inner lists.\r | |
60 | for ( var i = 0, count = listNode.getChildCount(); i < count; i++ ) {\r | |
61 | var listItem = listNode.getChild( i );\r | |
62 | \r | |
63 | // Fixing malformed nested lists by moving it into a previous list item. (#6236)\r | |
64 | if ( listItem.type == CKEDITOR.NODE_ELEMENT && listItem.getName() in CKEDITOR.dtd.$list )\r | |
65 | CKEDITOR.plugins.list.listToArray( listItem, database, baseArray, baseIndentLevel + 1 );\r | |
66 | \r | |
67 | // It may be a text node or some funny stuff.\r | |
68 | if ( listItem.$.nodeName.toLowerCase() != 'li' )\r | |
69 | continue;\r | |
70 | \r | |
71 | var itemObj = { 'parent': listNode, indent: baseIndentLevel, element: listItem, contents: [] };\r | |
72 | if ( !grandparentNode ) {\r | |
73 | itemObj.grandparent = listNode.getParent();\r | |
74 | if ( itemObj.grandparent && itemObj.grandparent.$.nodeName.toLowerCase() == 'li' )\r | |
75 | itemObj.grandparent = itemObj.grandparent.getParent();\r | |
76 | } else {\r | |
77 | itemObj.grandparent = grandparentNode;\r | |
78 | }\r | |
79 | \r | |
80 | if ( database )\r | |
81 | CKEDITOR.dom.element.setMarker( database, listItem, 'listarray_index', baseArray.length );\r | |
82 | baseArray.push( itemObj );\r | |
83 | \r | |
84 | for ( var j = 0, itemChildCount = listItem.getChildCount(), child; j < itemChildCount; j++ ) {\r | |
85 | child = listItem.getChild( j );\r | |
86 | if ( child.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ child.getName() ] )\r | |
87 | // Note the recursion here, it pushes inner list items with\r | |
88 | // +1 indentation in the correct order.\r | |
89 | CKEDITOR.plugins.list.listToArray( child, database, baseArray, baseIndentLevel + 1, itemObj.grandparent );\r | |
90 | else\r | |
91 | itemObj.contents.push( child );\r | |
92 | }\r | |
93 | }\r | |
94 | return baseArray;\r | |
95 | },\r | |
96 | \r | |
97 | /**\r | |
98 | * Convert our internal representation of a list back to a DOM forest.\r | |
99 | *\r | |
100 | * @member CKEDITOR.plugins.list\r | |
101 | * @todo params\r | |
102 | */\r | |
103 | arrayToList: function( listArray, database, baseIndex, paragraphMode, dir ) {\r | |
104 | if ( !baseIndex )\r | |
105 | baseIndex = 0;\r | |
106 | if ( !listArray || listArray.length < baseIndex + 1 )\r | |
107 | return null;\r | |
108 | \r | |
109 | var i,\r | |
110 | doc = listArray[ baseIndex ].parent.getDocument(),\r | |
111 | retval = new CKEDITOR.dom.documentFragment( doc ),\r | |
112 | rootNode = null,\r | |
113 | currentIndex = baseIndex,\r | |
114 | indentLevel = Math.max( listArray[ baseIndex ].indent, 0 ),\r | |
115 | currentListItem = null,\r | |
116 | orgDir, block,\r | |
117 | paragraphName = ( paragraphMode == CKEDITOR.ENTER_P ? 'p' : 'div' );\r | |
118 | \r | |
119 | while ( 1 ) {\r | |
120 | var item = listArray[ currentIndex ],\r | |
121 | itemGrandParent = item.grandparent;\r | |
122 | \r | |
123 | orgDir = item.element.getDirection( 1 );\r | |
124 | \r | |
125 | if ( item.indent == indentLevel ) {\r | |
126 | if ( !rootNode || listArray[ currentIndex ].parent.getName() != rootNode.getName() ) {\r | |
127 | rootNode = listArray[ currentIndex ].parent.clone( false, 1 );\r | |
128 | dir && rootNode.setAttribute( 'dir', dir );\r | |
129 | retval.append( rootNode );\r | |
130 | }\r | |
131 | currentListItem = rootNode.append( item.element.clone( 0, 1 ) );\r | |
132 | \r | |
133 | if ( orgDir != rootNode.getDirection( 1 ) )\r | |
134 | currentListItem.setAttribute( 'dir', orgDir );\r | |
135 | \r | |
136 | for ( i = 0; i < item.contents.length; i++ )\r | |
137 | currentListItem.append( item.contents[ i ].clone( 1, 1 ) );\r | |
138 | currentIndex++;\r | |
139 | } else if ( item.indent == Math.max( indentLevel, 0 ) + 1 ) {\r | |
140 | // Maintain original direction (#6861).\r | |
141 | var currDir = listArray[ currentIndex - 1 ].element.getDirection( 1 ),\r | |
142 | listData = CKEDITOR.plugins.list.arrayToList( listArray, null, currentIndex, paragraphMode, currDir != orgDir ? orgDir : null );\r | |
143 | \r | |
144 | // If the next block is an <li> with another list tree as the first\r | |
145 | // child, we'll need to append a filler (<br>/NBSP) or the list item\r | |
146 | // wouldn't be editable. (#6724)\r | |
147 | if ( !currentListItem.getChildCount() && CKEDITOR.env.needsNbspFiller && doc.$.documentMode <= 7 )\r | |
148 | currentListItem.append( doc.createText( '\xa0' ) );\r | |
149 | currentListItem.append( listData.listNode );\r | |
150 | currentIndex = listData.nextIndex;\r | |
151 | } else if ( item.indent == -1 && !baseIndex && itemGrandParent ) {\r | |
152 | if ( listNodeNames[ itemGrandParent.getName() ] ) {\r | |
153 | currentListItem = item.element.clone( false, true );\r | |
154 | if ( orgDir != itemGrandParent.getDirection( 1 ) )\r | |
155 | currentListItem.setAttribute( 'dir', orgDir );\r | |
156 | } else {\r | |
157 | currentListItem = new CKEDITOR.dom.documentFragment( doc );\r | |
158 | }\r | |
159 | \r | |
160 | // Migrate all children to the new container,\r | |
161 | // apply the proper text direction.\r | |
162 | var dirLoose = itemGrandParent.getDirection( 1 ) != orgDir,\r | |
163 | li = item.element,\r | |
164 | className = li.getAttribute( 'class' ),\r | |
165 | style = li.getAttribute( 'style' );\r | |
166 | \r | |
167 | var needsBlock = currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && ( paragraphMode != CKEDITOR.ENTER_BR || dirLoose || style || className );\r | |
168 | \r | |
169 | var child,\r | |
170 | count = item.contents.length,\r | |
171 | cachedBookmark;\r | |
172 | \r | |
173 | for ( i = 0; i < count; i++ ) {\r | |
174 | child = item.contents[ i ];\r | |
175 | \r | |
176 | // Append bookmark if we can, or cache it and append it when we'll know\r | |
177 | // what to do with it. Generally - we want to keep it next to its original neighbour.\r | |
178 | // Exception: if bookmark is the only child it hasn't got any neighbour, so handle it normally\r | |
179 | // (wrap with block if needed).\r | |
180 | if ( bookmarks( child ) && count > 1 ) {\r | |
181 | // If we don't need block, it's simple - append bookmark directly to the current list item.\r | |
182 | if ( !needsBlock )\r | |
183 | currentListItem.append( child.clone( 1, 1 ) );\r | |
184 | else\r | |
185 | cachedBookmark = child.clone( 1, 1 );\r | |
186 | }\r | |
187 | // Block content goes directly to the current list item, without wrapping.\r | |
188 | else if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() ) {\r | |
189 | // Apply direction on content blocks.\r | |
190 | if ( dirLoose && !child.getDirection() )\r | |
191 | child.setAttribute( 'dir', orgDir );\r | |
192 | \r | |
193 | inheritInlineStyles( li, child );\r | |
194 | \r | |
195 | className && child.addClass( className );\r | |
196 | \r | |
197 | // Close the block which we started for inline content.\r | |
198 | block = null;\r | |
199 | // Append bookmark directly before current child.\r | |
200 | if ( cachedBookmark ) {\r | |
201 | currentListItem.append( cachedBookmark );\r | |
202 | cachedBookmark = null;\r | |
203 | }\r | |
204 | // Append this block element to the list item.\r | |
205 | currentListItem.append( child.clone( 1, 1 ) );\r | |
206 | }\r | |
207 | // Some inline content was found - wrap it with block and append that\r | |
208 | // block to the current list item or append it to the block previously created.\r | |
209 | else if ( needsBlock ) {\r | |
210 | // Establish new block to hold text direction and styles.\r | |
211 | if ( !block ) {\r | |
212 | block = doc.createElement( paragraphName );\r | |
213 | currentListItem.append( block );\r | |
214 | dirLoose && block.setAttribute( 'dir', orgDir );\r | |
215 | }\r | |
216 | \r | |
217 | // Copy over styles to new block;\r | |
218 | style && block.setAttribute( 'style', style );\r | |
219 | className && block.setAttribute( 'class', className );\r | |
220 | \r | |
221 | // Append bookmark directly before current child.\r | |
222 | if ( cachedBookmark ) {\r | |
223 | block.append( cachedBookmark );\r | |
224 | cachedBookmark = null;\r | |
225 | }\r | |
226 | block.append( child.clone( 1, 1 ) );\r | |
227 | }\r | |
228 | // E.g. BR mode - inline content appended directly to the list item.\r | |
229 | else {\r | |
230 | currentListItem.append( child.clone( 1, 1 ) );\r | |
231 | }\r | |
232 | }\r | |
233 | \r | |
234 | // No content after bookmark - append it to the block if we had one\r | |
235 | // or directly to the current list item if we finished directly in the current list item.\r | |
236 | if ( cachedBookmark ) {\r | |
237 | ( block || currentListItem ).append( cachedBookmark );\r | |
238 | cachedBookmark = null;\r | |
239 | }\r | |
240 | \r | |
241 | if ( currentListItem.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT && currentIndex != listArray.length - 1 ) {\r | |
242 | var last;\r | |
243 | \r | |
244 | // Remove bogus <br> if this browser uses them.\r | |
245 | if ( CKEDITOR.env.needsBrFiller ) {\r | |
246 | last = currentListItem.getLast();\r | |
247 | if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )\r | |
248 | last.remove();\r | |
249 | }\r | |
250 | \r | |
251 | // If the last element is not a block, append <br> to separate merged list items.\r | |
252 | last = currentListItem.getLast( nonEmpty );\r | |
253 | if ( !( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( CKEDITOR.dtd.$block ) ) )\r | |
254 | currentListItem.append( doc.createElement( 'br' ) );\r | |
255 | }\r | |
256 | \r | |
257 | var currentListItemName = currentListItem.$.nodeName.toLowerCase();\r | |
258 | if ( currentListItemName == 'div' || currentListItemName == 'p' ) {\r | |
259 | currentListItem.appendBogus();\r | |
260 | }\r | |
261 | retval.append( currentListItem );\r | |
262 | rootNode = null;\r | |
263 | currentIndex++;\r | |
264 | } else {\r | |
265 | return null;\r | |
266 | }\r | |
267 | \r | |
268 | block = null;\r | |
269 | \r | |
270 | if ( listArray.length <= currentIndex || Math.max( listArray[ currentIndex ].indent, 0 ) < indentLevel )\r | |
271 | break;\r | |
272 | }\r | |
273 | \r | |
274 | if ( database ) {\r | |
275 | var currentNode = retval.getFirst();\r | |
276 | \r | |
277 | while ( currentNode ) {\r | |
278 | if ( currentNode.type == CKEDITOR.NODE_ELEMENT ) {\r | |
279 | // Clear marker attributes for the new list tree made of cloned nodes, if any.\r | |
280 | CKEDITOR.dom.element.clearMarkers( database, currentNode );\r | |
281 | \r | |
282 | // Clear redundant direction attribute specified on list items.\r | |
283 | if ( currentNode.getName() in CKEDITOR.dtd.$listItem )\r | |
284 | cleanUpDirection( currentNode );\r | |
285 | }\r | |
286 | \r | |
287 | currentNode = currentNode.getNextSourceNode();\r | |
288 | }\r | |
289 | }\r | |
290 | \r | |
291 | return { listNode: retval, nextIndex: currentIndex };\r | |
292 | }\r | |
293 | };\r | |
294 | \r | |
295 | function changeListType( editor, groupObj, database, listsCreated ) {\r | |
296 | // This case is easy...\r | |
297 | // 1. Convert the whole list into a one-dimensional array.\r | |
298 | // 2. Change the list type by modifying the array.\r | |
299 | // 3. Recreate the whole list by converting the array to a list.\r | |
300 | // 4. Replace the original list with the recreated list.\r | |
301 | var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),\r | |
302 | selectedListItems = [];\r | |
303 | \r | |
304 | for ( var i = 0; i < groupObj.contents.length; i++ ) {\r | |
305 | var itemNode = groupObj.contents[ i ];\r | |
306 | itemNode = itemNode.getAscendant( 'li', true );\r | |
307 | if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )\r | |
308 | continue;\r | |
309 | selectedListItems.push( itemNode );\r | |
310 | CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );\r | |
311 | }\r | |
312 | \r | |
313 | var root = groupObj.root,\r | |
314 | doc = root.getDocument(),\r | |
315 | listNode, newListNode;\r | |
316 | \r | |
317 | for ( i = 0; i < selectedListItems.length; i++ ) {\r | |
318 | var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' );\r | |
319 | listNode = listArray[ listIndex ].parent;\r | |
320 | \r | |
321 | // Switch to new list node for this particular item.\r | |
322 | if ( !listNode.is( this.type ) ) {\r | |
323 | newListNode = doc.createElement( this.type );\r | |
324 | // Copy all attributes, except from 'start' and 'type'.\r | |
325 | listNode.copyAttributes( newListNode, { start: 1, type: 1 } );\r | |
326 | // The list-style-type property should be ignored.\r | |
327 | newListNode.removeStyle( 'list-style-type' );\r | |
328 | listArray[ listIndex ].parent = newListNode;\r | |
329 | }\r | |
330 | }\r | |
331 | \r | |
332 | var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode );\r | |
333 | var child,\r | |
334 | length = newList.listNode.getChildCount();\r | |
335 | for ( i = 0; i < length && ( child = newList.listNode.getChild( i ) ); i++ ) {\r | |
336 | if ( child.getName() == this.type )\r | |
337 | listsCreated.push( child );\r | |
338 | }\r | |
339 | newList.listNode.replace( groupObj.root );\r | |
340 | \r | |
341 | editor.fire( 'contentDomInvalidated' );\r | |
342 | }\r | |
343 | \r | |
344 | function createList( editor, groupObj, listsCreated ) {\r | |
345 | var contents = groupObj.contents,\r | |
346 | doc = groupObj.root.getDocument(),\r | |
347 | listContents = [];\r | |
348 | \r | |
349 | // It is possible to have the contents returned by DomRangeIterator to be the same as the root.\r | |
350 | // e.g. when we're running into table cells.\r | |
351 | // In such a case, enclose the childNodes of contents[0] into a <div>.\r | |
352 | if ( contents.length == 1 && contents[ 0 ].equals( groupObj.root ) ) {\r | |
353 | var divBlock = doc.createElement( 'div' );\r | |
354 | contents[ 0 ].moveChildren && contents[ 0 ].moveChildren( divBlock );\r | |
355 | contents[ 0 ].append( divBlock );\r | |
356 | contents[ 0 ] = divBlock;\r | |
357 | }\r | |
358 | \r | |
359 | // Calculate the common parent node of all content blocks.\r | |
360 | var commonParent = groupObj.contents[ 0 ].getParent();\r | |
361 | for ( var i = 0; i < contents.length; i++ )\r | |
362 | commonParent = commonParent.getCommonAncestor( contents[ i ].getParent() );\r | |
363 | \r | |
364 | var useComputedState = editor.config.useComputedState,\r | |
365 | listDir, explicitDirection;\r | |
366 | \r | |
367 | useComputedState = useComputedState === undefined || useComputedState;\r | |
368 | \r | |
369 | // We want to insert things that are in the same tree level only, so calculate the contents again\r | |
370 | // by expanding the selected blocks to the same tree level.\r | |
371 | for ( i = 0; i < contents.length; i++ ) {\r | |
372 | var contentNode = contents[ i ],\r | |
373 | parentNode;\r | |
374 | while ( ( parentNode = contentNode.getParent() ) ) {\r | |
375 | if ( parentNode.equals( commonParent ) ) {\r | |
376 | listContents.push( contentNode );\r | |
377 | \r | |
378 | // Determine the lists's direction.\r | |
379 | if ( !explicitDirection && contentNode.getDirection() )\r | |
380 | explicitDirection = 1;\r | |
381 | \r | |
382 | var itemDir = contentNode.getDirection( useComputedState );\r | |
383 | \r | |
384 | if ( listDir !== null ) {\r | |
385 | // If at least one LI have a different direction than current listDir, we can't have listDir.\r | |
386 | if ( listDir && listDir != itemDir )\r | |
387 | listDir = null;\r | |
388 | else\r | |
389 | listDir = itemDir;\r | |
390 | }\r | |
391 | \r | |
392 | break;\r | |
393 | }\r | |
394 | contentNode = parentNode;\r | |
395 | }\r | |
396 | }\r | |
397 | \r | |
398 | if ( listContents.length < 1 )\r | |
399 | return;\r | |
400 | \r | |
401 | // Insert the list to the DOM tree.\r | |
402 | var insertAnchor = listContents[ listContents.length - 1 ].getNext(),\r | |
403 | listNode = doc.createElement( this.type );\r | |
404 | \r | |
405 | listsCreated.push( listNode );\r | |
406 | \r | |
407 | var contentBlock, listItem;\r | |
408 | \r | |
409 | while ( listContents.length ) {\r | |
410 | contentBlock = listContents.shift();\r | |
411 | listItem = doc.createElement( 'li' );\r | |
412 | \r | |
413 | // If current block should be preserved, append it to list item instead of\r | |
414 | // transforming it to <li> element.\r | |
415 | if ( shouldPreserveBlock( contentBlock ) )\r | |
416 | contentBlock.appendTo( listItem );\r | |
417 | else {\r | |
418 | contentBlock.copyAttributes( listItem );\r | |
419 | // Remove direction attribute after it was merged into list root. (#7657)\r | |
420 | if ( listDir && contentBlock.getDirection() ) {\r | |
421 | listItem.removeStyle( 'direction' );\r | |
422 | listItem.removeAttribute( 'dir' );\r | |
423 | }\r | |
424 | contentBlock.moveChildren( listItem );\r | |
425 | contentBlock.remove();\r | |
426 | }\r | |
427 | \r | |
428 | listItem.appendTo( listNode );\r | |
429 | }\r | |
430 | \r | |
431 | // Apply list root dir only if it has been explicitly declared.\r | |
432 | if ( listDir && explicitDirection )\r | |
433 | listNode.setAttribute( 'dir', listDir );\r | |
434 | \r | |
435 | if ( insertAnchor )\r | |
436 | listNode.insertBefore( insertAnchor );\r | |
437 | else\r | |
438 | listNode.appendTo( commonParent );\r | |
439 | }\r | |
440 | \r | |
441 | function removeList( editor, groupObj, database ) {\r | |
442 | // This is very much like the change list type operation.\r | |
443 | // Except that we're changing the selected items' indent to -1 in the list array.\r | |
444 | var listArray = CKEDITOR.plugins.list.listToArray( groupObj.root, database ),\r | |
445 | selectedListItems = [];\r | |
446 | \r | |
447 | for ( var i = 0; i < groupObj.contents.length; i++ ) {\r | |
448 | var itemNode = groupObj.contents[ i ];\r | |
449 | itemNode = itemNode.getAscendant( 'li', true );\r | |
450 | if ( !itemNode || itemNode.getCustomData( 'list_item_processed' ) )\r | |
451 | continue;\r | |
452 | selectedListItems.push( itemNode );\r | |
453 | CKEDITOR.dom.element.setMarker( database, itemNode, 'list_item_processed', true );\r | |
454 | }\r | |
455 | \r | |
456 | var lastListIndex = null;\r | |
457 | for ( i = 0; i < selectedListItems.length; i++ ) {\r | |
458 | var listIndex = selectedListItems[ i ].getCustomData( 'listarray_index' );\r | |
459 | listArray[ listIndex ].indent = -1;\r | |
460 | lastListIndex = listIndex;\r | |
461 | }\r | |
462 | \r | |
463 | // After cutting parts of the list out with indent=-1, we still have to maintain the array list\r | |
464 | // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the\r | |
465 | // list cannot be converted back to a real DOM list.\r | |
466 | for ( i = lastListIndex + 1; i < listArray.length; i++ ) {\r | |
467 | if ( listArray[ i ].indent > listArray[ i - 1 ].indent + 1 ) {\r | |
468 | var indentOffset = listArray[ i - 1 ].indent + 1 - listArray[ i ].indent;\r | |
469 | var oldIndent = listArray[ i ].indent;\r | |
470 | while ( listArray[ i ] && listArray[ i ].indent >= oldIndent ) {\r | |
471 | listArray[ i ].indent += indentOffset;\r | |
472 | i++;\r | |
473 | }\r | |
474 | i--;\r | |
475 | }\r | |
476 | }\r | |
477 | \r | |
478 | var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, groupObj.root.getAttribute( 'dir' ) );\r | |
479 | \r | |
480 | // Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)\r | |
481 | var docFragment = newList.listNode,\r | |
482 | boundaryNode, siblingNode;\r | |
483 | \r | |
484 | function compensateBrs( isStart ) {\r | |
485 | if (\r | |
486 | ( boundaryNode = docFragment[ isStart ? 'getFirst' : 'getLast' ]() ) &&\r | |
487 | !( boundaryNode.is && boundaryNode.isBlockBoundary() ) &&\r | |
488 | ( siblingNode = groupObj.root[ isStart ? 'getPrevious' : 'getNext' ]( CKEDITOR.dom.walker.invisible( true ) ) ) &&\r | |
489 | !( siblingNode.is && siblingNode.isBlockBoundary( { br: 1 } ) )\r | |
490 | ) {\r | |
491 | editor.document.createElement( 'br' )[ isStart ? 'insertBefore' : 'insertAfter' ]( boundaryNode );\r | |
492 | }\r | |
493 | }\r | |
494 | compensateBrs( true );\r | |
495 | compensateBrs();\r | |
496 | \r | |
497 | docFragment.replace( groupObj.root );\r | |
498 | \r | |
499 | editor.fire( 'contentDomInvalidated' );\r | |
500 | }\r | |
501 | \r | |
502 | var headerTagRegex = /^h[1-6]$/;\r | |
503 | \r | |
504 | // Checks wheather this block should be element preserved (not transformed to <li>) when creating list.\r | |
505 | function shouldPreserveBlock( block ) {\r | |
506 | return (\r | |
507 | // #5335\r | |
508 | block.is( 'pre' ) ||\r | |
509 | // #5271 - this is a header.\r | |
510 | headerTagRegex.test( block.getName() ) ||\r | |
511 | // 11083 - this is a non-editable element.\r | |
512 | block.getAttribute( 'contenteditable' ) == 'false'\r | |
513 | );\r | |
514 | }\r | |
515 | \r | |
516 | function listCommand( name, type ) {\r | |
517 | this.name = name;\r | |
518 | this.type = type;\r | |
519 | this.context = type;\r | |
520 | this.allowedContent = type + ' li';\r | |
521 | this.requiredContent = type;\r | |
522 | }\r | |
523 | \r | |
524 | var elementType = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT );\r | |
525 | \r | |
526 | // Merge child nodes with direction preserved. (#7448)\r | |
527 | function mergeChildren( from, into, refNode, forward ) {\r | |
528 | var child, itemDir;\r | |
529 | while ( ( child = from[ forward ? 'getLast' : 'getFirst' ]( elementType ) ) ) {\r | |
530 | if ( ( itemDir = child.getDirection( 1 ) ) !== into.getDirection( 1 ) )\r | |
531 | child.setAttribute( 'dir', itemDir );\r | |
532 | \r | |
533 | child.remove();\r | |
534 | \r | |
535 | refNode ? child[ forward ? 'insertBefore' : 'insertAfter' ]( refNode ) : into.append( child, forward );\r | |
536 | }\r | |
537 | }\r | |
538 | \r | |
539 | listCommand.prototype = {\r | |
540 | exec: function( editor ) {\r | |
541 | // Run state check first of all.\r | |
542 | this.refresh( editor, editor.elementPath() );\r | |
543 | \r | |
544 | var config = editor.config,\r | |
545 | selection = editor.getSelection(),\r | |
546 | ranges = selection && selection.getRanges();\r | |
547 | \r | |
548 | // Midas lists rule #1 says we can create a list even in an empty document.\r | |
549 | // But DOM iterator wouldn't run if the document is really empty.\r | |
550 | // So create a paragraph if the document is empty and we're going to create a list.\r | |
551 | if ( this.state == CKEDITOR.TRISTATE_OFF ) {\r | |
552 | var editable = editor.editable();\r | |
553 | if ( !editable.getFirst( nonEmpty ) ) {\r | |
554 | config.enterMode == CKEDITOR.ENTER_BR ? editable.appendBogus() : ranges[ 0 ].fixBlock( 1, config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' );\r | |
555 | \r | |
556 | selection.selectRanges( ranges );\r | |
557 | }\r | |
558 | // Maybe a single range there enclosing the whole list,\r | |
559 | // turn on the list state manually(#4129).\r | |
560 | else {\r | |
561 | var range = ranges.length == 1 && ranges[ 0 ],\r | |
562 | enclosedNode = range && range.getEnclosedNode();\r | |
563 | if ( enclosedNode && enclosedNode.is && this.type == enclosedNode.getName() )\r | |
564 | this.setState( CKEDITOR.TRISTATE_ON );\r | |
565 | }\r | |
566 | }\r | |
567 | \r | |
568 | var bookmarks = selection.createBookmarks( true );\r | |
569 | \r | |
570 | // Group the blocks up because there are many cases where multiple lists have to be created,\r | |
571 | // or multiple lists have to be cancelled.\r | |
572 | var listGroups = [],\r | |
573 | database = {},\r | |
574 | rangeIterator = ranges.createIterator(),\r | |
575 | index = 0;\r | |
576 | \r | |
577 | while ( ( range = rangeIterator.getNextRange() ) && ++index ) {\r | |
578 | var boundaryNodes = range.getBoundaryNodes(),\r | |
579 | startNode = boundaryNodes.startNode,\r | |
580 | endNode = boundaryNodes.endNode;\r | |
581 | \r | |
582 | if ( startNode.type == CKEDITOR.NODE_ELEMENT && startNode.getName() == 'td' )\r | |
583 | range.setStartAt( boundaryNodes.startNode, CKEDITOR.POSITION_AFTER_START );\r | |
584 | \r | |
585 | if ( endNode.type == CKEDITOR.NODE_ELEMENT && endNode.getName() == 'td' )\r | |
586 | range.setEndAt( boundaryNodes.endNode, CKEDITOR.POSITION_BEFORE_END );\r | |
587 | \r | |
588 | var iterator = range.createIterator(),\r | |
589 | block;\r | |
590 | \r | |
591 | iterator.forceBrBreak = ( this.state == CKEDITOR.TRISTATE_OFF );\r | |
592 | \r | |
593 | while ( ( block = iterator.getNextParagraph() ) ) {\r | |
594 | // Avoid duplicate blocks get processed across ranges.\r | |
595 | if ( block.getCustomData( 'list_block' ) )\r | |
596 | continue;\r | |
597 | else\r | |
598 | CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );\r | |
599 | \r | |
600 | var path = editor.elementPath( block ),\r | |
601 | pathElements = path.elements,\r | |
602 | pathElementsCount = pathElements.length,\r | |
603 | processedFlag = 0,\r | |
604 | blockLimit = path.blockLimit,\r | |
605 | element;\r | |
606 | \r | |
607 | // First, try to group by a list ancestor.\r | |
608 | for ( var i = pathElementsCount - 1; i >= 0 && ( element = pathElements[ i ] ); i-- ) {\r | |
609 | // Don't leak outside block limit (#3940).\r | |
610 | if ( listNodeNames[ element.getName() ] && blockLimit.contains( element ) ) {\r | |
611 | // If we've encountered a list inside a block limit\r | |
612 | // The last group object of the block limit element should\r | |
613 | // no longer be valid. Since paragraphs after the list\r | |
614 | // should belong to a different group of paragraphs before\r | |
615 | // the list. (Bug #1309)\r | |
616 | blockLimit.removeCustomData( 'list_group_object_' + index );\r | |
617 | \r | |
618 | var groupObj = element.getCustomData( 'list_group_object' );\r | |
619 | if ( groupObj )\r | |
620 | groupObj.contents.push( block );\r | |
621 | else {\r | |
622 | groupObj = { root: element, contents: [ block ] };\r | |
623 | listGroups.push( groupObj );\r | |
624 | CKEDITOR.dom.element.setMarker( database, element, 'list_group_object', groupObj );\r | |
625 | }\r | |
626 | processedFlag = 1;\r | |
627 | break;\r | |
628 | }\r | |
629 | }\r | |
630 | \r | |
631 | if ( processedFlag )\r | |
632 | continue;\r | |
633 | \r | |
634 | // No list ancestor? Group by block limit, but don't mix contents from different ranges.\r | |
635 | var root = blockLimit;\r | |
636 | if ( root.getCustomData( 'list_group_object_' + index ) )\r | |
637 | root.getCustomData( 'list_group_object_' + index ).contents.push( block );\r | |
638 | else {\r | |
639 | groupObj = { root: root, contents: [ block ] };\r | |
640 | CKEDITOR.dom.element.setMarker( database, root, 'list_group_object_' + index, groupObj );\r | |
641 | listGroups.push( groupObj );\r | |
642 | }\r | |
643 | }\r | |
644 | }\r | |
645 | \r | |
646 | // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.\r | |
647 | // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking\r | |
648 | // at the group that's not rooted at lists. So we have three cases to handle.\r | |
649 | var listsCreated = [];\r | |
650 | while ( listGroups.length > 0 ) {\r | |
651 | groupObj = listGroups.shift();\r | |
652 | if ( this.state == CKEDITOR.TRISTATE_OFF ) {\r | |
653 | if ( listNodeNames[ groupObj.root.getName() ] )\r | |
654 | changeListType.call( this, editor, groupObj, database, listsCreated );\r | |
655 | else\r | |
656 | createList.call( this, editor, groupObj, listsCreated );\r | |
657 | } else if ( this.state == CKEDITOR.TRISTATE_ON && listNodeNames[ groupObj.root.getName() ] ) {\r | |
658 | removeList.call( this, editor, groupObj, database );\r | |
659 | }\r | |
660 | }\r | |
661 | \r | |
662 | // For all new lists created, merge into adjacent, same type lists.\r | |
663 | for ( i = 0; i < listsCreated.length; i++ )\r | |
664 | mergeListSiblings( listsCreated[ i ] );\r | |
665 | \r | |
666 | // Clean up, restore selection and update toolbar button states.\r | |
667 | CKEDITOR.dom.element.clearAllMarkers( database );\r | |
668 | selection.selectBookmarks( bookmarks );\r | |
669 | editor.focus();\r | |
670 | },\r | |
671 | \r | |
672 | refresh: function( editor, path ) {\r | |
673 | var list = path.contains( listNodeNames, 1 ),\r | |
674 | limit = path.blockLimit || path.root;\r | |
675 | \r | |
676 | // 1. Only a single type of list activate.\r | |
677 | // 2. Do not show list outside of block limit.\r | |
678 | if ( list && limit.contains( list ) )\r | |
679 | this.setState( list.is( this.type ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );\r | |
680 | else\r | |
681 | this.setState( CKEDITOR.TRISTATE_OFF );\r | |
682 | }\r | |
683 | };\r | |
684 | \r | |
685 | // Merge list adjacent, of same type lists.\r | |
686 | function mergeListSiblings( listNode ) {\r | |
687 | \r | |
688 | function mergeSibling( rtl ) {\r | |
689 | var sibling = listNode[ rtl ? 'getPrevious' : 'getNext' ]( nonEmpty );\r | |
690 | if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && sibling.is( listNode.getName() ) ) {\r | |
691 | // Move children order by merge direction.(#3820)\r | |
692 | mergeChildren( listNode, sibling, null, !rtl );\r | |
693 | \r | |
694 | listNode.remove();\r | |
695 | listNode = sibling;\r | |
696 | }\r | |
697 | }\r | |
698 | \r | |
699 | mergeSibling();\r | |
700 | mergeSibling( 1 );\r | |
701 | }\r | |
702 | \r | |
703 | // Check if node is block element that recieves text.\r | |
704 | function isTextBlock( node ) {\r | |
705 | return node.type == CKEDITOR.NODE_ELEMENT && ( node.getName() in CKEDITOR.dtd.$block || node.getName() in CKEDITOR.dtd.$listItem ) && CKEDITOR.dtd[ node.getName() ][ '#' ];\r | |
706 | }\r | |
707 | \r | |
708 | // Join visually two block lines.\r | |
709 | function joinNextLineToCursor( editor, cursor, nextCursor ) {\r | |
710 | editor.fire( 'saveSnapshot' );\r | |
711 | \r | |
712 | // Merge with previous block's content.\r | |
713 | nextCursor.enlarge( CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS );\r | |
714 | var frag = nextCursor.extractContents();\r | |
715 | \r | |
716 | cursor.trim( false, true );\r | |
717 | var bm = cursor.createBookmark();\r | |
718 | \r | |
719 | // Kill original bogus;\r | |
720 | var currentPath = new CKEDITOR.dom.elementPath( cursor.startContainer ),\r | |
721 | pathBlock = currentPath.block,\r | |
722 | currentBlock = currentPath.lastElement.getAscendant( 'li', 1 ) || pathBlock,\r | |
723 | nextPath = new CKEDITOR.dom.elementPath( nextCursor.startContainer ),\r | |
724 | nextLi = nextPath.contains( CKEDITOR.dtd.$listItem ),\r | |
725 | nextList = nextPath.contains( CKEDITOR.dtd.$list ),\r | |
726 | last;\r | |
727 | \r | |
728 | // Remove bogus node the current block/pseudo block.\r | |
729 | if ( pathBlock ) {\r | |
730 | var bogus = pathBlock.getBogus();\r | |
731 | bogus && bogus.remove();\r | |
732 | }\r | |
733 | else if ( nextList ) {\r | |
734 | last = nextList.getPrevious( nonEmpty );\r | |
735 | if ( last && blockBogus( last ) )\r | |
736 | last.remove();\r | |
737 | }\r | |
738 | \r | |
739 | // Kill the tail br in extracted.\r | |
740 | last = frag.getLast();\r | |
741 | if ( last && last.type == CKEDITOR.NODE_ELEMENT && last.is( 'br' ) )\r | |
742 | last.remove();\r | |
743 | \r | |
744 | // Insert fragment at the range position.\r | |
745 | var nextNode = cursor.startContainer.getChild( cursor.startOffset );\r | |
746 | if ( nextNode )\r | |
747 | frag.insertBefore( nextNode );\r | |
748 | else\r | |
749 | cursor.startContainer.append( frag );\r | |
750 | \r | |
751 | // Move the sub list nested in the next list item.\r | |
752 | if ( nextLi ) {\r | |
753 | var sublist = getSubList( nextLi );\r | |
754 | if ( sublist ) {\r | |
755 | // If next line is in the sub list of the current list item.\r | |
756 | if ( currentBlock.contains( nextLi ) ) {\r | |
757 | mergeChildren( sublist, nextLi.getParent(), nextLi );\r | |
758 | sublist.remove();\r | |
759 | }\r | |
760 | // Migrate the sub list to current list item.\r | |
761 | else {\r | |
762 | currentBlock.append( sublist );\r | |
763 | }\r | |
764 | }\r | |
765 | }\r | |
766 | \r | |
767 | var nextBlock, parent;\r | |
768 | // Remove any remaining zombies path blocks at the end after line merged.\r | |
769 | while ( nextCursor.checkStartOfBlock() && nextCursor.checkEndOfBlock() ) {\r | |
770 | nextPath = nextCursor.startPath();\r | |
771 | nextBlock = nextPath.block;\r | |
772 | \r | |
773 | // Abort when nothing to be removed (#10890).\r | |
774 | if ( !nextBlock )\r | |
775 | break;\r | |
776 | \r | |
777 | // Check if also to remove empty list.\r | |
778 | if ( nextBlock.is( 'li' ) ) {\r | |
779 | parent = nextBlock.getParent();\r | |
780 | if ( nextBlock.equals( parent.getLast( nonEmpty ) ) && nextBlock.equals( parent.getFirst( nonEmpty ) ) )\r | |
781 | nextBlock = parent;\r | |
782 | }\r | |
783 | \r | |
784 | nextCursor.moveToPosition( nextBlock, CKEDITOR.POSITION_BEFORE_START );\r | |
785 | nextBlock.remove();\r | |
786 | }\r | |
787 | \r | |
788 | // Check if need to further merge with the list resides after the merged block. (#9080)\r | |
789 | var walkerRng = nextCursor.clone(), editable = editor.editable();\r | |
790 | walkerRng.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );\r | |
791 | var walker = new CKEDITOR.dom.walker( walkerRng );\r | |
792 | walker.evaluator = function( node ) {\r | |
793 | return nonEmpty( node ) && !blockBogus( node );\r | |
794 | };\r | |
795 | var next = walker.next();\r | |
796 | if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getName() in CKEDITOR.dtd.$list )\r | |
797 | mergeListSiblings( next );\r | |
798 | \r | |
799 | cursor.moveToBookmark( bm );\r | |
800 | \r | |
801 | // Make fresh selection.\r | |
802 | cursor.select();\r | |
803 | \r | |
804 | editor.fire( 'saveSnapshot' );\r | |
805 | }\r | |
806 | \r | |
807 | function getSubList( li ) {\r | |
808 | var last = li.getLast( nonEmpty );\r | |
809 | return last && last.type == CKEDITOR.NODE_ELEMENT && last.getName() in listNodeNames ? last : null;\r | |
810 | }\r | |
811 | \r | |
812 | CKEDITOR.plugins.add( 'list', {\r | |
813 | // jscs:disable maximumLineLength\r | |
814 | lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,de-ch,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%\r | |
815 | // jscs:enable maximumLineLength\r | |
816 | icons: 'bulletedlist,bulletedlist-rtl,numberedlist,numberedlist-rtl', // %REMOVE_LINE_CORE%\r | |
817 | hidpi: true, // %REMOVE_LINE_CORE%\r | |
818 | requires: 'indentlist',\r | |
819 | init: function( editor ) {\r | |
820 | if ( editor.blockless )\r | |
821 | return;\r | |
822 | \r | |
823 | // Register commands.\r | |
824 | editor.addCommand( 'numberedlist', new listCommand( 'numberedlist', 'ol' ) );\r | |
825 | editor.addCommand( 'bulletedlist', new listCommand( 'bulletedlist', 'ul' ) );\r | |
826 | \r | |
827 | // Register the toolbar button.\r | |
828 | if ( editor.ui.addButton ) {\r | |
829 | editor.ui.addButton( 'NumberedList', {\r | |
830 | label: editor.lang.list.numberedlist,\r | |
831 | command: 'numberedlist',\r | |
832 | directional: true,\r | |
833 | toolbar: 'list,10'\r | |
834 | } );\r | |
835 | editor.ui.addButton( 'BulletedList', {\r | |
836 | label: editor.lang.list.bulletedlist,\r | |
837 | command: 'bulletedlist',\r | |
838 | directional: true,\r | |
839 | toolbar: 'list,20'\r | |
840 | } );\r | |
841 | }\r | |
842 | \r | |
843 | // Handled backspace/del key to join list items. (#8248,#9080)\r | |
844 | editor.on( 'key', function( evt ) {\r | |
845 | // Use getKey directly in order to ignore modifiers.\r | |
846 | // Justification: http://dev.ckeditor.com/ticket/11861#comment:13\r | |
847 | var key = evt.data.domEvent.getKey(), li;\r | |
848 | \r | |
849 | // DEl/BACKSPACE\r | |
850 | if ( editor.mode == 'wysiwyg' && key in { 8: 1, 46: 1 } ) {\r | |
851 | var sel = editor.getSelection(),\r | |
852 | range = sel.getRanges()[ 0 ],\r | |
853 | path = range && range.startPath();\r | |
854 | \r | |
855 | if ( !range || !range.collapsed )\r | |
856 | return;\r | |
857 | \r | |
858 | var isBackspace = key == 8;\r | |
859 | var editable = editor.editable();\r | |
860 | var walker = new CKEDITOR.dom.walker( range.clone() );\r | |
861 | walker.evaluator = function( node ) {\r | |
862 | return nonEmpty( node ) && !blockBogus( node );\r | |
863 | };\r | |
864 | // Backspace/Del behavior at the start/end of table is handled in core.\r | |
865 | walker.guard = function( node, isOut ) {\r | |
866 | return !( isOut && node.type == CKEDITOR.NODE_ELEMENT && node.is( 'table' ) );\r | |
867 | };\r | |
868 | \r | |
869 | var cursor = range.clone();\r | |
870 | \r | |
871 | if ( isBackspace ) {\r | |
872 | var previous, joinWith;\r | |
873 | \r | |
874 | // Join a sub list's first line, with the previous visual line in parent.\r | |
875 | if (\r | |
876 | ( previous = path.contains( listNodeNames ) ) &&\r | |
877 | range.checkBoundaryOfElement( previous, CKEDITOR.START ) &&\r | |
878 | ( previous = previous.getParent() ) && previous.is( 'li' ) &&\r | |
879 | ( previous = getSubList( previous ) )\r | |
880 | ) {\r | |
881 | joinWith = previous;\r | |
882 | previous = previous.getPrevious( nonEmpty );\r | |
883 | // Place cursor before the nested list.\r | |
884 | cursor.moveToPosition(\r | |
885 | previous && blockBogus( previous ) ? previous : joinWith,\r | |
886 | CKEDITOR.POSITION_BEFORE_START );\r | |
887 | }\r | |
888 | // Join any line following a list, with the last visual line of the list.\r | |
889 | else {\r | |
890 | walker.range.setStartAt( editable, CKEDITOR.POSITION_AFTER_START );\r | |
891 | walker.range.setEnd( range.startContainer, range.startOffset );\r | |
892 | \r | |
893 | previous = walker.previous();\r | |
894 | \r | |
895 | if (\r | |
896 | previous && previous.type == CKEDITOR.NODE_ELEMENT &&\r | |
897 | ( previous.getName() in listNodeNames ||\r | |
898 | previous.is( 'li' ) )\r | |
899 | ) {\r | |
900 | if ( !previous.is( 'li' ) ) {\r | |
901 | walker.range.selectNodeContents( previous );\r | |
902 | walker.reset();\r | |
903 | walker.evaluator = isTextBlock;\r | |
904 | previous = walker.previous();\r | |
905 | }\r | |
906 | \r | |
907 | joinWith = previous;\r | |
908 | // Place cursor at the end of previous block.\r | |
909 | cursor.moveToElementEditEnd( joinWith );\r | |
910 | \r | |
911 | // And then just before end of closest block element (#12729).\r | |
912 | cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END );\r | |
913 | }\r | |
914 | }\r | |
915 | \r | |
916 | if ( joinWith ) {\r | |
917 | joinNextLineToCursor( editor, cursor, range );\r | |
918 | evt.cancel();\r | |
919 | }\r | |
920 | else {\r | |
921 | var list = path.contains( listNodeNames );\r | |
922 | // Backspace pressed at the start of list outdents the first list item. (#9129)\r | |
923 | if ( list && range.checkBoundaryOfElement( list, CKEDITOR.START ) ) {\r | |
924 | li = list.getFirst( nonEmpty );\r | |
925 | \r | |
926 | if ( range.checkBoundaryOfElement( li, CKEDITOR.START ) ) {\r | |
927 | previous = list.getPrevious( nonEmpty );\r | |
928 | \r | |
929 | // Only if the list item contains a sub list, do nothing but\r | |
930 | // simply move cursor backward one character.\r | |
931 | if ( getSubList( li ) ) {\r | |
932 | if ( previous ) {\r | |
933 | range.moveToElementEditEnd( previous );\r | |
934 | range.select();\r | |
935 | }\r | |
936 | \r | |
937 | evt.cancel();\r | |
938 | }\r | |
939 | else {\r | |
940 | editor.execCommand( 'outdent' );\r | |
941 | evt.cancel();\r | |
942 | }\r | |
943 | }\r | |
944 | }\r | |
945 | }\r | |
946 | \r | |
947 | } else {\r | |
948 | var next, nextLine;\r | |
949 | \r | |
950 | li = path.contains( 'li' );\r | |
951 | \r | |
952 | if ( li ) {\r | |
953 | walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );\r | |
954 | \r | |
955 | var last = li.getLast( nonEmpty );\r | |
956 | var block = last && isTextBlock( last ) ? last : li;\r | |
957 | \r | |
958 | // Indicate cursor at the visual end of an list item.\r | |
959 | var isAtEnd = 0;\r | |
960 | \r | |
961 | next = walker.next();\r | |
962 | \r | |
963 | // When list item contains a sub list.\r | |
964 | if (\r | |
965 | next && next.type == CKEDITOR.NODE_ELEMENT &&\r | |
966 | next.getName() in listNodeNames &&\r | |
967 | next.equals( last )\r | |
968 | ) {\r | |
969 | isAtEnd = 1;\r | |
970 | \r | |
971 | // Move to the first item in sub list.\r | |
972 | next = walker.next();\r | |
973 | }\r | |
974 | // Right at the end of list item.\r | |
975 | else if ( range.checkBoundaryOfElement( block, CKEDITOR.END ) ) {\r | |
976 | isAtEnd = 2;\r | |
977 | }\r | |
978 | \r | |
979 | if ( isAtEnd && next ) {\r | |
980 | // Put cursor range there.\r | |
981 | nextLine = range.clone();\r | |
982 | nextLine.moveToElementEditStart( next );\r | |
983 | \r | |
984 | // #13409\r | |
985 | // For the following case and similar\r | |
986 | //\r | |
987 | // <ul>\r | |
988 | // <li>\r | |
989 | // <p><a href="#one"><em>x^</em></a></p>\r | |
990 | // <ul>\r | |
991 | // <li><span>y</span></li>\r | |
992 | // </ul>\r | |
993 | // </li>\r | |
994 | // </ul>\r | |
995 | if ( isAtEnd == 1 ) {\r | |
996 | // Move the cursor to <em> if attached to "x" text node.\r | |
997 | cursor.optimize();\r | |
998 | \r | |
999 | // Abort if the range is attached directly in <li>, like\r | |
1000 | //\r | |
1001 | // <ul>\r | |
1002 | // <li>\r | |
1003 | // x^\r | |
1004 | // <ul>\r | |
1005 | // <li><span>y</span></li>\r | |
1006 | // </ul>\r | |
1007 | // </li>\r | |
1008 | // </ul>\r | |
1009 | if ( !cursor.startContainer.equals( li ) ) {\r | |
1010 | var node = cursor.startContainer,\r | |
1011 | farthestInlineAscendant;\r | |
1012 | \r | |
1013 | // Find <a>, which is farthest from <em> but still inline element.\r | |
1014 | while ( node.is( CKEDITOR.dtd.$inline ) ) {\r | |
1015 | farthestInlineAscendant = node;\r | |
1016 | node = node.getParent();\r | |
1017 | }\r | |
1018 | \r | |
1019 | // Move the range so it does not contain inline elements.\r | |
1020 | // It prevents <span> from being included in <em>.\r | |
1021 | //\r | |
1022 | // <ul>\r | |
1023 | // <li>\r | |
1024 | // <p><a href="#one"><em>x</em></a>^</p>\r | |
1025 | // <ul>\r | |
1026 | // <li><span>y</span></li>\r | |
1027 | // </ul>\r | |
1028 | // </li>\r | |
1029 | // </ul>\r | |
1030 | //\r | |
1031 | // so instead of\r | |
1032 | //\r | |
1033 | // <ul>\r | |
1034 | // <li>\r | |
1035 | // <p><a href="#one"><em>x^<span>y</span></em></a></p>\r | |
1036 | // </li>\r | |
1037 | // </ul>\r | |
1038 | //\r | |
1039 | // pressing DELETE produces\r | |
1040 | //\r | |
1041 | // <ul>\r | |
1042 | // <li>\r | |
1043 | // <p><a href="#one"><em>x</em></a>^<span>y</span></p>\r | |
1044 | // </li>\r | |
1045 | // </ul>\r | |
1046 | if ( farthestInlineAscendant ) {\r | |
1047 | cursor.moveToPosition( farthestInlineAscendant, CKEDITOR.POSITION_AFTER_END );\r | |
1048 | }\r | |
1049 | }\r | |
1050 | }\r | |
1051 | \r | |
1052 | // Moving `cursor` and `next line` only when at the end literally (#12729).\r | |
1053 | if ( isAtEnd == 2 ) {\r | |
1054 | cursor.moveToPosition( cursor.endPath().block, CKEDITOR.POSITION_BEFORE_END );\r | |
1055 | \r | |
1056 | // Next line might be text node not wrapped in block element.\r | |
1057 | if ( nextLine.endPath().block ) {\r | |
1058 | nextLine.moveToPosition( nextLine.endPath().block, CKEDITOR.POSITION_AFTER_START );\r | |
1059 | }\r | |
1060 | }\r | |
1061 | \r | |
1062 | joinNextLineToCursor( editor, cursor, nextLine );\r | |
1063 | evt.cancel();\r | |
1064 | }\r | |
1065 | } else {\r | |
1066 | // Handle Del key pressed before the list.\r | |
1067 | walker.range.setEndAt( editable, CKEDITOR.POSITION_BEFORE_END );\r | |
1068 | next = walker.next();\r | |
1069 | \r | |
1070 | if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.is( listNodeNames ) ) {\r | |
1071 | // The start <li>\r | |
1072 | next = next.getFirst( nonEmpty );\r | |
1073 | \r | |
1074 | // Simply remove the current empty block, move cursor to the\r | |
1075 | // subsequent list.\r | |
1076 | if ( path.block && range.checkStartOfBlock() && range.checkEndOfBlock() ) {\r | |
1077 | path.block.remove();\r | |
1078 | range.moveToElementEditStart( next );\r | |
1079 | range.select();\r | |
1080 | evt.cancel();\r | |
1081 | }\r | |
1082 | // Preventing the default (merge behavior), but simply move\r | |
1083 | // the cursor one character forward if subsequent list item\r | |
1084 | // contains sub list.\r | |
1085 | else if ( getSubList( next ) ) {\r | |
1086 | range.moveToElementEditStart( next );\r | |
1087 | range.select();\r | |
1088 | evt.cancel();\r | |
1089 | }\r | |
1090 | // Merge the first list item with the current line.\r | |
1091 | else {\r | |
1092 | nextLine = range.clone();\r | |
1093 | nextLine.moveToElementEditStart( next );\r | |
1094 | joinNextLineToCursor( editor, cursor, nextLine );\r | |
1095 | evt.cancel();\r | |
1096 | }\r | |
1097 | }\r | |
1098 | }\r | |
1099 | \r | |
1100 | }\r | |
1101 | \r | |
1102 | // The backspace/del could potentially put cursor at a bad position,\r | |
1103 | // being it handled or not, check immediately the selection to have it fixed.\r | |
1104 | setTimeout( function() {\r | |
1105 | editor.selectionChange( 1 );\r | |
1106 | } );\r | |
1107 | }\r | |
1108 | } );\r | |
1109 | }\r | |
1110 | } );\r | |
1111 | } )();\r |