1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 (function() 7 { 8 CKEDITOR.plugins.add( 'enterkey', 9 { 10 requires : [ 'keystrokes', 'indent' ], 11 12 init : function( editor ) 13 { 14 editor.addCommand( 'enter', { 15 modes : { wysiwyg:1 }, 16 editorFocus : false, 17 exec : function( editor ){ enter( editor ); } 18 }); 19 20 editor.addCommand( 'shiftEnter', { 21 modes : { wysiwyg:1 }, 22 editorFocus : false, 23 exec : function( editor ){ shiftEnter( editor ); } 24 }); 25 26 var keystrokes = editor.keystrokeHandler.keystrokes; 27 keystrokes[ 13 ] = 'enter'; 28 keystrokes[ CKEDITOR.SHIFT + 13 ] = 'shiftEnter'; 29 } 30 }); 31 32 CKEDITOR.plugins.enterkey = 33 { 34 enterBlock : function( editor, mode, range, forceMode ) 35 { 36 // Get the range for the current selection. 37 range = range || getRange( editor ); 38 39 // We may not have valid ranges to work on, like when inside a 40 // contenteditable=false element. 41 if ( !range ) 42 return; 43 44 var doc = range.document; 45 46 var atBlockStart = range.checkStartOfBlock(), 47 atBlockEnd = range.checkEndOfBlock(), 48 path = new CKEDITOR.dom.elementPath( range.startContainer ), 49 block = path.block; 50 51 if ( atBlockStart && atBlockEnd ) 52 { 53 // Exit the list when we're inside an empty list item block. (#5376) 54 if ( block && ( block.is( 'li' ) || block.getParent().is( 'li' ) ) ) 55 { 56 editor.execCommand( 'outdent' ); 57 return; 58 } 59 60 if ( block && block.getParent().is( 'blockquote' ) ) 61 { 62 block.breakParent( block.getParent() ); 63 64 // If we were at the start of <blockquote>, there will be an empty element before it now. 65 if ( !block.getPrevious().getFirst( CKEDITOR.dom.walker.invisible(1) ) ) 66 block.getPrevious().remove(); 67 68 // If we were at the end of <blockquote>, there will be an empty element after it now. 69 if ( !block.getNext().getFirst( CKEDITOR.dom.walker.invisible(1) ) ) 70 block.getNext().remove(); 71 72 range.moveToElementEditStart( block ); 73 range.select(); 74 return; 75 } 76 } 77 // Don't split <pre> if we're in the middle of it, act as shift enter key. 78 else if ( block && block.is( 'pre' ) ) 79 { 80 if ( !atBlockEnd ) 81 { 82 enterBr( editor, mode, range, forceMode ); 83 return; 84 } 85 } 86 // Don't split caption blocks. (#7944) 87 else if ( block && CKEDITOR.dtd.$captionBlock[ block.getName() ] ) 88 { 89 enterBr( editor, mode, range, forceMode ); 90 return; 91 } 92 93 // Determine the block element to be used. 94 var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ); 95 96 // Split the range. 97 var splitInfo = range.splitBlock( blockTag ); 98 99 if ( !splitInfo ) 100 return; 101 102 // Get the current blocks. 103 var previousBlock = splitInfo.previousBlock, 104 nextBlock = splitInfo.nextBlock; 105 106 var isStartOfBlock = splitInfo.wasStartOfBlock, 107 isEndOfBlock = splitInfo.wasEndOfBlock; 108 109 var node; 110 111 // If this is a block under a list item, split it as well. (#1647) 112 if ( nextBlock ) 113 { 114 node = nextBlock.getParent(); 115 if ( node.is( 'li' ) ) 116 { 117 nextBlock.breakParent( node ); 118 nextBlock.move( nextBlock.getNext(), 1 ); 119 } 120 } 121 else if ( previousBlock && ( node = previousBlock.getParent() ) && node.is( 'li' ) ) 122 { 123 previousBlock.breakParent( node ); 124 node = previousBlock.getNext(); 125 range.moveToElementEditStart( node ); 126 previousBlock.move( previousBlock.getPrevious() ); 127 } 128 129 // If we have both the previous and next blocks, it means that the 130 // boundaries were on separated blocks, or none of them where on the 131 // block limits (start/end). 132 if ( !isStartOfBlock && !isEndOfBlock ) 133 { 134 // If the next block is an <li> with another list tree as the first 135 // child, we'll need to append a filler (<br>/NBSP) or the list item 136 // wouldn't be editable. (#1420) 137 if ( nextBlock.is( 'li' ) 138 && ( node = nextBlock.getFirst( CKEDITOR.dom.walker.invisible( true ) ) ) 139 && node.is && node.is( 'ul', 'ol' ) ) 140 ( CKEDITOR.env.ie ? doc.createText( '\xa0' ) : doc.createElement( 'br' ) ).insertBefore( node ); 141 142 // Move the selection to the end block. 143 if ( nextBlock ) 144 range.moveToElementEditStart( nextBlock ); 145 } 146 else 147 { 148 var newBlock, 149 newBlockDir; 150 151 if ( previousBlock ) 152 { 153 // Do not enter this block if it's a header tag, or we are in 154 // a Shift+Enter (#77). Create a new block element instead 155 // (later in the code). 156 if ( previousBlock.is( 'li' ) || 157 ! ( headerTagRegex.test( previousBlock.getName() ) || previousBlock.is( 'pre' ) ) ) 158 { 159 // Otherwise, duplicate the previous block. 160 newBlock = previousBlock.clone(); 161 } 162 } 163 else if ( nextBlock ) 164 newBlock = nextBlock.clone(); 165 166 if ( !newBlock ) 167 { 168 // We have already created a new list item. (#6849) 169 if ( node && node.is( 'li' ) ) 170 newBlock = node; 171 else 172 { 173 newBlock = doc.createElement( blockTag ); 174 if ( previousBlock && ( newBlockDir = previousBlock.getDirection() ) ) 175 newBlock.setAttribute( 'dir', newBlockDir ); 176 } 177 } 178 // Force the enter block unless we're talking of a list item. 179 else if ( forceMode && !newBlock.is( 'li' ) ) 180 newBlock.renameNode( blockTag ); 181 182 // Recreate the inline elements tree, which was available 183 // before hitting enter, so the same styles will be available in 184 // the new block. 185 var elementPath = splitInfo.elementPath; 186 if ( elementPath ) 187 { 188 for ( var i = 0, len = elementPath.elements.length ; i < len ; i++ ) 189 { 190 var element = elementPath.elements[ i ]; 191 192 if ( element.equals( elementPath.block ) || element.equals( elementPath.blockLimit ) ) 193 break; 194 195 if ( CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) 196 { 197 element = element.clone(); 198 newBlock.moveChildren( element ); 199 newBlock.append( element ); 200 } 201 } 202 } 203 204 if ( !CKEDITOR.env.ie ) 205 newBlock.appendBogus(); 206 207 if ( !newBlock.getParent() ) 208 range.insertNode( newBlock ); 209 210 // list item start number should not be duplicated (#7330), but we need 211 // to remove the attribute after it's onto the DOM tree because of old IEs (#7581). 212 newBlock.is( 'li' ) && newBlock.removeAttribute( 'value' ); 213 214 // This is tricky, but to make the new block visible correctly 215 // we must select it. 216 // The previousBlock check has been included because it may be 217 // empty if we have fixed a block-less space (like ENTER into an 218 // empty table cell). 219 if ( CKEDITOR.env.ie && isStartOfBlock && ( !isEndOfBlock || !previousBlock.getChildCount() ) ) 220 { 221 // Move the selection to the new block. 222 range.moveToElementEditStart( isEndOfBlock ? previousBlock : newBlock ); 223 range.select(); 224 } 225 226 // Move the selection to the new block. 227 range.moveToElementEditStart( isStartOfBlock && !isEndOfBlock ? nextBlock : newBlock ); 228 } 229 230 if ( !CKEDITOR.env.ie ) 231 { 232 if ( nextBlock ) 233 { 234 // If we have split the block, adds a temporary span at the 235 // range position and scroll relatively to it. 236 var tmpNode = doc.createElement( 'span' ); 237 238 // We need some content for Safari. 239 tmpNode.setHtml( ' ' ); 240 241 range.insertNode( tmpNode ); 242 tmpNode.scrollIntoView(); 243 range.deleteContents(); 244 } 245 else 246 { 247 // We may use the above scroll logic for the new block case 248 // too, but it gives some weird result with Opera. 249 newBlock.scrollIntoView(); 250 } 251 } 252 253 range.select(); 254 }, 255 256 enterBr : function( editor, mode, range, forceMode ) 257 { 258 // Get the range for the current selection. 259 range = range || getRange( editor ); 260 261 // We may not have valid ranges to work on, like when inside a 262 // contenteditable=false element. 263 if ( !range ) 264 return; 265 266 var doc = range.document; 267 268 // Determine the block element to be used. 269 var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ); 270 271 var isEndOfBlock = range.checkEndOfBlock(); 272 273 var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() ); 274 275 var startBlock = elementPath.block, 276 startBlockTag = startBlock && elementPath.block.getName(); 277 278 var isPre = false; 279 280 if ( !forceMode && startBlockTag == 'li' ) 281 { 282 enterBlock( editor, mode, range, forceMode ); 283 return; 284 } 285 286 // If we are at the end of a header block. 287 if ( !forceMode && isEndOfBlock && headerTagRegex.test( startBlockTag ) ) 288 { 289 var newBlock, 290 newBlockDir; 291 292 if ( ( newBlockDir = startBlock.getDirection() ) ) 293 { 294 newBlock = doc.createElement( 'div' ); 295 newBlock.setAttribute( 'dir', newBlockDir ); 296 newBlock.insertAfter( startBlock ); 297 range.setStart( newBlock, 0 ); 298 } 299 else 300 { 301 // Insert a <br> after the current paragraph. 302 doc.createElement( 'br' ).insertAfter( startBlock ); 303 304 // A text node is required by Gecko only to make the cursor blink. 305 if ( CKEDITOR.env.gecko ) 306 doc.createText( '' ).insertAfter( startBlock ); 307 308 // IE has different behaviors regarding position. 309 range.setStartAt( startBlock.getNext(), CKEDITOR.env.ie ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_START ); 310 } 311 } 312 else 313 { 314 var lineBreak; 315 316 isPre = ( startBlockTag == 'pre' ); 317 318 // Gecko prefers <br> as line-break inside <pre> (#4711). 319 if ( isPre && !CKEDITOR.env.gecko ) 320 lineBreak = doc.createText( CKEDITOR.env.ie ? '\r' : '\n' ); 321 else 322 lineBreak = doc.createElement( 'br' ); 323 324 range.deleteContents(); 325 range.insertNode( lineBreak ); 326 327 // IE has different behavior regarding position. 328 if ( CKEDITOR.env.ie ) 329 range.setStartAt( lineBreak, CKEDITOR.POSITION_AFTER_END ); 330 else 331 { 332 // A text node is required by Gecko only to make the cursor blink. 333 // We need some text inside of it, so the bogus <br> is properly 334 // created. 335 doc.createText( '\ufeff' ).insertAfter( lineBreak ); 336 337 // If we are at the end of a block, we must be sure the bogus node is available in that block. 338 if ( isEndOfBlock ) 339 lineBreak.getParent().appendBogus(); 340 341 // Now we can remove the text node contents, so the caret doesn't 342 // stop on it. 343 lineBreak.getNext().$.nodeValue = ''; 344 345 range.setStartAt( lineBreak.getNext(), CKEDITOR.POSITION_AFTER_START ); 346 347 // Scroll into view, for non IE. 348 var dummy = null; 349 350 // BR is not positioned in Opera and Webkit. 351 if ( !CKEDITOR.env.gecko ) 352 { 353 dummy = doc.createElement( 'span' ); 354 // We need have some contents for Webkit to position it 355 // under parent node. ( #3681) 356 dummy.setHtml(' '); 357 } 358 else 359 dummy = doc.createElement( 'br' ); 360 361 dummy.insertBefore( lineBreak.getNext() ); 362 dummy.scrollIntoView(); 363 dummy.remove(); 364 } 365 } 366 367 // This collapse guarantees the cursor will be blinking. 368 range.collapse( true ); 369 370 range.select( isPre ); 371 } 372 }; 373 374 var plugin = CKEDITOR.plugins.enterkey, 375 enterBr = plugin.enterBr, 376 enterBlock = plugin.enterBlock, 377 headerTagRegex = /^h[1-6]$/; 378 379 function shiftEnter( editor ) 380 { 381 // Only effective within document. 382 if ( editor.mode != 'wysiwyg' ) 383 return false; 384 385 // On SHIFT+ENTER: 386 // 1. We want to enforce the mode to be respected, instead 387 // of cloning the current block. (#77) 388 return enter( editor, editor.config.shiftEnterMode, 1 ); 389 } 390 391 function enter( editor, mode, forceMode ) 392 { 393 forceMode = editor.config.forceEnterMode || forceMode; 394 395 // Only effective within document. 396 if ( editor.mode != 'wysiwyg' ) 397 return false; 398 399 if ( !mode ) 400 mode = editor.config.enterMode; 401 402 // Use setTimout so the keys get cancelled immediatelly. 403 setTimeout( function() 404 { 405 editor.fire( 'saveSnapshot' ); // Save undo step. 406 407 if ( mode == CKEDITOR.ENTER_BR ) 408 enterBr( editor, mode, null, forceMode ); 409 else 410 enterBlock( editor, mode, null, forceMode ); 411 412 editor.fire( 'saveSnapshot' ); 413 414 }, 0 ); 415 416 return true; 417 } 418 419 function getRange( editor ) 420 { 421 // Get the selection ranges. 422 var ranges = editor.getSelection().getRanges( true ); 423 424 // Delete the contents of all ranges except the first one. 425 for ( var i = ranges.length - 1 ; i > 0 ; i-- ) 426 { 427 ranges[ i ].deleteContents(); 428 } 429 430 // Return the first range. 431 return ranges[ 0 ]; 432 } 433 })(); 434