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