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 CKEDITOR.dom.range = function( document )
  7 {
  8 	this.startContainer	= null;
  9 	this.startOffset	= null;
 10 	this.endContainer	= null;
 11 	this.endOffset		= null;
 12 	this.collapsed		= true;
 13
 14 	this.document = document;
 15 };
 16
 17 (function()
 18 {
 19 	// Updates the "collapsed" property for the given range object.
 20 	var updateCollapsed = function( range )
 21 	{
 22 		range.collapsed = (
 23 			range.startContainer &&
 24 			range.endContainer &&
 25 			range.startContainer.equals( range.endContainer ) &&
 26 			range.startOffset == range.endOffset );
 27 	};
 28
 29 	// This is a shared function used to delete, extract and clone the range
 30 	// contents.
 31 	// V2
 32 	var execContentsAction = function( range, action, docFrag )
 33 	{
 34 		var startNode	= range.startContainer;
 35 		var endNode		= range.endContainer;
 36
 37 		var startOffset	= range.startOffset;
 38 		var endOffset	= range.endOffset;
 39
 40 		var removeStartNode;
 41 		var removeEndNode;
 42
 43 		// For text containers, we must simply split the node and point to the
 44 		// second part. The removal will be handled by the rest of the code .
 45 		if ( endNode.type == CKEDITOR.NODE_TEXT )
 46 			endNode = endNode.split( endOffset );
 47 		else
 48 		{
 49 			// If the end container has children and the offset is pointing
 50 			// to a child, then we should start from it.
 51 			if ( endNode.getChildCount() > 0 )
 52 			{
 53 				// If the offset points after the last node.
 54 				if ( endOffset >= endNode.getChildCount() )
 55 				{
 56 					// Let's create a temporary node and mark it for removal.
 57 					endNode = endNode.append( range.document.createText( '' ) );
 58 					removeEndNode = true;
 59 				}
 60 				else
 61 					endNode = endNode.getChild( endOffset );
 62 			}
 63 		}
 64
 65 		// For text containers, we must simply split the node. The removal will
 66 		// be handled by the rest of the code .
 67 		if ( startNode.type == CKEDITOR.NODE_TEXT )
 68 		{
 69 			startNode.split( startOffset );
 70
 71 			// In cases the end node is the same as the start node, the above
 72 			// splitting will also split the end, so me must move the end to
 73 			// the second part of the split.
 74 			if ( startNode.equals( endNode ) )
 75 				endNode = startNode.getNext();
 76 		}
 77 		else
 78 		{
 79 			// If the start container has children and the offset is pointing
 80 			// to a child, then we should start from its previous sibling.
 81
 82 			// If the offset points to the first node, we don't have a
 83 			// sibling, so let's use the first one, but mark it for removal.
 84 			if ( !startOffset )
 85 			{
 86 				// Let's create a temporary node and mark it for removal.
 87 				startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) );
 88 				removeStartNode = true;
 89 			}
 90 			else if ( startOffset >= startNode.getChildCount() )
 91 			{
 92 				// Let's create a temporary node and mark it for removal.
 93 				startNode = startNode.append( range.document.createText( '' ) );
 94 				removeStartNode = true;
 95 			}
 96 			else
 97 				startNode = startNode.getChild( startOffset ).getPrevious();
 98 		}
 99
100 		// Get the parent nodes tree for the start and end boundaries.
101 		var startParents	= startNode.getParents();
102 		var endParents		= endNode.getParents();
103
104 		// Compare them, to find the top most siblings.
105 		var i, topStart, topEnd;
106
107 		for ( i = 0 ; i < startParents.length ; i++ )
108 		{
109 			topStart = startParents[ i ];
110 			topEnd = endParents[ i ];
111
112 			// The compared nodes will match until we find the top most
113 			// siblings (different nodes that have the same parent).
114 			// "i" will hold the index in the parents array for the top
115 			// most element.
116 			if ( !topStart.equals( topEnd ) )
117 				break;
118 		}
119
120 		var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling;
121
122 		// Remove all successive sibling nodes for every node in the
123 		// startParents tree.
124 		for ( var j = i ; j < startParents.length ; j++ )
125 		{
126 			levelStartNode = startParents[j];
127
128 			// For Extract and Clone, we must clone this level.
129 			if ( clone && !levelStartNode.equals( startNode ) )		// action = 0 = Delete
130 				levelClone = clone.append( levelStartNode.clone() );
131
132 			currentNode = levelStartNode.getNext();
133
134 			while( currentNode )
135 			{
136 				// Stop processing when the current node matches a node in the
137 				// endParents tree or if it is the endNode.
138 				if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )
139 					break;
140
141 				// Cache the next sibling.
142 				currentSibling = currentNode.getNext();
143
144 				// If cloning, just clone it.
145 				if ( action == 2 )	// 2 = Clone
146 					clone.append( currentNode.clone( true ) );
147 				else
148 				{
149 					// Both Delete and Extract will remove the node.
150 					currentNode.remove();
151
152 					// When Extracting, move the removed node to the docFrag.
153 					if ( action == 1 )	// 1 = Extract
154 						clone.append( currentNode );
155 				}
156
157 				currentNode = currentSibling;
158 			}
159
160 			if ( clone )
161 				clone = levelClone;
162 		}
163
164 		clone = docFrag;
165
166 		// Remove all previous sibling nodes for every node in the
167 		// endParents tree.
168 		for ( var k = i ; k < endParents.length ; k++ )
169 		{
170 			levelStartNode = endParents[ k ];
171
172 			// For Extract and Clone, we must clone this level.
173 			if ( action > 0 && !levelStartNode.equals( endNode ) )		// action = 0 = Delete
174 				levelClone = clone.append( levelStartNode.clone() );
175
176 			// The processing of siblings may have already been done by the parent.
177 			if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode )
178 			{
179 				currentNode = levelStartNode.getPrevious();
180
181 				while( currentNode )
182 				{
183 					// Stop processing when the current node matches a node in the
184 					// startParents tree or if it is the startNode.
185 					if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )
186 						break;
187
188 					// Cache the next sibling.
189 					currentSibling = currentNode.getPrevious();
190
191 					// If cloning, just clone it.
192 					if ( action == 2 )	// 2 = Clone
193 						clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ;
194 					else
195 					{
196 						// Both Delete and Extract will remove the node.
197 						currentNode.remove();
198
199 						// When Extracting, mode the removed node to the docFrag.
200 						if ( action == 1 )	// 1 = Extract
201 							clone.$.insertBefore( currentNode.$, clone.$.firstChild );
202 					}
203
204 					currentNode = currentSibling;
205 				}
206 			}
207
208 			if ( clone )
209 				clone = levelClone;
210 		}
211
212 		if ( action == 2 )		// 2 = Clone.
213 		{
214 			// No changes in the DOM should be done, so fix the split text (if any).
215
216 			var startTextNode = range.startContainer;
217 			if ( startTextNode.type == CKEDITOR.NODE_TEXT )
218 			{
219 				startTextNode.$.data += startTextNode.$.nextSibling.data;
220 				startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
221 			}
222
223 			var endTextNode = range.endContainer;
224 			if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling )
225 			{
226 				endTextNode.$.data += endTextNode.$.nextSibling.data;
227 				endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
228 			}
229 		}
230 		else
231 		{
232 			// Collapse the range.
233
234 			// If a node has been partially selected, collapse the range between
235 			// topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
236 			if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) )
237 			{
238 				var endIndex = topEnd.getIndex();
239
240 				// If the start node is to be removed, we must correct the
241 				// index to reflect the removal.
242 				if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )
243 					endIndex--;
244
245 				range.setStart( topEnd.getParent(), endIndex );
246 			}
247
248 			// Collapse it to the start.
249 			range.collapse( true );
250 		}
251
252 		// Cleanup any marked node.
253 		if( removeStartNode )
254 			startNode.remove();
255
256 		if( removeEndNode && endNode.$.parentNode )
257 			endNode.remove();
258 	};
259
260 	var inlineChildReqElements = { abbr:1,acronym:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,'var':1 };
261
262 	var getBoundaryNodes = function()
263 	{
264 		var startNode = this.startContainer,
265 			endNode = this.endContainer,
266 			startOffset = this.startOffset,
267 			endOffset = this.endOffset,
268 			childCount;
269
270 		if ( startNode.type == CKEDITOR.NODE_ELEMENT )
271 		{
272 			childCount = startNode.getChildCount();
273 			if ( childCount > startOffset )
274 				startNode = startNode.getChild( startOffset );
275 			else if ( childCount < 1 )
276 				startNode = startNode.getPreviousSourceNode();
277 			else		// startOffset > childCount but childCount is not 0
278 			{
279 				// Try to take the node just after the current position.
280 				startNode = startNode.$;
281 				while ( startNode.lastChild )
282 					startNode = startNode.lastChild;
283 				startNode = new CKEDITOR.dom.node( startNode );
284
285 				// Normally we should take the next node in DFS order. But it
286 				// is also possible that we've already reached the end of
287 				// document.
288 				startNode = startNode.getNextSourceNode() || startNode;
289 			}
290 		}
291 		if ( endNode.type == CKEDITOR.NODE_ELEMENT )
292 		{
293 			childCount = endNode.getChildCount();
294 			if ( childCount > endOffset )
295 				endNode = endNode.getChild( endOffset ).getPreviousSourceNode();
296 			else if ( childCount < 1 )
297 				endNode = endNode.getPreviousSourceNode();
298 			else		// endOffset > childCount but childCount is not 0
299 			{
300 				// Try to take the node just before the current position.
301 				endNode = endNode.$;
302 				while ( endNode.lastChild )
303 					endNode = endNode.lastChild;
304 				endNode = new CKEDITOR.dom.node( endNode );
305 			}
306 		}
307
308 		return { startNode : startNode, endNode : endNode };
309 	};
310
311 	// Check every node between the block boundary and the startNode or endNode.
312 	var getCheckStartEndBlockFunction = function( isStart )
313 	{
314 		return function( evt )
315 		{
316 			// Don't check the block boundary itself.
317 			if ( this.stopped() || !evt.data.node )
318 				return;
319
320 			var node = evt.data.node,
321 				hadBr = false;
322 			if ( node.type == CKEDITOR.NODE_ELEMENT )
323 			{
324 				// If there are non-empty inline elements (e.g. <img />), then we're not
325 				// at the start.
326 				if ( !inlineChildReqElements[ node.getName() ] )
327 				{
328 					// If we're working at the end-of-block, forgive the first <br />.
329 					if ( !isStart && node.getName() == 'br' && !hadBr )
330 						hadBr = true;
331 					else
332 					{
333 						this.checkFailed = true;
334 						this.stop();
335 					}
336 				}
337 			}
338 			else if ( node.type == CKEDITOR.NODE_TEXT )
339 			{
340 				// If there's any visible text, then we're not at the start.
341 				var visibleText = CKEDITOR.tools.trim( node.getText() );
342 				if ( visibleText.length > 0 )
343 				{
344 					this.checkFailed = true;
345 					this.stop();
346 				}
347 			}
348 		};
349 	};
350
351
352 	CKEDITOR.dom.range.prototype =
353 	{
354 		clone : function()
355 		{
356 			var clone = new CKEDITOR.dom.range( this.document );
357
358 			clone.startContainer = this.startContainer;
359 			clone.startOffset = this.startOffset;
360 			clone.endContainer = this.endContainer;
361 			clone.endOffset = this.endOffset;
362 			clone.collapsed = this.collapsed;
363
364 			return clone;
365 		},
366
367 		collapse : function( toStart )
368 		{
369 			if ( toStart )
370 			{
371 				this.endContainer	= this.startContainer;
372 				this.endOffset		= this.startOffset;
373 			}
374 			else
375 			{
376 				this.startContainer	= this.endContainer;
377 				this.startOffset	= this.endOffset;
378 			}
379
380 			this.collapsed = true;
381 		},
382
383 		// The selection may be lost when cloning (due to the splitText() call).
384 		cloneContents : function()
385 		{
386 			var docFrag = new CKEDITOR.dom.documentFragment( this.document );
387
388 			if ( !this.collapsed )
389 				execContentsAction( this, 2, docFrag );
390
391 			return docFrag;
392 		},
393
394 		deleteContents : function()
395 		{
396 			if ( this.collapsed )
397 				return;
398
399 			execContentsAction( this, 0 );
400 		},
401
402 		extractContents : function()
403 		{
404 			var docFrag = new CKEDITOR.dom.documentFragment( this.document );
405
406 			if ( !this.collapsed )
407 				execContentsAction( this, 1, docFrag );
408
409 			return docFrag;
410 		},
411
412 		// This is an "intrusive" way to create a bookmark. It includes <span> tags
413 		// in the range boundaries. The advantage of it is that it is possible to
414 		// handle DOM mutations when moving back to the bookmark.
415 		// Attention: the inclusion of nodes in the DOM is a design choice and
416 		// should not be changed as there are other points in the code that may be
417 		// using those nodes to perform operations. See GetBookmarkNode.
418 		createBookmark : function()
419 		{
420 			var startNode, endNode;
421 			var clone;
422
423 			startNode = this.document.createElement( 'span' );
424 			startNode.setAttribute( '_fck_bookmark', 1 );
425 			startNode.setStyle( 'display', 'none' );
426
427 			// For IE, it must have something inside, otherwise it may be
428 			// removed during DOM operations.
429 			startNode.setHtml( ' ' );
430
431 			// If collapsed, the endNode will not be created.
432 			if ( !this.collapsed )
433 			{
434 				endNode = startNode.clone();
435 				endNode.setHtml( ' ' );
436
437 				clone = this.clone();
438 				clone.collapse();
439 				clone.insertNode( endNode );
440 			}
441
442 			clone = this.clone();
443 			clone.collapse( true );
444 			clone.insertNode( startNode );
445
446 			// Update the range position.
447 			if ( endNode )
448 			{
449 				this.setStartAfter( startNode );
450 				this.setEndBefore( endNode );
451 			}
452 			else
453 				this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
454
455 			return {
456 				startNode : startNode,
457 				endNode : endNode
458 			};
459 		},
460
461 		moveToBookmark : function( bookmark )
462 		{
463 			// Set the range start at the bookmark start node position.
464 			this.setStartBefore( bookmark.startNode );
465
466 			// Remove it, because it may interfere in the setEndBefore call.
467 			bookmark.startNode.remove();
468
469 			// Set the range end at the bookmark end node position, or simply
470 			// collapse it if it is not available.
471 			var endNode = bookmark.endNode;
472 			if ( endNode )
473 			{
474 				this.setEndBefore( endNode );
475 				endNode.remove();
476 			}
477 			else
478 				this.collapse( true );
479 		},
480
481 		getCommonAncestor : function( includeSelf )
482 		{
483 			var start = this.startContainer;
484 			var end = this.endContainer;
485
486 			if ( start.equals( end ) )
487 			{
488 				if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
489 					return start.getChild( this.startOffset );
490 				return start;
491 			}
492
493 			if ( end.type == CKEDITOR.NODE_ELEMENT && end.contains( start ) )
494 				return end;
495
496 			if ( start.type != CKEDITOR.NODE_ELEMENT )
497 				start = start.getParent();
498
499 			do
500 			{
501 				if ( start.contains( end ) )
502 					return start;
503 			}
504 			while( ( start = start.getParent() ) )
505
506 			return null;
507 		},
508
509 		/**
510 		 * Transforms the startContainer and endContainer properties from text
511 		 * nodes to element nodes, whenever possible. This is actually possible
512 		 * if either of the boundary containers point to a text node, and its
513 		 * offset is set to zero, or after the last char in the node.
514 		 */
515 		optimize : function()
516 		{
517 			var container = this.startContainer;
518 			var offset = this.startOffset;
519
520 			if ( container.type != CKEDITOR.NODE_ELEMENT )
521 			{
522 				if ( !offset )
523 					this.setStartBefore( container );
524 				else if ( offset >= container.getLength() )
525 					this.setStartAfter( container );
526 			}
527
528 			container = this.endContainer;
529 			offset = this.endOffset;
530
531 			if ( container.type != CKEDITOR.NODE_ELEMENT )
532 			{
533 				if ( !offset )
534 					this.setEndBefore( container );
535 				else if ( offset >= container.getLength() )
536 					this.setEndAfter( container );
537 			}
538 		},
539
540 		trim : function( ignoreStart, ignoreEnd )
541 		{
542 			var startContainer = this.startContainer;
543 			var startOffset = this.startOffset;
544
545 			var endContainer = this.endContainer;
546 			var endOffset = this.endOffset;
547
548 			if ( !ignoreStart && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )
549 			{
550 				// If the offset is zero, we just insert the new node before
551 				// the start.
552 				if ( !startOffset )
553 				{
554 					startOffset = startContainer.getIndex();
555 					startContainer = startContainer.getParent();
556 				}
557 				// If the offset is at the end, we'll insert it after the text
558 				// node.
559 				else if ( startOffset >= startContainer.getLength() )
560 				{
561 					startOffset = startContainer.getIndex() + 1;
562 					startContainer = startContainer.getParent();
563 				}
564 				// In other case, we split the text node and insert the new
565 				// node at the split point.
566 				else
567 				{
568 					var nextText = startContainer.split( startOffset );
569
570 					startOffset = startContainer.getIndex() + 1;
571 					startContainer = startContainer.getParent();
572
573 					// Check if it is necessary to update the end boundary.
574 					if ( this.collapsed )
575 						this.setEnd( startContainer, startOffset );
576 					else if ( this.startContainer.equals( this.endContainer ) )
577 						this.setEnd( nextText, this.endOffset - this.startOffset );
578 				}
579
580 				this.setStart( startContainer, startOffset );
581 			}
582
583 			if ( !ignoreEnd && endContainer && !this.collapsed && endContainer.type == CKEDITOR.NODE_TEXT )
584 			{
585 				// If the offset is zero, we just insert the new node before
586 				// the start.
587 				if ( !endOffset )
588 				{
589 					endOffset = endContainer.getIndex();
590 					endContainer = endContainer.getParent();
591 				}
592 				// If the offset is at the end, we'll insert it after the text
593 				// node.
594 				else if ( endOffset >= endContainer.getLength() )
595 				{
596 					endOffset = endContainer.getIndex() + 1;
597 					endContainer = endContainer.getParent();
598 				}
599 				// In other case, we split the text node and insert the new
600 				// node at the split point.
601 				else
602 				{
603 					endContainer.split( endOffset );
604
605 					endOffset = endContainer.getIndex() + 1;
606 					endContainer = endContainer.getParent();
607 				}
608
609 				this.setEnd( endContainer, endOffset );
610 			}
611 		},
612
613 		enlarge : function( unit )
614 		{
615 			switch ( unit )
616 			{
617 				case CKEDITOR.ENLARGE_ELEMENT :
618
619 					if ( this.collapsed )
620 						return;
621
622 					// Get the common ancestor.
623 					var commonAncestor = this.getCommonAncestor();
624
625 					var body = this.document.getBody();
626
627 					// For each boundary
628 					//		a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
629 					//		b. Go ahead checking siblings and enlarging the boundary as much as possible until the common ancestor is not reached. After reaching the common ancestor, just save the enlargeable node to be used later.
630
631 					var startTop, endTop;
632
633 					var enlargeable, sibling, commonReached;
634
635 					// Indicates that the node can be added only if whitespace
636 					// is available before it.
637 					var needsWhiteSpace = false;
638 					var isWhiteSpace;
639 					var siblingText;
640
641 					// Process the start boundary.
642
643 					var container = this.startContainer;
644 					var offset = this.startOffset;
645
646 					if ( container.type == CKEDITOR.NODE_TEXT )
647 					{
648 						if ( offset )
649 						{
650 							// Check if there is any non-space text before the
651 							// offset. Otherwise, container is null.
652 							container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
653
654 							// If we found only whitespace in the node, it
655 							// means that we'll need more whitespace to be able
656 							// to expand. For example, <i> can be expanded in
657 							// "A <i> [B]</i>", but not in "A<i> [B]</i>".
658 							needsWhiteSpace = !!container;
659 						}
660
661 						if ( container )
662 						{
663 							if ( !( sibling = container.getPrevious() ) )
664 								enlargeable = container.getParent();
665 						}
666 					}
667 					else
668 					{
669 						// If we have offset, get the node preceeding it as the
670 						// first sibling to be checked.
671 						if ( offset )
672 							sibling = container.getChild( offset - 1 ) || container.getLast();
673
674 						// If there is no sibling, mark the container to be
675 						// enlarged.
676 						if ( !sibling )
677 							enlargeable = container;
678 					}
679
680 					while ( enlargeable || sibling )
681 					{
682 						if ( enlargeable && !sibling )
683 						{
684 							// If we reached the common ancestor, mark the flag
685 							// for it.
686 							if ( !commonReached && enlargeable.equals( commonAncestor ) )
687 								commonReached = true;
688
689 							if ( !body.contains( enlargeable ) )
690 								break;
691
692 							// If we don't need space or this element breaks
693 							// the line, then enlarge it.
694 							if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
695 							{
696 								needsWhiteSpace = false;
697
698 								// If the common ancestor has been reached,
699 								// we'll not enlarge it immediately, but just
700 								// mark it to be enlarged later if the end
701 								// boundary also enlarges it.
702 								if ( commonReached )
703 									startTop = enlargeable;
704 								else
705 									this.setStartBefore( enlargeable );
706 							}
707
708 							sibling = enlargeable.getPrevious();
709 						}
710
711 						// Check all sibling nodes preceeding the enlargeable
712 						// node. The node wil lbe enlarged only if none of them
713 						// blocks it.
714 						while ( sibling )
715 						{
716 							// This flag indicates that this node has
717 							// whitespaces at the end.
718 							isWhiteSpace = false;
719
720 							if ( sibling.type == CKEDITOR.NODE_TEXT )
721 							{
722 								siblingText = sibling.getText();
723
724 								if ( /[^\s\ufeff]/.test( siblingText ) )
725 									sibling = null;
726
727 								isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
728 							}
729 							else
730 							{
731 								// If this is a visible element.
732 								if ( sibling.$.offsetWidth > 0 )
733 								{
734 									// We'll accept it only if we need
735 									// whitespace, and this is an inline
736 									// element with whitespace only.
737 									if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
738 									{
739 										// It must contains spaces and inline elements only.
740
741 										siblingText = sibling.getText();
742
743 										if ( !(/[^\s\ufeff]/).test( siblingText ) )	// Spaces + Zero Width No-Break Space (U+FEFF)
744 											sibling = null;
745 										else
746 										{
747 											var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
748 											for ( var i = 0, child ; child = allChildren[ i++ ] ; )
749 											{
750 												if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
751 												{
752 													sibling = null;
753 													break;
754 												}
755 											}
756 										}
757
758 										if ( sibling )
759 											isWhiteSpace = !!siblingText.length;
760 									}
761 									else
762 										sibling = null;
763 								}
764 							}
765
766 							// A node with whitespaces has been found.
767 							if ( isWhiteSpace )
768 							{
769 								// Enlarge the last enlargeable node, if we
770 								// were waiting for spaces.
771 								if ( needsWhiteSpace )
772 								{
773 									if ( commonReached )
774 										startTop = enlargeable;
775 									else if ( enlargeable )
776 										this.setStartBefore( enlargeable );
777 								}
778 								else
779 									needsWhiteSpace = true;
780 							}
781
782 							if ( sibling )
783 							{
784 								var next = sibling.getPrevious();
785
786 								if ( !enlargeable && !next )
787 								{
788 									// Set the sibling as enlargeable, so it's
789 									// parent will be get later outside this while.
790 									enlargeable = sibling;
791 									sibling = null;
792 									break;
793 								}
794
795 								sibling = next;
796 							}
797 							else
798 							{
799 								// If sibling has been set to null, then we
800 								// need to stop enlarging.
801 								enlargeable = null;
802 							}
803 						}
804
805 						if ( enlargeable )
806 							enlargeable = enlargeable.getParent();
807 					}
808
809 					// Process the end boundary. This is basically the same
810 					// code used for the start boundary, with small changes to
811 					// make it work in the oposite side (to the right). This
812 					// makes it difficult to reuse the code here. So, fixes to
813 					// the above code are likely to be replicated here.
814
815 					container = this.endContainer;
816 					offset = this.endOffset;
817
818 					// Reset the common variables.
819 					enlargeable = sibling = null;
820 					commonReached = needsWhiteSpace = false;
821
822 					if ( container.type == CKEDITOR.NODE_TEXT )
823 					{
824 						// Check if there is any non-space text after the
825 						// offset. Otherwise, container is null.
826 						container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;
827
828 						// If we found only whitespace in the node, it
829 						// means that we'll need more whitespace to be able
830 						// to expand. For example, <i> can be expanded in
831 						// "A <i> [B]</i>", but not in "A<i> [B]</i>".
832 						needsWhiteSpace = !( container && container.getLength() );
833
834 						if ( container )
835 						{
836 							if ( !( sibling = container.getNext() ) )
837 								enlargeable = container.getParent();
838 						}
839 					}
840 					else
841 					{
842 						// Get the node right after the boudary to be checked
843 						// first.
844 						sibling = container.getChild( offset );
845
846 						if ( !sibling )
847 							enlargeable = container;
848 					}
849
850 					while ( enlargeable || sibling )
851 					{
852 						if ( enlargeable && !sibling )
853 						{
854 							if ( !commonReached && enlargeable.equals( commonAncestor ) )
855 								commonReached = true;
856
857 							if ( !body.contains( enlargeable ) )
858 								break;
859
860 							if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )
861 							{
862 								needsWhiteSpace = false;
863
864 								if ( commonReached )
865 									endTop = enlargeable;
866 								else if ( enlargeable )
867 									this.setEndAfter( enlargeable );
868 							}
869
870 							sibling = enlargeable.getNext();
871 						}
872
873 						while ( sibling )
874 						{
875 							isWhiteSpace = false;
876
877 							if ( sibling.type == CKEDITOR.NODE_TEXT )
878 							{
879 								siblingText = sibling.getText();
880
881 								if ( /[^\s\ufeff]/.test( siblingText ) )
882 									sibling = null;
883
884 								isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
885 							}
886 							else
887 							{
888 								// If this is a visible element.
889 								if ( sibling.$.offsetWidth > 0 )
890 								{
891 									// We'll accept it only if we need
892 									// whitespace, and this is an inline
893 									// element with whitespace only.
894 									if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )
895 									{
896 										// It must contains spaces and inline elements only.
897
898 										siblingText = sibling.getText();
899
900 										if ( !(/[^\s\ufeff]/).test( siblingText ) )
901 											sibling = null;
902 										else
903 										{
904 											allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );
905 											for ( i = 0 ; child = allChildren[ i++ ] ; )
906 											{
907 												if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )
908 												{
909 													sibling = null;
910 													break;
911 												}
912 											}
913 										}
914
915 										if ( sibling )
916 											isWhiteSpace = !!siblingText.length;
917 									}
918 									else
919 										sibling = null;
920 								}
921 							}
922
923 							if ( isWhiteSpace )
924 							{
925 								if ( needsWhiteSpace )
926 								{
927 									if ( commonReached )
928 										endTop = enlargeable;
929 									else
930 										this.setEndAfter( enlargeable );
931 								}
932 							}
933
934 							if ( sibling )
935 							{
936 								next = sibling.getNext();
937
938 								if ( !enlargeable && !next )
939 								{
940 									enlargeable = sibling;
941 									sibling = null;
942 									break;
943 								}
944
945 								sibling = next;
946 							}
947 							else
948 							{
949 								// If sibling has been set to null, then we
950 								// need to stop enlarging.
951 								enlargeable = null;
952 							}
953 						}
954
955 						if ( enlargeable )
956 							enlargeable = enlargeable.getParent();
957 					}
958
959 					// If the common ancestor can be enlarged by both boundaries, then include it also.
960 					if ( startTop && endTop )
961 					{
962 						commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
963
964 						this.setStartBefore( commonAncestor );
965 						this.setEndAfter( commonAncestor );
966 					}
967 					break;
968
969 				case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
970 				case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
971 					// DFS backward to get the block/list item boundary at or before the start.
972 					var boundaryNodes = getBoundaryNodes.apply( this ),
973 						startNode = boundaryNodes.startNode,
974 						endNode = boundaryNodes.endNode,
975 						guardFunction = ( unit == CKEDITOR.ENLARGE_BLOCK_CONTENTS ?
976 							CKEDITOR.dom.domWalker.blockBoundary() :
977 							CKEDITOR.dom.domWalker.listItemBoundary() ),
978 						walker = new CKEDITOR.dom.domWalker( startNode ),
979 						data = walker.reverse( guardFunction ),
980 						boundaryEvent = data.events.shift();
981
982 					this.setStartBefore( boundaryEvent.from );
983
984 					// DFS forward to get the block/list item boundary at or before the end.
985 					walker.setNode( endNode );
986 					data = walker.forward( guardFunction );
987 					boundaryEvent = data.events.shift();
988
989 					this.setEndAfter( boundaryEvent.from );
990 					break;
991
992 				default:
993 			}
994 		},
995
996 		/**
997 		 * Inserts a node at the start of the range. The range will be expanded
998 		 * the contain the node.
999 		 */
1000 		insertNode : function( node )
1001 		{
1002 			this.trim( false, true );
1003
1004 			var startContainer = this.startContainer;
1005 			var startOffset = this.startOffset;
1006
1007 			var nextNode = startContainer.getChild( startOffset );
1008
1009 			if ( nextNode )
1010 				node.insertBefore( nextNode );
1011 			else
1012 				startContainer.append( node );
1013
1014 			// Check if we need to update the end boundary.
1015 			if ( node.getParent().equals( this.endContainer ) )
1016 				this.endOffset++;
1017
1018 			// Expand the range to embrace the new node.
1019 			this.setStartBefore( node );
1020 		},
1021
1022 		moveToPosition : function( node, position )
1023 		{
1024 			this.setStartAt( node, position );
1025 			this.collapse( true );
1026 		},
1027
1028 		selectNodeContents : function( node )
1029 		{
1030 			this.setStart( node, 0 );
1031 			this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
1032 		},
1033
1034 		/**
1035 		 * Sets the start position of a Range.
1036 		 * @param {CKEDITOR.dom.node} startNode The node to start the range.
1037 		 * @param {Number} startOffset An integer greater than or equal to zero
1038 		 *		representing the offset for the start of the range from the start
1039 		 *		of startNode.
1040 		 */
1041 		setStart : function( startNode, startOffset )
1042 		{
1043 			// W3C requires a check for the new position. If it is after the end
1044 			// boundary, the range should be collapsed to the new start. It seams
1045 			// we will not need this check for our use of this class so we can
1046 			// ignore it for now.
1047
1048 			this.startContainer	= startNode;
1049 			this.startOffset	= startOffset;
1050
1051 			if ( !this.endContainer )
1052 			{
1053 				this.endContainer	= startNode;
1054 				this.endOffset		= startOffset;
1055 			}
1056
1057 			updateCollapsed( this );
1058 		},
1059
1060 		/**
1061 		 * Sets the end position of a Range.
1062 		 * @param {CKEDITOR.dom.node} endNode The node to end the range.
1063 		 * @param {Number} endOffset An integer greater than or equal to zero
1064 		 *		representing the offset for the end of the range from the start
1065 		 *		of endNode.
1066 		 */
1067 		setEnd : function( endNode, endOffset )
1068 		{
1069 			// W3C requires a check for the new position. If it is before the start
1070 			// boundary, the range should be collapsed to the new end. It seams we
1071 			// will not need this check for our use of this class so we can ignore
1072 			// it for now.
1073
1074 			this.endContainer	= endNode;
1075 			this.endOffset		= endOffset;
1076
1077 			if ( !this.startContainer )
1078 			{
1079 				this.startContainer	= endNode;
1080 				this.startOffset	= endOffset;
1081 			}
1082
1083 			updateCollapsed( this );
1084 		},
1085
1086 		setStartAfter : function( node )
1087 		{
1088 			this.setStart( node.getParent(), node.getIndex() + 1 );
1089 		},
1090
1091 		setStartBefore : function( node )
1092 		{
1093 			this.setStart( node.getParent(), node.getIndex() );
1094 		},
1095
1096 		setEndAfter : function( node )
1097 		{
1098 			this.setEnd( node.getParent(), node.getIndex() + 1 );
1099 		},
1100
1101 		setEndBefore : function( node )
1102 		{
1103 			this.setEnd( node.getParent(), node.getIndex() );
1104 		},
1105
1106 		setStartAt : function( node, position )
1107 		{
1108 			switch( position )
1109 			{
1110 				case CKEDITOR.POSITION_AFTER_START :
1111 					this.setStart( node, 0 );
1112 					break;
1113
1114 				case CKEDITOR.POSITION_BEFORE_END :
1115 					if ( node.type == CKEDITOR.NODE_TEXT )
1116 						this.setStart( node, node.getLength() );
1117 					else
1118 						this.setStart( node, node.getChildCount() );
1119 					break;
1120
1121 				case CKEDITOR.POSITION_BEFORE_START :
1122 					this.setStartBefore( node );
1123 					break;
1124
1125 				case CKEDITOR.POSITION_AFTER_END :
1126 					this.setStartAfter( node );
1127 			}
1128
1129 			updateCollapsed( this );
1130 		},
1131
1132 		setEndAt : function( node, position )
1133 		{
1134 			switch( position )
1135 			{
1136 				case CKEDITOR.POSITION_AFTER_START :
1137 					this.setEnd( node, 0 );
1138 					break;
1139
1140 				case CKEDITOR.POSITION_BEFORE_END :
1141 					if ( node.type == CKEDITOR.NODE_TEXT )
1142 						this.setEnd( node, node.getLength() );
1143 					else
1144 						this.setEnd( node, node.getChildCount() );
1145 					break;
1146
1147 				case CKEDITOR.POSITION_BEFORE_START :
1148 					this.setEndBefore( node );
1149 					break;
1150
1151 				case CKEDITOR.POSITION_AFTER_END :
1152 					this.setEndAfter( node );
1153 			}
1154
1155 			updateCollapsed( this );
1156 		},
1157
1158 		// TODO: The fixed block isn't trimmed, does not work for <pre>.
1159 		// TODO: Does not add bogus <br> to empty fixed blocks.
1160 		fixBlock : function( isStart, blockTag )
1161 		{
1162 			var bookmark = this.createBookmark(),
1163 				fixedBlock = new CKEDITOR.dom.element( blockTag, this.document );
1164 			this.collapse( isStart );
1165 			this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
1166 			this.extractContents().appendTo( fixedBlock );
1167 			this.insertNode( fixedBlock );
1168 			this.moveToBookmark( bookmark );
1169 			return fixedBlock;
1170 		},
1171
1172 		splitBlock : function( blockTag )
1173 		{
1174 			var startPath = new CKEDITOR.dom.elementPath( this.startContainer ),
1175 				endPath = new CKEDITOR.dom.elementPath( this.endContainer ),
1176 				startBlockLimit = startPath.blockLimit,
1177 				endBlockLimit = endPath.blockLimit,
1178 				startBlock = startPath.block,
1179 				endBlock = endPath.block,
1180 				elementPath = null;
1181
1182 			if ( !startBlockLimit.equals( endBlockLimit ) )
1183 				return null;
1184
1185 			// Get or fix current blocks.
1186 			if ( blockTag != 'br' )
1187 			{
1188 				if ( !startBlock )
1189 				{
1190 					startBlock = this.fixBlock( true, blockTag );
1191 					endBlock = new CKEDITOR.dom.elementPath( this.endContainer );
1192 				}
1193
1194 				if ( !endBlock )
1195 					endBlock = this.fixBlock( false, blockTag );
1196 			}
1197
1198 			// Get the range position.
1199 			var isStartOfBlock = startBlock && this.checkStartOfBlock(),
1200 				isEndOfBlock = endBlock && this.checkEndOfBlock();
1201
1202 			// Delete the current contents.
1203 			// TODO: Why is 2.x doing CheckIsEmpty()?
1204 			this.deleteContents();
1205
1206 			if ( startBlock && startBlock.equals( endBlock ) )
1207 			{
1208 				if ( isEndOfBlock )
1209 				{
1210 					elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1211 					this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
1212 					endBlock = null;
1213 				}
1214 				else if ( isStartOfBlock )
1215 				{
1216 					elementPath = new CKEDITOR.dom.elementPath( this.startContainer );
1217 					this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
1218 					startBlock = null;
1219 				}
1220 				else
1221 				{
1222 					// Extract the contents of the block from the selection point to the end
1223 					// of its contents.
1224 					this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END );
1225 					var documentFragment = this.extractContents();
1226
1227 					// Duplicate the block element after it.
1228 					endBlock = startBlock.clone( false );
1229 					endBlock.removeAttribute( 'id' );
1230
1231 					// Place the extracted contents into the duplicated block.
1232 					documentFragment.appendTo( endBlock );
1233 					endBlock.insertAfter( startBlock );
1234 					this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END );
1235
1236 					// TODO: Append bogus br to startBlock for Gecko
1237 				}
1238 			}
1239
1240 			return {
1241 				previousBlock : startBlock,
1242 				nextBlock : endBlock,
1243 				wasStartOfBlock : isStartOfBlock,
1244 				wasEndOfBlock : isEndOfBlock,
1245 				elementPath : elementPath
1246 			};
1247 		},
1248
1249 		checkStartOfBlock : function()
1250 		{
1251 			var startContainer = this.startContainer,
1252 				startOffset = this.startOffset;
1253
1254 			// If the starting node is a text node, and non-empty before the offset,
1255 			// then we're surely not at the start of block.
1256 			if ( startContainer.type == CKEDITOR.NODE_TEXT )
1257 			{
1258 				var textBefore = CKEDITOR.tools.ltrim( startContainer.getText().substr( 0, startOffset ) );
1259 				if ( textBefore.length > 0 )
1260 					return false;
1261 			}
1262
1263 			var startNode = getBoundaryNodes.apply( this ).startNode,
1264 				walker = new CKEDITOR.dom.domWalker( startNode );
1265
1266 			// DFS backwards until the block boundary, with the checker function.
1267 			walker.on( 'step', getCheckStartEndBlockFunction( true ), null, null, 20 );
1268 			walker.reverse( CKEDITOR.dom.domWalker.blockBoundary() );
1269
1270 			return !walker.checkFailed;
1271 		},
1272
1273 		checkEndOfBlock : function()
1274 		{
1275 			var endContainer = this.endContainer,
1276 				endOffset = this.endOffset;
1277
1278 			// If the ending node is a text node, and non-empty after the offset,
1279 			// then we're surely not at the end of block.
1280 			if ( endContainer.type == CKEDITOR.NODE_TEXT )
1281 			{
1282 				var textAfter = CKEDITOR.tools.rtrim( endContainer.getText().substr( endOffset ) );
1283 				if ( textAfter.length > 0 )
1284 					return false;
1285 			}
1286
1287 			var endNode = getBoundaryNodes.apply( this ).endNode,
1288 				walker = new CKEDITOR.dom.domWalker( endNode );
1289
1290 			// DFS forward until the block boundary, with the checker function.
1291 			walker.on( 'step', getCheckStartEndBlockFunction( false ), null, null, 20 );
1292 			walker.forward( CKEDITOR.dom.domWalker.blockBoundary() );
1293
1294 			return !walker.checkFailed;
1295 		}
1296 	};
1297 })();
1298
1299 CKEDITOR.POSITION_AFTER_START	= 1;	// <element>^contents</element>		"^text"
1300 CKEDITOR.POSITION_BEFORE_END	= 2;	// <element>contents^</element>		"text^"
1301 CKEDITOR.POSITION_BEFORE_START	= 3;	// ^<element>contents</element>		^"text"
1302 CKEDITOR.POSITION_AFTER_END		= 4;	// <element>contents</element>^		"text"
1303
1304 CKEDITOR.ENLARGE_ELEMENT = 1;
1305 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
1306 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
1307