1/* 2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net 3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben 4 * 5 * == BEGIN LICENSE == 6 * 7 * Licensed under the terms of any of the following licenses at your 8 * choice: 9 * 10 * - GNU General Public License Version 2 or later (the "GPL") 11 * http://www.gnu.org/licenses/gpl.html 12 * 13 * - GNU Lesser General Public License Version 2.1 or later (the "LGPL") 14 * http://www.gnu.org/licenses/lgpl.html 15 * 16 * - Mozilla Public License Version 1.1 or later (the "MPL") 17 * http://www.mozilla.org/MPL/MPL-1.1.html 18 * 19 * == END LICENSE == 20 * 21 * Controls the [Enter] keystroke behavior in a document. 22 */ 23 24/* 25 * Constructor. 26 * @targetDocument : the target document. 27 * @enterMode : the behavior for the <Enter> keystroke. 28 * May be "p", "div", "br". Default is "p". 29 * @shiftEnterMode : the behavior for the <Shift>+<Enter> keystroke. 30 * May be "p", "div", "br". Defaults to "br". 31 */ 32var FCKEnterKey = function( targetWindow, enterMode, shiftEnterMode, tabSpaces ) 33{ 34 this.Window = targetWindow ; 35 this.EnterMode = enterMode || 'p' ; 36 this.ShiftEnterMode = shiftEnterMode || 'br' ; 37 38 // Setup the Keystroke Handler. 39 var oKeystrokeHandler = new FCKKeystrokeHandler( false ) ; 40 oKeystrokeHandler._EnterKey = this ; 41 oKeystrokeHandler.OnKeystroke = FCKEnterKey_OnKeystroke ; 42 43 oKeystrokeHandler.SetKeystrokes( [ 44 [ 13 , 'Enter' ], 45 [ SHIFT + 13, 'ShiftEnter' ], 46 [ 9 , 'Tab' ], 47 [ 8 , 'Backspace' ], 48 [ CTRL + 8 , 'CtrlBackspace' ], 49 [ 46 , 'Delete' ] 50 ] ) ; 51 52 if ( tabSpaces > 0 ) 53 { 54 this.TabText = '' ; 55 while ( tabSpaces-- > 0 ) 56 this.TabText += '\xa0' ; 57 } 58 59 oKeystrokeHandler.AttachToElement( targetWindow.document ) ; 60} 61 62 63function FCKEnterKey_OnKeystroke( keyCombination, keystrokeValue ) 64{ 65 var oEnterKey = this._EnterKey ; 66 67 try 68 { 69 switch ( keystrokeValue ) 70 { 71 case 'Enter' : 72 return oEnterKey.DoEnter() ; 73 break ; 74 case 'ShiftEnter' : 75 return oEnterKey.DoShiftEnter() ; 76 break ; 77 case 'Backspace' : 78 return oEnterKey.DoBackspace() ; 79 break ; 80 case 'Delete' : 81 return oEnterKey.DoDelete() ; 82 break ; 83 case 'Tab' : 84 return oEnterKey.DoTab() ; 85 break ; 86 case 'CtrlBackspace' : 87 return oEnterKey.DoCtrlBackspace() ; 88 break ; 89 } 90 } 91 catch (e) 92 { 93 // If for any reason we are not able to handle it, go 94 // ahead with the browser default behavior. 95 } 96 97 return false ; 98} 99 100/* 101 * Executes the <Enter> key behavior. 102 */ 103FCKEnterKey.prototype.DoEnter = function( mode, hasShift ) 104{ 105 // Save an undo snapshot before doing anything 106 FCKUndo.SaveUndoStep() ; 107 108 this._HasShift = ( hasShift === true ) ; 109 110 var parentElement = FCKSelection.GetParentElement() ; 111 var parentPath = new FCKElementPath( parentElement ) ; 112 var sMode = mode || this.EnterMode ; 113 114 if ( sMode == 'br' || parentPath.Block && parentPath.Block.tagName.toLowerCase() == 'pre' ) 115 return this._ExecuteEnterBr() ; 116 else 117 return this._ExecuteEnterBlock( sMode ) ; 118} 119 120/* 121 * Executes the <Shift>+<Enter> key behavior. 122 */ 123FCKEnterKey.prototype.DoShiftEnter = function() 124{ 125 return this.DoEnter( this.ShiftEnterMode, true ) ; 126} 127 128/* 129 * Executes the <Backspace> key behavior. 130 */ 131FCKEnterKey.prototype.DoBackspace = function() 132{ 133 var bCustom = false ; 134 135 // Get the current selection. 136 var oRange = new FCKDomRange( this.Window ) ; 137 oRange.MoveToSelection() ; 138 139 // Kludge for #247 140 if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) ) 141 { 142 this._FixIESelectAllBug( oRange ) ; 143 return true ; 144 } 145 146 var isCollapsed = oRange.CheckIsCollapsed() ; 147 148 if ( !isCollapsed ) 149 { 150 // Bug #327, Backspace with an img selection would activate the default action in IE. 151 // Let's override that with our logic here. 152 if ( FCKBrowserInfo.IsIE && this.Window.document.selection.type.toLowerCase() == "control" ) 153 { 154 var controls = this.Window.document.selection.createRange() ; 155 for ( var i = controls.length - 1 ; i >= 0 ; i-- ) 156 { 157 var el = controls.item( i ) ; 158 el.parentNode.removeChild( el ) ; 159 } 160 return true ; 161 } 162 163 return false ; 164 } 165 166 var oStartBlock = oRange.StartBlock ; 167 var oEndBlock = oRange.EndBlock ; 168 169 // The selection boundaries must be in the same "block limit" element 170 if ( oRange.StartBlockLimit == oRange.EndBlockLimit && oStartBlock && oEndBlock ) 171 { 172 if ( !isCollapsed ) 173 { 174 var bEndOfBlock = oRange.CheckEndOfBlock() ; 175 176 oRange.DeleteContents() ; 177 178 if ( oStartBlock != oEndBlock ) 179 { 180 oRange.SetStart(oEndBlock,1) ; 181 oRange.SetEnd(oEndBlock,1) ; 182 183// if ( bEndOfBlock ) 184// oEndBlock.parentNode.removeChild( oEndBlock ) ; 185 } 186 187 oRange.Select() ; 188 189 bCustom = ( oStartBlock == oEndBlock ) ; 190 } 191 192 if ( oRange.CheckStartOfBlock() ) 193 { 194 var oCurrentBlock = oRange.StartBlock ; 195 196 var ePrevious = FCKDomTools.GetPreviousSourceElement( oCurrentBlock, true, [ 'BODY', oRange.StartBlockLimit.nodeName ], ['UL','OL'] ) ; 197 198 bCustom = this._ExecuteBackspace( oRange, ePrevious, oCurrentBlock ) ; 199 } 200 else if ( FCKBrowserInfo.IsGeckoLike ) 201 { 202 // Firefox and Opera (#1095) loose the selection when executing 203 // CheckStartOfBlock, so we must reselect. 204 oRange.Select() ; 205 } 206 } 207 208 oRange.Release() ; 209 return bCustom ; 210} 211 212FCKEnterKey.prototype.DoCtrlBackspace = function() 213{ 214 FCKUndo.SaveUndoStep() ; 215 var oRange = new FCKDomRange( this.Window ) ; 216 oRange.MoveToSelection() ; 217 if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) ) 218 { 219 this._FixIESelectAllBug( oRange ) ; 220 return true ; 221 } 222 return false ; 223} 224 225FCKEnterKey.prototype._ExecuteBackspace = function( range, previous, currentBlock ) 226{ 227 var bCustom = false ; 228 229 // We could be in a nested LI. 230 if ( !previous && currentBlock && currentBlock.nodeName.IEquals( 'LI' ) && currentBlock.parentNode.parentNode.nodeName.IEquals( 'LI' ) ) 231 { 232 this._OutdentWithSelection( currentBlock, range ) ; 233 return true ; 234 } 235 236 if ( previous && previous.nodeName.IEquals( 'LI' ) ) 237 { 238 var oNestedList = FCKDomTools.GetLastChild( previous, ['UL','OL'] ) ; 239 240 while ( oNestedList ) 241 { 242 previous = FCKDomTools.GetLastChild( oNestedList, 'LI' ) ; 243 oNestedList = FCKDomTools.GetLastChild( previous, ['UL','OL'] ) ; 244 } 245 } 246 247 if ( previous && currentBlock ) 248 { 249 // If we are in a LI, and the previous block is not an LI, we must outdent it. 250 if ( currentBlock.nodeName.IEquals( 'LI' ) && !previous.nodeName.IEquals( 'LI' ) ) 251 { 252 this._OutdentWithSelection( currentBlock, range ) ; 253 return true ; 254 } 255 256 // Take a reference to the parent for post processing cleanup. 257 var oCurrentParent = currentBlock.parentNode ; 258 259 var sPreviousName = previous.nodeName.toLowerCase() ; 260 if ( FCKListsLib.EmptyElements[ sPreviousName ] != null || sPreviousName == 'table' ) 261 { 262 FCKDomTools.RemoveNode( previous ) ; 263 bCustom = true ; 264 } 265 else 266 { 267 // Remove the current block. 268 FCKDomTools.RemoveNode( currentBlock ) ; 269 270 // Remove any empty tag left by the block removal. 271 while ( oCurrentParent.innerHTML.Trim().length == 0 ) 272 { 273 var oParent = oCurrentParent.parentNode ; 274 oParent.removeChild( oCurrentParent ) ; 275 oCurrentParent = oParent ; 276 } 277 278 // Cleanup the previous and the current elements. 279 FCKDomTools.LTrimNode( currentBlock ) ; 280 FCKDomTools.RTrimNode( previous ) ; 281 282 // Append a space to the previous. 283 // Maybe it is not always desirable... 284 // previous.appendChild( this.Window.document.createTextNode( ' ' ) ) ; 285 286 // Set the range to the end of the previous element and bookmark it. 287 range.SetStart( previous, 2, true ) ; 288 range.Collapse( true ) ; 289 var oBookmark = range.CreateBookmark() ; 290 291 // Move the contents of the block to the previous element and delete it. 292 // But for some block types (e.g. table), moving the children to the previous block makes no sense. 293 // So a check is needed. (See #1081) 294 if ( ! currentBlock.tagName.IEquals( [ 'TABLE' ] ) ) 295 FCKDomTools.MoveChildren( currentBlock, previous ) ; 296 297 // Place the selection at the bookmark. 298 range.MoveToBookmark( oBookmark ) ; 299 range.Select() ; 300 301 bCustom = true ; 302 } 303 } 304 305 return bCustom ; 306} 307 308/* 309 * Executes the <Delete> key behavior. 310 */ 311FCKEnterKey.prototype.DoDelete = function() 312{ 313 // Save an undo snapshot before doing anything 314 // This is to conform with the behavior seen in MS Word 315 FCKUndo.SaveUndoStep() ; 316 317 // The <Delete> has the same effect as the <Backspace>, so we have the same 318 // results if we just move to the next block and apply the same <Backspace> logic. 319 320 var bCustom = false ; 321 322 // Get the current selection. 323 var oRange = new FCKDomRange( this.Window ) ; 324 oRange.MoveToSelection() ; 325 326 // Kludge for #247 327 if ( FCKBrowserInfo.IsIE && this._CheckIsAllContentsIncluded( oRange, this.Window.document.body ) ) 328 { 329 this._FixIESelectAllBug( oRange ) ; 330 return true ; 331 } 332 333 // There is just one special case for collapsed selections at the end of a block. 334 if ( oRange.CheckIsCollapsed() && oRange.CheckEndOfBlock( FCKBrowserInfo.IsGeckoLike ) ) 335 { 336 var oCurrentBlock = oRange.StartBlock ; 337 var eCurrentCell = FCKTools.GetElementAscensor( oCurrentBlock, 'td' ); 338 339 var eNext = FCKDomTools.GetNextSourceElement( oCurrentBlock, true, [ oRange.StartBlockLimit.nodeName ], 340 ['UL','OL','TR'] ) ; 341 342 // Bug #1323 : if we're in a table cell, and the next node belongs to a different cell, then don't 343 // delete anything. 344 if ( eCurrentCell ) 345 { 346 var eNextCell = FCKTools.GetElementAscensor( eNext, 'td' ); 347 if ( eNextCell != eCurrentCell ) 348 return true ; 349 } 350 351 bCustom = this._ExecuteBackspace( oRange, oCurrentBlock, eNext ) ; 352 } 353 354 oRange.Release() ; 355 return bCustom ; 356} 357 358/* 359 * Executes the <Tab> key behavior. 360 */ 361FCKEnterKey.prototype.DoTab = function() 362{ 363 var oRange = new FCKDomRange( this.Window ); 364 oRange.MoveToSelection() ; 365 366 // If the user pressed <tab> inside a table, we should give him the default behavior ( moving between cells ) 367 // instead of giving him more non-breaking spaces. (Bug #973) 368 var node = oRange._Range.startContainer ; 369 while ( node ) 370 { 371 if ( node.nodeType == 1 ) 372 { 373 var tagName = node.tagName.toLowerCase() ; 374 if ( tagName == "tr" || tagName == "td" || tagName == "th" || tagName == "tbody" || tagName == "table" ) 375 return false ; 376 else 377 break ; 378 } 379 node = node.parentNode ; 380 } 381 382 if ( this.TabText ) 383 { 384 oRange.DeleteContents() ; 385 oRange.InsertNode( this.Window.document.createTextNode( this.TabText ) ) ; 386 oRange.Collapse( false ) ; 387 oRange.Select() ; 388 } 389 return true ; 390} 391 392FCKEnterKey.prototype._ExecuteEnterBlock = function( blockTag, range ) 393{ 394 // Get the current selection. 395 var oRange = range || new FCKDomRange( this.Window ) ; 396 397 var oSplitInfo = oRange.SplitBlock() ; 398 399 if ( oSplitInfo ) 400 { 401 // Get the current blocks. 402 var ePreviousBlock = oSplitInfo.PreviousBlock ; 403 var eNextBlock = oSplitInfo.NextBlock ; 404 405 var bIsStartOfBlock = oSplitInfo.WasStartOfBlock ; 406 var bIsEndOfBlock = oSplitInfo.WasEndOfBlock ; 407 408 // If we have both the previous and next blocks, it means that the 409 // boundaries were on separated blocks, or none of them where on the 410 // block limits (start/end). 411 if ( !oSplitInfo.WasStartOfBlock && !oSplitInfo.WasEndOfBlock ) 412 { 413 // Move the selection to the end block. 414 if ( eNextBlock ) 415 oRange.MoveToElementEditStart( eNextBlock ) ; 416 } 417 else 418 { 419 if ( bIsStartOfBlock && bIsEndOfBlock && ePreviousBlock.tagName.toUpperCase() == 'LI' ) 420 { 421 oRange.MoveToElementStart( ePreviousBlock ) ; 422 this._OutdentWithSelection( ePreviousBlock, oRange ) ; 423 oRange.Release() ; 424 return true ; 425 } 426 427 var eNewBlock ; 428 429 if ( ePreviousBlock ) 430 { 431 var sPreviousBlockTag = ePreviousBlock.tagName.toUpperCase() ; 432 433 // If is a header tag, or we are in a Shift+Enter (#77), 434 // create a new block element. 435 if ( this._HasShift || (/^H[1-6]$/).test( sPreviousBlockTag ) ) 436 eNewBlock = this.Window.document.createElement( blockTag ) ; 437 else 438 { 439 // Otherwise, duplicate the previous block. 440 eNewBlock = FCKDomTools.CloneElement( ePreviousBlock ) ; 441 442 this._RecreateEndingTree( ePreviousBlock, eNewBlock ) ; 443 } 444 } 445 else if ( eNextBlock ) 446 { 447 eNewBlock = FCKDomTools.CloneElement( eNextBlock ) ; 448 } 449 else 450 eNewBlock = this.Window.document.createElement( blockTag ) ; 451 452 if ( FCKBrowserInfo.IsGeckoLike ) 453 FCKTools.AppendBogusBr( eNewBlock ) ; 454 455 oRange.InsertNode( eNewBlock ) ; 456 457 // This is tricky, but to make the new block visible correctly 458 // we must select it. 459 if ( FCKBrowserInfo.IsIE ) 460 { 461 // Move the selection to the new block. 462 oRange.MoveToNodeContents( eNewBlock ) ; 463 oRange.Select() ; 464 } 465 466 oRange.MoveToElementEditStart( bIsStartOfBlock && !bIsEndOfBlock ? eNextBlock : eNewBlock ) ; 467 468 if ( FCKBrowserInfo.IsGeckoLike ) 469 eNewBlock.scrollIntoView( false ) ; 470 } 471 472 oRange.Select() ; 473 } 474 475 // Release the resources used by the range. 476 oRange.Release() ; 477 478 return true ; 479} 480 481FCKEnterKey.prototype._ExecuteEnterBr = function( blockTag ) 482{ 483 // Get the current selection. 484 var oRange = new FCKDomRange( this.Window ) ; 485 oRange.MoveToSelection() ; 486 487 // The selection boundaries must be in the same "block limit" element. 488 if ( oRange.StartBlockLimit == oRange.EndBlockLimit ) 489 { 490 oRange.DeleteContents() ; 491 492 // Get the new selection (it is collapsed at this point). 493 oRange.MoveToSelection() ; 494 495 var bIsStartOfBlock = oRange.CheckStartOfBlock() ; 496 var bIsEndOfBlock = oRange.CheckEndOfBlock() ; 497 498 var sStartBlockTag = oRange.StartBlock ? oRange.StartBlock.tagName.toUpperCase() : '' ; 499 500 var bHasShift = this._HasShift ; 501 502 if ( !bHasShift && sStartBlockTag == 'LI' ) 503 return this._ExecuteEnterBlock( null, oRange ) ; 504 505 // If we are at the end of a header block. 506 if ( !bHasShift && bIsEndOfBlock && (/^H[1-6]$/).test( sStartBlockTag ) ) 507 { 508 // Insert a BR after the current paragraph. 509 FCKDomTools.InsertAfterNode( oRange.StartBlock, this.Window.document.createElement( 'br' ) ) ; 510 511 // The space is required by Gecko only to make the cursor blink. 512 if ( FCKBrowserInfo.IsGecko ) 513 FCKDomTools.InsertAfterNode( oRange.StartBlock, this.Window.document.createTextNode( '' ) ) ; 514 515 // IE and Gecko have different behaviors regarding the position. 516 oRange.SetStart( oRange.StartBlock.nextSibling, FCKBrowserInfo.IsIE ? 3 : 1 ) ; 517 } 518 else 519 { 520 var eLineBreak = null ; 521 if ( sStartBlockTag.IEquals( 'pre' ) ) 522 eLineBreak = this.Window.document.createTextNode( FCKBrowserInfo.IsIE ? '\r' : '\n' ) ; 523 else 524 eLineBreak = this.Window.document.createElement( 'br' ) ; 525 526 oRange.InsertNode( eLineBreak ) ; 527 528 // The space is required by Gecko only to make the cursor blink. 529 if ( FCKBrowserInfo.IsGecko ) 530 FCKDomTools.InsertAfterNode( eLineBreak, this.Window.document.createTextNode( '' ) ) ; 531 532 // If we are at the end of a block, we must be sure the bogus node is available in that block. 533 if ( bIsEndOfBlock && FCKBrowserInfo.IsGeckoLike ) 534 FCKTools.AppendBogusBr( eLineBreak.parentNode ) ; 535 536 if ( FCKBrowserInfo.IsIE ) 537 oRange.SetStart( eLineBreak, 4 ) ; 538 else 539 oRange.SetStart( eLineBreak.nextSibling, 1 ) ; 540 541 if ( ! FCKBrowserInfo.IsIE ) 542 { 543 var dummy = null ; 544 if ( FCKBrowserInfo.IsOpera ) 545 dummy = this.Window.document.createElement( 'span' ) ; 546 else 547 dummy = this.Window.document.createElement( 'br' ) ; 548 eLineBreak.parentNode.insertBefore( dummy, eLineBreak.nextSibling ) ; 549 dummy.scrollIntoView( false ) ; 550 dummy.parentNode.removeChild( dummy ) ; 551 } 552 } 553 554 // This collapse guarantees the cursor will be blinking. 555 oRange.Collapse( true ) ; 556 557 oRange.Select() ; 558 } 559 560 // Release the resources used by the range. 561 oRange.Release() ; 562 563 return true ; 564} 565 566// Recreate the elements tree at the end of the source block, at the beginning 567// of the target block. Eg.: 568// If source = <p><u>Some</u> sample <b><i>text</i></b></p> then target = <p><b><i></i></b></p> 569// If source = <p><u>Some</u> sample text</p> then target = <p></p> 570FCKEnterKey.prototype._RecreateEndingTree = function( source, target ) 571{ 572 while ( ( source = source.lastChild ) && source.nodeType == 1 && FCKListsLib.InlineChildReqElements[ source.nodeName.toLowerCase() ] != null ) 573 target = target.insertBefore( FCKDomTools.CloneElement( source ), target.firstChild ) ; 574} 575 576// Outdents a LI, maintaining the selection defined on a range. 577FCKEnterKey.prototype._OutdentWithSelection = function( li, range ) 578{ 579 var oBookmark = range.CreateBookmark() ; 580 581 FCKListHandler.OutdentListItem( li ) ; 582 583 range.MoveToBookmark( oBookmark ) ; 584 range.Select() ; 585} 586 587// Is all the contents under a node included by a range? 588FCKEnterKey.prototype._CheckIsAllContentsIncluded = function( range, node ) 589{ 590 var startOk = false ; 591 var endOk = false ; 592 593 /* 594 FCKDebug.Output( 'sc='+range.StartContainer.nodeName+ 595 ',so='+range._Range.startOffset+ 596 ',ec='+range.EndContainer.nodeName+ 597 ',eo='+range._Range.endOffset ) ; 598 */ 599 if ( range.StartContainer == node || range.StartContainer == node.firstChild ) 600 startOk = ( range._Range.startOffset == 0 ) ; 601 602 if ( range.EndContainer == node || range.EndContainer == node.lastChild ) 603 { 604 var nodeLength = range.EndContainer.nodeType == 3 ? range.EndContainer.length : range.EndContainer.childNodes.length ; 605 endOk = ( range._Range.endOffset == nodeLength ) ; 606 } 607 608 return startOk && endOk ; 609} 610 611// Kludge for #247 612FCKEnterKey.prototype._FixIESelectAllBug = function( range ) 613{ 614 var doc = this.Window.document ; 615 doc.body.innerHTML = '' ; 616 var editBlock ; 617 if ( FCKConfig.EnterMode.IEquals( ['div', 'p'] ) ) 618 { 619 editBlock = doc.createElement( FCKConfig.EnterMode ) ; 620 doc.body.appendChild( editBlock ) ; 621 } 622 else 623 editBlock = doc.body ; 624 625 range.MoveToNodeContents( editBlock ) ; 626 range.Collapse( true ) ; 627 range.Select() ; 628 range.Release() ; 629} 630