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 (function()
  7 {
  8 	// #### checkSelectionChange : START
  9
 10 	// The selection change check basically saves the element parent tree of
 11 	// the current node and check it on successive requests. If there is any
 12 	// change on the tree, then the selectionChange event gets fired.
 13 	var checkSelectionChange = function()
 14 	{
 15 		// In IE, the "selectionchange" event may still get thrown when
 16 		// releasing the WYSIWYG mode, so we need to check it first.
 17 		var sel = this.getSelection();
 18 		if ( !sel )
 19 			return;
 20
 21 		var firstElement = sel.getStartElement();
 22 		var currentPath = new CKEDITOR.dom.elementPath( firstElement );
 23
 24 		if ( !currentPath.compare( this._.selectionPreviousPath ) )
 25 		{
 26 			this._.selectionPreviousPath = currentPath;
 27 			this.fire( 'selectionChange', { selection : sel, path : currentPath, element : firstElement } );
 28 		}
 29 	};
 30
 31 	var checkSelectionChangeTimer;
 32 	var checkSelectionChangeTimeoutPending;
 33 	var checkSelectionChangeTimeout = function()
 34 	{
 35 		// Firing the "OnSelectionChange" event on every key press started to
 36 		// be too slow. This function guarantees that there will be at least
 37 		// 200ms delay between selection checks.
 38
 39 		checkSelectionChangeTimeoutPending = true;
 40
 41 		if ( checkSelectionChangeTimer )
 42 			return;
 43
 44 		checkSelectionChangeTimeoutExec.call( this );
 45
 46 		checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this );
 47 	};
 48
 49 	var checkSelectionChangeTimeoutExec = function()
 50 	{
 51 		checkSelectionChangeTimer = null;
 52
 53 		if ( checkSelectionChangeTimeoutPending )
 54 		{
 55 			// Call this with a timeout so the browser properly moves the
 56 			// selection after the mouseup. It happened that the selection was
 57 			// being moved after the mouseup when clicking inside selected text
 58 			// with Firefox.
 59 			CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this );
 60
 61 			checkSelectionChangeTimeoutPending = false;
 62 		}
 63 	};
 64
 65 	// #### checkSelectionChange : END
 66
 67 	var selectAllCmd =
 68 	{
 69 		exec : function( editor )
 70 		{
 71 			switch ( editor.mode )
 72 			{
 73 				case 'wysiwyg' :
 74 					editor.document.$.execCommand( 'SelectAll', false, null );
 75 					break;
 76 				case 'source' :
 77 					// TODO
 78 			}
 79 		}
 80 	};
 81
 82 	CKEDITOR.plugins.add( 'selection',
 83 	{
 84 		init : function( editor, pluginPath )
 85 		{
 86 			editor.on( 'contentDom', function()
 87 				{
 88 					if ( CKEDITOR.env.ie )
 89 					{
 90 						// IE is the only to provide the "selectionchange"
 91 						// event.
 92 						editor.document.on( 'selectionchange', checkSelectionChangeTimeout, editor );
 93 					}
 94 					else
 95 					{
 96 						// In other browsers, we make the selection change
 97 						// check based on other events, like clicks or keys
 98 						// press.
 99
100 						editor.document.on( 'mouseup', checkSelectionChangeTimeout, editor );
101 						editor.document.on( 'keyup', checkSelectionChangeTimeout, editor );
102 					}
103 				});
104
105 			editor.addCommand( 'selectAll', selectAllCmd );
106 			editor.ui.addButton( 'SelectAll',
107 				{
108 					label : editor.lang.selectAll,
109 					command : 'selectAll'
110 				});
111 		}
112 	});
113 })();
114
115 /**
116  * Gets the current selection from the editing area when in WYSIWYG mode.
117  * @returns {CKEDITOR.dom.selection} A selection object or null if not on
118  *		WYSIWYG mode or no selection is available.
119  * @example
120  * var selection = CKEDITOR.instances.editor1.<b>getSelection()</b>;
121  * alert( selection.getType() );
122  */
123 CKEDITOR.editor.prototype.getSelection = function()
124 {
125 	var retval = this.document ? this.document.getSelection() : null;
126
127 	/**
128 	 * IE BUG: The selection's document may be a different document than the
129 	 * editor document. Return null if that's the case.
130 	 */
131 	if ( retval && CKEDITOR.env.ie )
132 	{
133 		var range = retval.getNative().createRange();
134 		if ( !range )
135 			return null;
136 		else if ( range.item )
137 			return range.item(0).ownerDocument == this.document.$ ? retval : null;
138 		else
139 			return range.parentElement().ownerDocument == this.document.$ ? retval : null;
140 	}
141 	return retval;
142 };
143
144 /**
145  * Gets the current selection from the document.
146  * @returns {CKEDITOR.dom.selection} A selection object.
147  * @example
148  * var selection = CKEDITOR.instances.editor1.document.<b>getSelection()</b>;
149  * alert( selection.getType() );
150  */
151 CKEDITOR.dom.document.prototype.getSelection = function()
152 {
153 	return new CKEDITOR.dom.selection( this );
154 };
155
156 /**
157  * No selection.
158  * @constant
159  * @example
160  * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
161  *     alert( 'Nothing is selected' );
162  */
163 CKEDITOR.SELECTION_NONE		= 1;
164
165 /**
166  * Text or collapsed selection.
167  * @constant
168  * @example
169  * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
170  *     alert( 'Text is selected' );
171  */
172 CKEDITOR.SELECTION_TEXT		= 2;
173
174 /**
175  * Element selection.
176  * @constant
177  * @example
178  * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
179  *     alert( 'An element is selected' );
180  */
181 CKEDITOR.SELECTION_ELEMENT	= 3;
182
183 /**
184  * Manipulates the selection in a DOM document.
185  * @constructor
186  * @example
187  */
188 CKEDITOR.dom.selection = function( document )
189 {
190 	this.document = document;
191 	this._ =
192 	{
193 		cache : {}
194 	};
195 };
196
197 (function()
198 {
199 	var styleObjectElements = { img:1,hr:1,li:1,table:1,tr:1,td:1,embed:1,object:1,ol:1,ul:1 };
200
201 	CKEDITOR.dom.selection.prototype =
202 	{
203 		/**
204 		 * Gets the native selection object from the browser.
205 		 * @function
206 		 * @returns {Object} The native selection object.
207 		 * @example
208 		 * var selection = editor.getSelection().<b>getNative()</b>;
209 		 */
210 		getNative :
211 			CKEDITOR.env.ie ?
212 				function()
213 				{
214 					return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.$.selection );
215 				}
216 			:
217 				function()
218 				{
219 					return this._.cache.nativeSel || ( this._.cache.nativeSel = this.document.getWindow().$.getSelection() );
220 				},
221
222 		/**
223 		 * Gets the type of the current selection. The following values are
224 		 * available:
225 		 * <ul>
226 		 *		<li>{@link CKEDITOR.SELECTION_NONE} (1): No selection.</li>
227 		 *		<li>{@link CKEDITOR.SELECTION_TEXT} (2): Text is selected or
228 		 *			collapsed selection.</li>
229 		 *		<li>{@link CKEDITOR.SELECTION_ELEMENT} (3): A element
230 		 *			selection.</li>
231 		 * </ul>
232 		 * @function
233 		 * @returns {Number} One of the following constant values:
234 		 *		{@link CKEDITOR.SELECTION_NONE}, {@link CKEDITOR.SELECTION_TEXT} or
235 		 *		{@link CKEDITOR.SELECTION_ELEMENT}.
236 		 * @example
237 		 * if ( editor.getSelection().<b>getType()</b> == CKEDITOR.SELECTION_TEXT )
238 		 *     alert( 'Text is selected' );
239 		 */
240 		getType :
241 			CKEDITOR.env.ie ?
242 				function()
243 				{
244 					if ( this._.cache.type )
245 						return this._.cache.type;
246
247 					var type = CKEDITOR.SELECTION_NONE;
248
249 					try
250 					{
251 						var sel = this.getNative(),
252 							ieType = sel.type;
253
254 						if ( ieType == 'Text' )
255 							type = CKEDITOR.SELECTION_TEXT;
256
257 						if ( ieType == 'Control' )
258 							type = CKEDITOR.SELECTION_ELEMENT;
259
260 						// It is possible that we can still get a text range
261 						// object even when type == 'None' is returned by IE.
262 						// So we'd better check the object returned by
263 						// createRange() rather than by looking at the type.
264 						if ( sel.createRange().parentElement )
265 							type = CKEDITOR.SELECTION_TEXT;
266 					}
267 					catch(e) {}
268
269 					return ( this._.cache.type = type );
270 				}
271 			:
272 				function()
273 				{
274 					if ( this._.cache.type )
275 						return this._.cache.type;
276
277 					var type = CKEDITOR.SELECTION_TEXT;
278
279 					var sel = this.getNative();
280
281 					if ( !sel )
282 						type = CKEDITOR.SELECTION_NONE;
283 					else if ( sel.rangeCount == 1 )
284 					{
285 						// Check if the actual selection is a control (IMG,
286 						// TABLE, HR, etc...).
287
288 						var range = sel.getRangeAt(0),
289 							startContainer = range.startContainer;
290
291 						if ( startContainer == range.endContainer
292 							&& startContainer.nodeType == 1
293 							&& ( range.endOffset - range.startOffset ) == 1
294 							&& styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] )
295 						{
296 							type = CKEDITOR.SELECTION_ELEMENT;
297 						}
298 					}
299
300 					return ( this._.cache.type = type );
301 				},
302
303 		getRanges :
304 			CKEDITOR.env.ie ?
305 				( function()
306 				{
307 					// Finds the container and offset for a specific boundary
308 					// of an IE range.
309 					var getBoundaryInformation = function( range, start )
310 					{
311 						// Creates a collapsed range at the requested boundary.
312 						range = range.duplicate();
313 						range.collapse( start );
314
315 						// Gets the element that encloses the range entirely.
316 						var parent = range.parentElement();
317 						var siblings = parent.childNodes;
318
319 						var testRange;
320
321 						for ( var i = 0 ; i < siblings.length ; i++ )
322 						{
323 							var child = siblings[ i ];
324 							if ( child.nodeType == 1 )
325 							{
326 								testRange = range.duplicate();
327
328 								testRange.moveToElementText( child );
329 								testRange.collapse();
330
331 								var comparison = testRange.compareEndPoints( 'StartToStart', range );
332
333 								if ( comparison > 0 )
334 									break;
335 								else if ( comparison === 0 )
336 									return {
337 										container : parent,
338 										offset : i
339 									};
340
341 								testRange = null;
342 							}
343 						}
344
345 						if ( !testRange )
346 						{
347 							testRange = range.duplicate();
348 							testRange.moveToElementText( parent );
349 							testRange.collapse( false );
350 						}
351
352 						testRange.setEndPoint( 'StartToStart', range );
353 						var distance = testRange.text.length;
354
355 						while ( distance > 0 )
356 							distance -= siblings[ --i ].nodeValue.length;
357
358 						if ( distance === 0 )
359 						{
360 							return {
361 								container : parent,
362 								offset : i
363 							};
364 						}
365 						else
366 						{
367 							return {
368 								container : siblings[ i ],
369 								offset : -distance
370 							};
371 						}
372 					};
373
374 					return function()
375 					{
376 						if ( this._.cache.ranges )
377 							return this._.cache.ranges;
378
379 						// IE doesn't have range support (in the W3C way), so we
380 						// need to do some magic to transform selections into
381 						// CKEDITOR.dom.range instances.
382
383 						var sel = this.getNative(),
384 							nativeRange = sel.createRange(),
385 							type = this.getType(),
386 							range;
387
388 						if ( type == CKEDITOR.SELECTION_TEXT )
389 						{
390 							range = new CKEDITOR.dom.range( this.document );
391
392 							var boundaryInfo = getBoundaryInformation( nativeRange, true );
393 							range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
394
395 							boundaryInfo = getBoundaryInformation( nativeRange );
396 							range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
397
398 							return ( this._.cache.ranges = [ range ] );
399 						}
400 						else if ( type == CKEDITOR.SELECTION_ELEMENT )
401 						{
402 							var retval = this._.cache.ranges = [];
403
404 							for ( var i = 0 ; i < nativeRange.length ; i++ )
405 							{
406 								var element = nativeRange.item( i ),
407 									parentElement = element.parentNode,
408 									j = 0;
409
410 								range = new CKEDITOR.dom.range( this.document );
411
412 								for (; j < parentElement.childNodes.length && parentElement.childNodes[j] != element ; j++ )
413 								{ /*jsl:pass*/ }
414
415 								range.setStart( new CKEDITOR.dom.node( parentElement ), j );
416 								range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
417 								retval.push( range );
418 							}
419
420 							return retval;
421 						}
422
423 						return ( this._.cache.ranges = [] );
424 					};
425 				})()
426 			:
427 				function()
428 				{
429 					if ( this._.cache.ranges )
430 						return this._.cache.ranges;
431
432 					// On browsers implementing the W3C range, we simply
433 					// tranform the native ranges in CKEDITOR.dom.range
434 					// instances.
435
436 					var ranges = [];
437 					var sel = this.getNative();
438
439 					for ( var i = 0 ; i < sel.rangeCount ; i++ )
440 					{
441 						var nativeRange = sel.getRangeAt( i );
442 						var range = new CKEDITOR.dom.range( this.document );
443
444 						range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
445 						range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
446 						ranges.push( range );
447 					}
448
449 					return ( this._.cache.ranges = ranges );
450 				},
451
452 		/**
453 		 * Gets the DOM element in which the selection starts.
454 		 * @returns {CKEDITOR.dom.element} The element at the beginning of the
455 		 *		selection.
456 		 * @example
457 		 * var element = editor.getSelection().<b>getStartElement()</b>;
458 		 * alert( element.getName() );
459 		 */
460 		getStartElement : function()
461 		{
462 			var node,
463 				sel = this.getNative();
464
465 			switch ( this.getType() )
466 			{
467 				case CKEDITOR.SELECTION_ELEMENT :
468 					return this.getSelectedElement();
469
470 				case CKEDITOR.SELECTION_TEXT :
471
472 					var range = this.getRanges()[0];
473
474 					if ( range )
475 					{
476 						if ( !range.collapsed )
477 						{
478 							range.optimize();
479
480 							node = range.startContainer;
481
482 							if ( node.type != CKEDITOR.NODE_ELEMENT )
483 								return node.getParent();
484
485 							node = node.getChild( range.startOffset );
486
487 							if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
488 								return range.startContainer;
489
490 							var child = node.getFirst();
491 							while (  child && child.type == CKEDITOR.NODE_ELEMENT )
492 							{
493 								node = child;
494 								child = child.getFirst();
495 							}
496
497 							return node;
498 						}
499 					}
500
501 					if ( CKEDITOR.env.ie )
502 					{
503 						range = sel.createRange();
504 						range.collapse( true );
505
506 						node = range.parentElement();
507 					}
508 					else
509 					{
510 						node = sel.anchorNode;
511
512 						if ( node.nodeType != 1 )
513 							node = node.parentNode;
514 					}
515 			}
516
517 			return ( node ? new CKEDITOR.dom.element( node ) : null );
518 		},
519
520 		/**
521 		 * Gets the current selected element.
522 		 * @returns {CKEDITOR.dom.element} The selected element. Null if no
523 		 *		selection is available or the selection type is not
524 		 *		{@link CKEDITOR.SELECTION_ELEMENT}.
525 		 * @example
526 		 * var element = editor.getSelection().<b>getSelectedElement()</b>;
527 		 * alert( element.getName() );
528 		 */
529 		getSelectedElement : function()
530 		{
531 			var node;
532
533 			if ( this.getType() == CKEDITOR.SELECTION_ELEMENT )
534 			{
535 				var sel = this.getNative();
536
537 				if ( CKEDITOR.env.ie )
538 				{
539 					try
540 					{
541 						node = sel.createRange().item(0);
542 					}
543 					catch(e) {}
544 				}
545 				else
546 				{
547 					var range = sel.getRangeAt( 0 );
548 					node = range.startContainer.childNodes[ range.startOffset ];
549 				}
550 			}
551
552 			return ( node ? new CKEDITOR.dom.element( node ) : null );
553 		},
554
555 		reset : function()
556 		{
557 			this._.cache = {};
558 		},
559
560 		selectElement :
561 			CKEDITOR.env.ie ?
562 				function( element )
563 				{
564 					this.getNative().empty();
565
566 					var range;
567 					try
568 					{
569 						// Try to select the node as a control.
570 						range = this.document.$.body.createControlRange();
571 						range.addElement( element.$ );
572 					}
573 					catch(e)
574 					{
575 						// If failed, select it as a text range.
576 						range = this.document.$.body.createTextRange();
577 						range.moveToElementText( element.$ );
578 					}
579
580 					range.select();
581 				}
582 			:
583 				function( element )
584 				{
585 					// Create the range for the element.
586 					var range = this.document.$.createRange();
587 					range.selectNode( element.$ );
588
589 					// Select the range.
590 					var sel = this.getNative();
591 					sel.removeAllRanges();
592 					sel.addRange( range );
593 				},
594
595 		selectRanges :
596 			CKEDITOR.env.ie ?
597 				function( ranges )
598 				{
599 					// IE doesn't accept multiple ranges selection, so we just
600 					// select the first one.
601 					if ( ranges[ 0 ] )
602 						ranges[ 0 ].select();
603 				}
604 			:
605 				function( ranges )
606 				{
607 					var sel = this.getNative();
608 					sel.removeAllRanges();
609
610 					for ( var i = 0 ; i < ranges.length ; i++ )
611 					{
612 						var range = ranges[ i ];
613 						var nativeRange = this.document.$.createRange();
614 						nativeRange.setStart( range.startContainer.$, range.startOffset );
615 						nativeRange.setEnd( range.endContainer.$, range.endOffset );
616
617 						// Select the range.
618 						sel.addRange( nativeRange );
619 					}
620 				},
621
622 		createBookmarks : function()
623 		{
624 			var retval = [],
625 				ranges = this.getRanges();
626 			for ( var i = 0 ; i < ranges.length ; i++ )
627 				retval.push( ranges[i].createBookmark() );
628 			return retval;
629 		},
630
631 		selectBookmarks : function( bookmarks )
632 		{
633 			var ranges = [];
634 			for ( var i = 0 ; i < bookmarks.length ; i++ )
635 			{
636 				var range = new CKEDITOR.dom.range( this.document );
637 				range.moveToBookmark( bookmarks[i] );
638 				ranges.push( range );
639 			}
640 			this.selectRanges( ranges );
641 			return this;
642 		}
643 	};
644 })();
645
646 CKEDITOR.dom.range.prototype.select =
647 	CKEDITOR.env.ie ?
648 		// V2
649 		function()
650 		{
651 			var collapsed = this.collapsed;
652 			var isStartMakerAlone;
653 			var dummySpan;
654
655 			var bookmark = this.createBookmark();
656
657 			// Create marker tags for the start and end boundaries.
658 			var startNode = bookmark.startNode;
659
660 			var endNode;
661 			if ( !collapsed )
662 				endNode = bookmark.endNode;
663
664 			// Create the main range which will be used for the selection.
665 			var ieRange = this.document.$.body.createTextRange();
666
667 			// Position the range at the start boundary.
668 			ieRange.moveToElementText( startNode.$ );
669 			ieRange.moveStart( 'character', 1 );
670
671 			if ( endNode )
672 			{
673 				// Create a tool range for the end.
674 				var ieRangeEnd = this.document.$.body.createTextRange();
675
676 				// Position the tool range at the end.
677 				ieRangeEnd.moveToElementText( endNode.$ );
678
679 				// Move the end boundary of the main range to match the tool range.
680 				ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
681 				ieRange.moveEnd( 'character', -1 );
682 			}
683 			else
684 			{
685 				isStartMakerAlone = ( !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) )
686 					&& !startNode.hasNext();
687
688 				// Append a temporary <span></span> before the selection.
689 				// This is needed to avoid IE destroying selections inside empty
690 				// inline elements, like <b></b> (#253).
691 				// It is also needed when placing the selection right after an inline
692 				// element to avoid the selection moving inside of it.
693 				dummySpan = this.document.createElement( 'span' );
694 				dummySpan.setHtml( '' );	// Zero Width No-Break Space (U+FEFF). See #1359.
695 				dummySpan.insertBefore( startNode );
696
697 				if ( isStartMakerAlone )
698 				{
699 					// To expand empty blocks or line spaces after <br>, we need
700 					// instead to have any char, which will be later deleted using the
701 					// selection.
702 					// \ufeff = Zero Width No-Break Space (U+FEFF). See #1359.
703 					this.document.createText( '\ufeff' ).insertBefore( startNode );
704 				}
705 			}
706
707 			// Remove the markers (reset the position, because of the changes in the DOM tree).
708 			this.setStartBefore( startNode );
709 			startNode.remove();
710
711 			if ( collapsed )
712 			{
713 				if ( isStartMakerAlone )
714 				{
715 					// Move the selection start to include the temporary .
716 					//ieRange.moveStart( 'character', -1 );
717
718 					ieRange.select();
719
720 					// Remove our temporary stuff.
721 //					this.document.$.selection.clear();
722 				}
723 				else
724 					ieRange.select();
725
726 				dummySpan.remove();
727 			}
728 			else
729 			{
730 				this.setEndBefore( endNode );
731 				endNode.remove();
732 				ieRange.select();
733 			}
734 		}
735 	:
736 		function()
737 		{
738 			var startContainer = this.startContainer;
739
740 			// If we have a collapsed range, inside an empty element, we must add
741 			// something to it, otherwise the caret will not be visible.
742 			if ( this.collapsed && startContainer.type == CKEDITOR.NODE_ELEMENT && !startContainer.getChildCount() )
743 				startContainer.append( new CKEDITOR.dom.text( '' ) );
744
745 			var nativeRange = this.document.$.createRange();
746 			nativeRange.setStart( startContainer.$, this.startOffset );
747
748 			try
749 			{
750 				nativeRange.setEnd( this.endContainer.$, this.endOffset );
751 			}
752 			catch ( e )
753 			{
754 				// There is a bug in Firefox implementation (it would be too easy
755 				// otherwise). The new start can't be after the end (W3C says it can).
756 				// So, let's create a new range and collapse it to the desired point.
757 				if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 )
758 				{
759 					this.collapse( true );
760 					nativeRange.setEnd( this.endContainer.$, this.endOffset );
761 				}
762 				else
763 					throw( e );
764 			}
765
766 			var selection = this.document.getSelection().getNative();
767 			selection.removeAllRanges();
768 			selection.addRange( nativeRange );
769 		};
770