1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 /** 7 * @fileOverview The "wysiwygarea" plugin. It registers the "wysiwyg" editing 8 * mode, which handles the main editing area space. 9 */ 10 11 (function() 12 { 13 /** 14 * List of elements in which has no way to move editing focus outside. 15 */ 16 var nonExitableElementNames = { table:1,pre:1 }; 17 // Matching an empty paragraph at the end of document. 18 var emptyParagraphRegexp = /\s*<(p|div|address|h\d|center)[^>]*>\s*(?:<br[^>]*>| | )\s*(:?<\/\1>)?\s*$/gi; 19 20 function onInsertHtml( evt ) 21 { 22 if ( this.mode == 'wysiwyg' ) 23 { 24 this.focus(); 25 26 var selection = this.getSelection(), 27 data = evt.data; 28 29 if ( this.dataProcessor ) 30 data = this.dataProcessor.toHtml( data ); 31 32 if ( CKEDITOR.env.ie ) 33 { 34 var selIsLocked = selection.isLocked; 35 36 if ( selIsLocked ) 37 selection.unlock(); 38 39 var $sel = selection.getNative(); 40 if ( $sel.type == 'Control' ) 41 $sel.clear(); 42 $sel.createRange().pasteHTML( data ); 43 44 if ( selIsLocked ) 45 this.getSelection().lock(); 46 } 47 else 48 this.document.$.execCommand( 'inserthtml', false, data ); 49 } 50 } 51 52 function onInsertElement( evt ) 53 { 54 if ( this.mode == 'wysiwyg' ) 55 { 56 this.focus(); 57 this.fire( 'saveSnapshot' ); 58 59 var element = evt.data, 60 elementName = element.getName(), 61 isBlock = CKEDITOR.dtd.$block[ elementName ]; 62 63 var selection = this.getSelection(), 64 ranges = selection.getRanges(); 65 66 var selIsLocked = selection.isLocked; 67 68 if ( selIsLocked ) 69 selection.unlock(); 70 71 var range, clone, lastElement, bookmark; 72 73 for ( var i = ranges.length - 1 ; i >= 0 ; i-- ) 74 { 75 range = ranges[ i ]; 76 77 // Remove the original contents. 78 range.deleteContents(); 79 80 clone = !i && element || element.clone( true ); 81 82 // If we're inserting a block at dtd-violated position, split 83 // the parent blocks until we reach blockLimit. 84 var parent, dtd; 85 if ( this.config.enterMode != CKEDITOR.ENTER_BR && isBlock ) 86 { 87 while( ( parent = range.getCommonAncestor( false, true ) ) 88 && ( dtd = CKEDITOR.dtd[ parent.getName() ] ) 89 && !( dtd && dtd [ elementName ] ) ) 90 { 91 range.splitBlock(); 92 } 93 } 94 95 // Insert the new node. 96 range.insertNode( clone ); 97 98 // Save the last element reference so we can make the 99 // selection later. 100 if ( !lastElement ) 101 lastElement = clone; 102 } 103 104 range.moveToPosition( lastElement, CKEDITOR.POSITION_AFTER_END ); 105 106 var next = lastElement.getNextSourceNode( true ); 107 if ( next && next.type == CKEDITOR.NODE_ELEMENT ) 108 range.moveToElementEditStart( next ); 109 110 selection.selectRanges( [ range ] ); 111 112 if ( selIsLocked ) 113 this.getSelection().lock(); 114 115 // Save snaps after the whole execution completed. 116 // This's a workaround for make DOM modification's happened after 117 // 'insertElement' to be included either, e.g. Form-based dialogs' 'commitContents' 118 // call. 119 CKEDITOR.tools.setTimeout( function(){ 120 this.fire( 'saveSnapshot' ); 121 }, 0, this ); 122 } 123 } 124 125 /** 126 * Auto-fixing block-less content by wrapping paragraph (#3190), prevent 127 * non-exitable-block by padding extra br.(#3189) 128 */ 129 function onSelectionChangeFixBody( evt ) 130 { 131 var editor = evt.editor, 132 path = evt.data.path, 133 blockLimit = path.blockLimit, 134 selection = evt.data.selection, 135 range = selection.getRanges()[0], 136 body = editor.document.getBody(), 137 enterMode = editor.config.enterMode; 138 139 // When enterMode set to block, we'll establing new paragraph only if we're 140 // selecting inline contents right under body. (#3657) 141 if ( enterMode != CKEDITOR.ENTER_BR 142 && range.collapsed 143 && blockLimit.getName() == 'body' 144 && !path.block ) 145 { 146 var bms = selection.createBookmarks(), 147 fixedBlock = range.fixBlock( true, 148 editor.config.enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ); 149 150 // For IE, we'll be removing any bogus br ( introduce by fixing body ) 151 // right now to prevent it introducing visual line break. 152 if ( CKEDITOR.env.ie ) 153 { 154 var brNodeList = fixedBlock.getElementsByTag( 'br' ), brNode; 155 for ( var i = 0 ; i < brNodeList.count() ; i++ ) 156 { 157 if( ( brNode = brNodeList.getItem( i ) ) && brNode.hasAttribute( '_cke_bogus' ) ) 158 brNode.remove(); 159 } 160 } 161 162 selection.selectBookmarks( bms ); 163 164 // If the fixed block is blank and is already followed by a exitable 165 // block, we should drop it and move to the exist block(#3684). 166 var children = fixedBlock.getChildren(), 167 count = children.count(), 168 firstChild, 169 previousElement = fixedBlock.getPrevious( true ), 170 nextElement = fixedBlock.getNext( true ), 171 enterBlock; 172 if ( previousElement && previousElement.getName 173 && !( previousElement.getName() in nonExitableElementNames ) ) 174 enterBlock = previousElement; 175 else if ( nextElement && nextElement.getName 176 && !( nextElement.getName() in nonExitableElementNames ) ) 177 enterBlock = nextElement; 178 179 if( ( !count 180 || ( firstChild = children.getItem( 0 ) ) && firstChild.is && firstChild.is( 'br' ) ) 181 && enterBlock ) 182 { 183 fixedBlock.remove(); 184 range.moveToElementEditStart( enterBlock ); 185 range.select(); 186 } 187 } 188 189 // Inserting the padding-br before body if it's preceded by an 190 // unexitable block. 191 var lastNode = body.getLast( true ); 192 if ( lastNode.getName && ( lastNode.getName() in nonExitableElementNames ) ) 193 { 194 var paddingBlock = editor.document.createElement( 195 ( CKEDITOR.env.ie && enterMode != CKEDITOR.ENTER_BR ) ? 196 '<br _cke_bogus="true" />' : 'br' ); 197 body.append( paddingBlock ); 198 } 199 } 200 201 CKEDITOR.plugins.add( 'wysiwygarea', 202 { 203 requires : [ 'editingblock' ], 204 205 init : function( editor ) 206 { 207 var fixForBody = ( editor.config.enterMode != CKEDITOR.ENTER_BR ) 208 ? editor.config.enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false; 209 210 editor.on( 'editingBlockReady', function() 211 { 212 var mainElement, 213 iframe, 214 isLoadingData, 215 isPendingFocus, 216 fireMode; 217 218 // Support for custom document.domain in IE. 219 var isCustomDomain = CKEDITOR.env.isCustomDomain(); 220 221 // Creates the iframe that holds the editable document. 222 var createIFrame = function() 223 { 224 if ( iframe ) 225 iframe.remove(); 226 227 iframe = new CKEDITOR.dom.element( 'iframe' ) 228 .setAttributes({ 229 frameBorder : 0, 230 tabIndex : -1, 231 allowTransparency : true }) 232 .setStyles({ 233 width : '100%', 234 height : '100%' }); 235 236 if ( CKEDITOR.env.ie ) 237 { 238 if ( isCustomDomain ) 239 { 240 // The document domain must be set within the src 241 // attribute. 242 iframe.setAttribute( 'src', 243 'javascript:void( (function(){' + 244 'document.open();' + 245 'document.domain="' + document.domain + '";' + 246 'document.write( window.parent._cke_htmlToLoad_' + editor.name + ' );' + 247 'document.close();' + 248 'window.parent._cke_htmlToLoad_' + editor.name + ' = null;' + 249 '})() )' ); 250 } 251 else 252 // To avoid HTTPS warnings. 253 iframe.setAttribute( 'src', 'javascript:void(0)' ); 254 } 255 256 var accTitle = editor.lang.editorTitle.replace( '%1', editor.name ); 257 258 if ( CKEDITOR.env.gecko ) 259 { 260 // Accessibility attributes for Firefox. 261 mainElement.setAttributes( 262 { 263 role : 'region', 264 title : accTitle 265 } ); 266 iframe.setAttributes( 267 { 268 role : 'region', 269 title : ' ' 270 } ); 271 } 272 else if ( CKEDITOR.env.webkit ) 273 { 274 iframe.setAttribute( 'title', accTitle ); // Safari 4 275 iframe.setAttribute( 'name', accTitle ); // Safari 3 276 } 277 else if ( CKEDITOR.env.ie ) 278 { 279 // Accessibility label for IE. 280 var fieldset = CKEDITOR.dom.element.createFromHtml( 281 '<fieldset style="height:100%' + 282 ( CKEDITOR.env.quirks ? ';position:relative' : '' ) + 283 '">' + 284 '<legend style="position:absolute;left:-10000px">' + 285 CKEDITOR.tools.htmlEncode( accTitle ) + 286 '</legend>' + 287 '</fieldset>' 288 , CKEDITOR.document ); 289 iframe.appendTo( fieldset ); 290 fieldset.appendTo( mainElement ); 291 } 292 293 if ( !CKEDITOR.env.ie ) 294 mainElement.append( iframe ); 295 }; 296 297 // The script that is appended to the data being loaded. It 298 // enables editing, and makes some 299 var activationScript = 300 '<script id="cke_actscrpt" type="text/javascript">' + 301 'window.onload = function()' + 302 '{' + 303 // Remove this script from the DOM. 304 'var s = document.getElementById( "cke_actscrpt" );' + 305 's.parentNode.removeChild( s );' + 306 307 // Call the temporary function for the editing 308 // boostrap. 309 'window.parent.CKEDITOR._["contentDomReady' + editor.name + '"]( window );' + 310 '}' + 311 '</script>'; 312 313 // Editing area bootstrap code. 314 var contentDomReady = function( domWindow ) 315 { 316 delete CKEDITOR._[ 'contentDomReady' + editor.name ]; 317 318 var domDocument = domWindow.document, 319 body = domDocument.body; 320 321 body.spellcheck = !editor.config.disableNativeSpellChecker; 322 323 if ( CKEDITOR.env.ie ) 324 { 325 // Don't display the focus border. 326 body.hideFocus = true; 327 328 // Disable and re-enable the body to avoid IE from 329 // taking the editing focus at startup. (#141 / #523) 330 body.disabled = true; 331 body.contentEditable = true; 332 body.removeAttribute( 'disabled' ); 333 } 334 else 335 domDocument.designMode = 'on'; 336 337 // IE, Opera and Safari may not support it and throw 338 // errors. 339 try { domDocument.execCommand( 'enableObjectResizing', false, !editor.config.disableObjectResizing ) ; } catch(e) {} 340 try { domDocument.execCommand( 'enableInlineTableEditing', false, !editor.config.disableNativeTableHandles ) ; } catch(e) {} 341 342 domWindow = editor.window = new CKEDITOR.dom.window( domWindow ); 343 domDocument = editor.document = new CKEDITOR.dom.document( domDocument ); 344 345 // Gecko/Webkit need some help when selecting control type elements. (#3448) 346 if ( !( CKEDITOR.env.ie || CKEDITOR.env.opera) ) 347 { 348 domDocument.on( 'mousedown', function( ev ) 349 { 350 var control = ev.data.getTarget(); 351 if ( control.is( 'img', 'hr', 'input', 'textarea', 'select' ) ) 352 editor.getSelection().selectElement( control ); 353 } ); 354 } 355 356 // Webkit: avoid from editing form control elements content. 357 if ( CKEDITOR.env.webkit ) 358 { 359 // Prevent from tick checkbox/radiobox/select 360 domDocument.on( 'click', function( ev ) 361 { 362 if ( ev.data.getTarget().is( 'input', 'select' ) ) 363 ev.data.preventDefault(); 364 } ); 365 366 // Prevent from editig textfield/textarea value. 367 domDocument.on( 'mouseup', function( ev ) 368 { 369 if ( ev.data.getTarget().is( 'input', 'textarea' ) ) 370 ev.data.preventDefault(); 371 } ); 372 } 373 374 var focusTarget = ( CKEDITOR.env.ie || CKEDITOR.env.safari ) ? 375 domWindow : domDocument; 376 377 focusTarget.on( 'blur', function() 378 { 379 editor.focusManager.blur(); 380 }); 381 382 focusTarget.on( 'focus', function() 383 { 384 editor.focusManager.focus(); 385 }); 386 387 var keystrokeHandler = editor.keystrokeHandler; 388 if ( keystrokeHandler ) 389 keystrokeHandler.attach( domDocument ); 390 391 // Adds the document body as a context menu target. 392 if ( editor.contextMenu ) 393 editor.contextMenu.addTarget( domDocument ); 394 395 setTimeout( function() 396 { 397 editor.fire( 'contentDom' ); 398 399 if ( fireMode ) 400 { 401 editor.mode = 'wysiwyg'; 402 editor.fire( 'mode' ); 403 fireMode = false; 404 } 405 406 isLoadingData = false; 407 408 if ( isPendingFocus ) 409 { 410 editor.focus(); 411 isPendingFocus = false; 412 } 413 414 /* 415 * IE BUG: IE might have rendered the iframe with invisible contents. 416 * (#3623). Push some inconsequential CSS style changes to force IE to 417 * refresh it. 418 * 419 * Also, for some unknown reasons, short timeouts (e.g. 100ms) do not 420 * fix the problem. :( 421 */ 422 if ( CKEDITOR.env.ie ) 423 { 424 setTimeout( function() 425 { 426 if ( editor.document ) 427 { 428 var $body = editor.document.$.body; 429 $body.runtimeStyle.marginBottom = '0px'; 430 $body.runtimeStyle.marginBottom = ''; 431 } 432 }, 1000 ); 433 } 434 }, 435 0 ); 436 }; 437 438 editor.addMode( 'wysiwyg', 439 { 440 load : function( holderElement, data, isSnapshot ) 441 { 442 mainElement = holderElement; 443 444 if ( CKEDITOR.env.ie && ( CKEDITOR.env.quirks || CKEDITOR.env.version < 8 ) ) 445 holderElement.setStyle( 'position', 'relative' ); 446 447 // Create the iframe at load for all browsers 448 // except FF and IE with custom domain. 449 if ( !isCustomDomain || !CKEDITOR.env.gecko ) 450 createIFrame(); 451 452 // The editor data "may be dirty" after this 453 // point. 454 editor.mayBeDirty = true; 455 456 fireMode = true; 457 458 if ( isSnapshot ) 459 this.loadSnapshotData( data ); 460 else 461 this.loadData( data ); 462 }, 463 464 loadData : function( data ) 465 { 466 isLoadingData = true; 467 468 // Get the HTML version of the data. 469 if ( editor.dataProcessor ) 470 { 471 data = editor.dataProcessor.toHtml( data, fixForBody ); 472 } 473 474 data = 475 editor.config.docType + 476 '<html dir="' + editor.config.contentsLangDirection + '">' + 477 '<head>' + 478 '<link href="' + editor.config.contentsCss + '" type="text/css" rel="stylesheet" _fcktemp="true"/>' + 479 '<style type="text/css" _fcktemp="true">' + 480 editor._.styles.join( '\n' ) + 481 '</style>'+ 482 '</head>' + 483 '<body>' + 484 data + 485 '</body>' + 486 '</html>' + 487 activationScript; 488 489 // For custom domain in IE, set the global variable 490 // that will temporarily hold the editor data. This 491 // reference will be used in the ifram src. 492 if ( isCustomDomain ) 493 window[ '_cke_htmlToLoad_' + editor.name ] = data; 494 495 CKEDITOR._[ 'contentDomReady' + editor.name ] = contentDomReady; 496 497 // We need to recreate the iframe in FF for every 498 // data load, otherwise the following spellcheck 499 // and execCommand features will be active only for 500 // the first time. 501 // The same is valid for IE with custom domain, 502 // because the iframe src must be reset every time. 503 if ( isCustomDomain || CKEDITOR.env.gecko ) 504 createIFrame(); 505 506 // For custom domain in IE, the data loading is 507 // done through the src attribute of the iframe. 508 if ( !isCustomDomain ) 509 { 510 var doc = iframe.$.contentWindow.document; 511 doc.open(); 512 doc.write( data ); 513 doc.close(); 514 } 515 }, 516 517 getData : function() 518 { 519 var data = iframe.getFrameDocument().getBody().getHtml(); 520 521 if ( editor.dataProcessor ) 522 data = editor.dataProcessor.toDataFormat( data, fixForBody ); 523 524 // Strip the last blank paragraph within document. 525 if ( editor.config.ignoreEmptyParagraph ) 526 data = data.replace( emptyParagraphRegexp, '' ); 527 528 return data; 529 }, 530 531 getSnapshotData : function() 532 { 533 return iframe.getFrameDocument().getBody().getHtml(); 534 }, 535 536 loadSnapshotData : function( data ) 537 { 538 iframe.getFrameDocument().getBody().setHtml( data ); 539 }, 540 541 unload : function( holderElement ) 542 { 543 editor.window = editor.document = iframe = mainElement = isPendingFocus = null; 544 545 editor.fire( 'contentDomUnload' ); 546 }, 547 548 focus : function() 549 { 550 if ( isLoadingData ) 551 isPendingFocus = true; 552 else if ( editor.window ) 553 { 554 editor.window.focus(); 555 editor.selectionChange(); 556 } 557 } 558 }); 559 560 editor.on( 'insertHtml', onInsertHtml, null, null, 20 ); 561 editor.on( 'insertElement', onInsertElement, null, null, 20 ); 562 // Auto fixing on some document structure weakness to enhance usabilities. (#3190 and #3189) 563 editor.on( 'selectionChange', onSelectionChangeFixBody, null, null, 1 ); 564 }); 565 } 566 }); 567 })(); 568 569 /** 570 * Disables the ability of resize objects (image and tables) in the editing 571 * area 572 * @type Boolean 573 * @default false 574 * @example 575 * config.disableObjectResizing = true; 576 */ 577 CKEDITOR.config.disableObjectResizing = false; 578 579 /** 580 * Disables the "table tools" offered natively by the browser (currently 581 * Firefox only) to make quick table editing operations, like adding or 582 * deleting rows and columns. 583 * @type Boolean 584 * @default true 585 * @example 586 * config.disableNativeTableHandles = false; 587 */ 588 CKEDITOR.config.disableNativeTableHandles = true; 589 590 /** 591 * Disables the built-in spell checker while typing natively available in the 592 * browser (currently Firefox and Safari only).<br /><br /> 593 * 594 * Even if word suggestions will not appear in the CKEditor context menu, this 595 * feature is useful to help quickly identifying misspelled words.<br /><br /> 596 * 597 * This setting is currently compatible with Firefox only due to limitations in 598 * other browsers. 599 * @type Boolean 600 * @default true 601 * @example 602 * config.disableNativeSpellChecker = false; 603 */ 604 CKEDITOR.config.disableNativeSpellChecker = true; 605 /** 606 * The editor will post an empty value ("") if you have just an empty paragraph on it, like this: 607 * @example 608 * <p></p> 609 * <p><br /></p> 610 * <p><b></b></p> 611 */ 612 CKEDITOR.config.ignoreEmptyParagraph = true; 613