1/*
2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben
4 *
5 * == BEGIN LICENSE ==
6 *
7 * Licensed under the terms of any of the following licenses at your
8 * choice:
9 *
10 *  - GNU General Public License Version 2 or later (the "GPL")
11 *    http://www.gnu.org/licenses/gpl.html
12 *
13 *  - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
14 *    http://www.gnu.org/licenses/lgpl.html
15 *
16 *  - Mozilla Public License Version 1.1 or later (the "MPL")
17 *    http://www.mozilla.org/MPL/MPL-1.1.html
18 *
19 * == END LICENSE ==
20 *
21 * Class for working with a selection range, much like the W3C DOM Range, but
22 * it is not intended to be an implementation of the W3C interface.
23 */
24
25var FCKDomRange = function( sourceWindow )
26{
27	this.Window = sourceWindow ;
28	this._Cache = {} ;
29}
30
31FCKDomRange.prototype =
32{
33
34	_UpdateElementInfo : function()
35	{
36		var innerRange = this._Range ;
37
38		if ( !innerRange )
39			this.Release( true ) ;
40		else
41		{
42			// For text nodes, the node itself is the StartNode.
43			var eStart	= innerRange.startContainer ;
44			var eEnd	= innerRange.endContainer ;
45
46			var oElementPath = new FCKElementPath( eStart ) ;
47			this.StartNode			= eStart.nodeType == 3 ? eStart : eStart.childNodes[ innerRange.startOffset ] ;
48			this.StartContainer		= eStart ;
49			this.StartBlock			= oElementPath.Block ;
50			this.StartBlockLimit	= oElementPath.BlockLimit ;
51
52			if ( eStart != eEnd )
53				oElementPath = new FCKElementPath( eEnd ) ;
54
55			// The innerRange.endContainer[ innerRange.endOffset ] is not
56			// usually part of the range, but the marker for the range end. So,
57			// let's get the previous available node as the real end.
58			var eEndNode = eEnd ;
59			if ( innerRange.endOffset == 0 )
60			{
61				while ( eEndNode && !eEndNode.previousSibling )
62					eEndNode = eEndNode.parentNode ;
63
64				if ( eEndNode )
65					eEndNode = eEndNode.previousSibling ;
66			}
67			else if ( eEndNode.nodeType == 1 )
68				eEndNode = eEndNode.childNodes[ innerRange.endOffset - 1 ] ;
69
70			this.EndNode			= eEndNode ;
71			this.EndContainer		= eEnd ;
72			this.EndBlock			= oElementPath.Block ;
73			this.EndBlockLimit		= oElementPath.BlockLimit ;
74		}
75
76		this._Cache = {} ;
77	},
78
79	CreateRange : function()
80	{
81		return new FCKW3CRange( this.Window.document ) ;
82	},
83
84	DeleteContents : function()
85	{
86		if ( this._Range )
87		{
88			this._Range.deleteContents() ;
89			this._UpdateElementInfo() ;
90		}
91	},
92
93	ExtractContents : function()
94	{
95		if ( this._Range )
96		{
97			var docFrag = this._Range.extractContents() ;
98			this._UpdateElementInfo() ;
99			return docFrag ;
100		}
101	},
102
103	CheckIsCollapsed : function()
104	{
105		if ( this._Range )
106			return this._Range.collapsed ;
107	},
108
109	Collapse : function( toStart )
110	{
111		if ( this._Range )
112			this._Range.collapse( toStart ) ;
113
114		this._UpdateElementInfo() ;
115	},
116
117	Clone : function()
118	{
119		var oClone = FCKTools.CloneObject( this ) ;
120
121		if ( this._Range )
122			oClone._Range = this._Range.cloneRange() ;
123
124		return oClone ;
125	},
126
127	MoveToNodeContents : function( targetNode )
128	{
129		if ( !this._Range )
130			this._Range = this.CreateRange() ;
131
132		this._Range.selectNodeContents( targetNode ) ;
133
134		this._UpdateElementInfo() ;
135	},
136
137	MoveToElementStart : function( targetElement )
138	{
139		this.SetStart(targetElement,1) ;
140		this.SetEnd(targetElement,1) ;
141	},
142
143	// Moves to the first editing point inside a element. For example, in a
144	// element tree like "<p><b><i></i></b> Text</p>", the start editing point
145	// is "<p><b><i>^</i></b> Text</p>" (inside <i>).
146	MoveToElementEditStart : function( targetElement )
147	{
148		var child ;
149
150		while ( ( child = targetElement.firstChild ) && child.nodeType == 1 && FCKListsLib.EmptyElements[ child.nodeName.toLowerCase() ] == null )
151			targetElement = child ;
152
153		this.MoveToElementStart( targetElement ) ;
154	},
155
156	InsertNode : function( node )
157	{
158		if ( this._Range )
159			this._Range.insertNode( node ) ;
160	},
161
162	CheckIsEmpty : function()
163	{
164		if ( this.CheckIsCollapsed() )
165			return true ;
166
167		// Inserts the contents of the range in a div tag.
168		var eToolDiv = this.Window.document.createElement( 'div' ) ;
169		this._Range.cloneContents().AppendTo( eToolDiv ) ;
170
171		FCKDomTools.TrimNode( eToolDiv ) ;
172
173		return ( eToolDiv.innerHTML.length == 0 ) ;
174	},
175
176	CheckStartOfBlock : function()
177	{
178		var bIsStartOfBlock = this._Cache.IsStartOfBlock ;
179
180		if ( bIsStartOfBlock != undefined )
181			return bIsStartOfBlock ;
182
183		// Create a clone of the current range.
184		var oTestRange = this.Clone() ;
185
186		// Collapse it to its start point.
187		oTestRange.Collapse( true ) ;
188
189		// Move the start boundary to the start of the block.
190		oTestRange.SetStart( oTestRange.StartBlock || oTestRange.StartBlockLimit, 1 ) ;
191
192		if ( oTestRange.CheckIsCollapsed() )
193			bIsStartOfBlock = true ;
194		else
195		{
196			// Inserts the contents of the range in a div tag.
197			var eToolDiv = oTestRange.Window.document.createElement( 'div' ) ;
198			oTestRange._Range.cloneContents().AppendTo( eToolDiv ) ;
199
200			// This line is why we don't use CheckIsEmpty() here...
201			// Because using RTrimNode() or TrimNode() would be incorrect -
202			// TrimNode() and RTrimNode() would delete <br> nodes at the end of the div node,
203			// but for checking start of block they are actually meaningful. (Bug #1350)
204			FCKDomTools.LTrimNode( eToolDiv ) ;
205
206			bIsStartOfBlock = ( eToolDiv.innerHTML.length == 0 ) ;
207		}
208
209		oTestRange.Release() ;
210
211		return ( this._Cache.IsStartOfBlock = bIsStartOfBlock ) ;
212	},
213
214	CheckEndOfBlock : function( refreshSelection )
215	{
216		var bIsEndOfBlock = this._Cache.IsEndOfBlock ;
217
218		if ( bIsEndOfBlock != undefined )
219			return bIsEndOfBlock ;
220
221		// Create a clone of the current range.
222		var oTestRange = this.Clone() ;
223
224		// Collapse it to its end point.
225		oTestRange.Collapse( false ) ;
226
227		// Move the end boundary to the end of the block.
228		oTestRange.SetEnd( oTestRange.EndBlock || oTestRange.EndBlockLimit, 2 ) ;
229
230		bIsEndOfBlock = oTestRange.CheckIsCollapsed() ;
231
232		if ( !bIsEndOfBlock )
233		{
234			// Inserts the contents of the range in a div tag.
235			var eToolDiv = this.Window.document.createElement( 'div' ) ;
236			oTestRange._Range.cloneContents().AppendTo( eToolDiv ) ;
237			FCKDomTools.TrimNode( eToolDiv ) ;
238
239			// Find out if we are in an empty tree of inline elements, like <b><i><span></span></i></b>
240			bIsEndOfBlock = true ;
241			var eLastChild = eToolDiv ;
242			while ( ( eLastChild = eLastChild.lastChild ) )
243			{
244				// Check the following:
245				//		1. Is there more than one node in the parents children?
246				//		2. Is the node not an element node?
247				//		3. Is it not a inline element.
248				if ( eLastChild.previousSibling || eLastChild.nodeType != 1 || FCKListsLib.InlineChildReqElements[ eLastChild.nodeName.toLowerCase() ] == null )
249				{
250					// So we are not in the end of the range.
251					bIsEndOfBlock = false ;
252					break ;
253				}
254			}
255		}
256
257		oTestRange.Release() ;
258
259		if ( refreshSelection )
260			this.Select() ;
261
262		return this._Cache.IsEndOfBlock = bIsEndOfBlock ;
263	},
264
265	// This is an "intrusive" way to create a bookmark. It includes <span> tags
266	// in the range boundaries. The advantage of it is that it is possible to
267	// handle DOM mutations when moving back to the bookmark.
268	// Attention: the inclusion of nodes in the DOM is a design choice and
269	// should not be changed as there are other points in the code that may be
270	// using those nodes to perform operations. See GetBookmarkNode.
271	// For performance, includeNodes=true if intended to SelectBookmark.
272	CreateBookmark : function( includeNodes )
273	{
274		// Create the bookmark info (random IDs).
275		var oBookmark =
276		{
277			StartId	: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'S',
278			EndId	: (new Date()).valueOf() + Math.floor(Math.random()*1000) + 'E'
279		} ;
280
281		var oDoc = this.Window.document ;
282		var eStartSpan ;
283		var eEndSpan ;
284		var oClone ;
285
286		// For collapsed ranges, add just the start marker.
287		if ( !this.CheckIsCollapsed() )
288		{
289			eEndSpan = oDoc.createElement( 'span' ) ;
290			eEndSpan.style.display = 'none' ;
291			eEndSpan.id = oBookmark.EndId ;
292			eEndSpan.setAttribute( '_fck_bookmark', true ) ;
293
294			// For IE, it must have something inside, otherwise it may be
295			// removed during DOM operations.
296//			if ( FCKBrowserInfo.IsIE )
297				eEndSpan.innerHTML = '&nbsp;' ;
298
299			oClone = this.Clone() ;
300			oClone.Collapse( false ) ;
301			oClone.InsertNode( eEndSpan ) ;
302		}
303
304		eStartSpan = oDoc.createElement( 'span' ) ;
305		eStartSpan.style.display = 'none' ;
306		eStartSpan.id = oBookmark.StartId ;
307		eStartSpan.setAttribute( '_fck_bookmark', true ) ;
308
309		// For IE, it must have something inside, otherwise it may be removed
310		// during DOM operations.
311//		if ( FCKBrowserInfo.IsIE )
312			eStartSpan.innerHTML = '&nbsp;' ;
313
314		oClone = this.Clone() ;
315		oClone.Collapse( true ) ;
316		oClone.InsertNode( eStartSpan ) ;
317
318		if ( includeNodes )
319		{
320			oBookmark.StartNode = eStartSpan ;
321			oBookmark.EndNode = eEndSpan ;
322		}
323
324		// Update the range position.
325		if ( eEndSpan )
326		{
327			this.SetStart( eStartSpan, 4 ) ;
328			this.SetEnd( eEndSpan, 3 ) ;
329		}
330		else
331			this.MoveToPosition( eStartSpan, 4 ) ;
332
333		return oBookmark ;
334	},
335
336	// This one should be a part of a hypothetic "bookmark" object.
337	GetBookmarkNode : function( bookmark, start )
338	{
339		var doc = this.Window.document ;
340
341		if ( start )
342			return bookmark.StartNode || doc.getElementById( bookmark.StartId ) ;
343		else
344			return bookmark.EndNode || doc.getElementById( bookmark.EndId ) ;
345	},
346
347	MoveToBookmark : function( bookmark, preserveBookmark )
348	{
349		var eStartSpan	= this.GetBookmarkNode( bookmark, true ) ;
350		var eEndSpan	= this.GetBookmarkNode( bookmark, false ) ;
351
352		this.SetStart( eStartSpan, 3 ) ;
353
354		if ( !preserveBookmark )
355			FCKDomTools.RemoveNode( eStartSpan ) ;
356
357		// If collapsed, the end span will not be available.
358		if ( eEndSpan )
359		{
360			this.SetEnd( eEndSpan, 3 ) ;
361
362			if ( !preserveBookmark )
363				FCKDomTools.RemoveNode( eEndSpan ) ;
364		}
365		else
366			this.Collapse( true ) ;
367
368		this._UpdateElementInfo() ;
369	},
370
371	// Non-intrusive bookmark algorithm
372	CreateBookmark2 : function()
373	{
374		// If there is no range then get out of here.
375		// It happens on initial load in Safari #962 and if the editor it's hidden also in Firefox
376		if ( ! this._Range )
377			return { "Start" : 0, "End" : 0 } ;
378
379		// First, we record down the offset values
380		var bookmark =
381		{
382			"Start" : [ this._Range.startOffset ],
383			"End" : [ this._Range.endOffset ]
384		} ;
385		var curStart = this._Range.startContainer.previousSibling ;
386		var curEnd = this._Range.endContainer.previousSibling ;
387		while ( curStart && curStart.nodeType == 3 )
388		{
389			bookmark.Start[0] += curStart.length ;
390			curStart = curStart.previousSibling ;
391		}
392		while ( curEnd && curEnd.nodeType == 3 )
393		{
394			bookmark.End[0] += curEnd.length ;
395			curEnd = curEnd.previousSibling ;
396		}
397		// Then, we record down the precise position of the container nodes
398		// by walking up the DOM tree and counting their childNode index
399		bookmark.Start = FCKDomTools.GetNodeAddress( this._Range.startContainer, true ).concat( bookmark.Start ) ;
400		bookmark.End = FCKDomTools.GetNodeAddress( this._Range.endContainer, true ).concat( bookmark.End ) ;
401		return bookmark;
402	},
403
404	MoveToBookmark2 : function( bookmark )
405	{
406		// Reverse the childNode counting algorithm in CreateBookmark2()
407		var curStart = FCKDomTools.GetNodeFromAddress( this.Window.document, bookmark.Start.slice( 0, -1 ), true ) ;
408		var curEnd = FCKDomTools.GetNodeFromAddress( this.Window.document, bookmark.End.slice( 0, -1 ), true ) ;
409
410		// Generate the W3C Range object and update relevant data
411		this.Release( true ) ;
412		this._Range = new FCKW3CRange( this.Window.document ) ;
413		var startOffset = bookmark.Start[ bookmark.Start.length - 1 ] ;
414		var endOffset = bookmark.End[ bookmark.End.length - 1 ] ;
415		while ( curStart.nodeType == 3 && startOffset > curStart.length )
416		{
417			if ( ! curStart.nextSibling || curStart.nextSibling.nodeType != 3 )
418				break ;
419			startOffset -= curStart.length ;
420			curStart = curStart.nextSibling ;
421		}
422		while ( curEnd.nodeType == 3 && endOffset > curEnd.length )
423		{
424			if ( ! curEnd.nextSibling || curEnd.nextSibling.nodeType != 3 )
425				break ;
426			endOffset -= curEnd.length ;
427			curEnd = curEnd.nextSibling ;
428		}
429		this._Range.setStart( curStart, startOffset ) ;
430		this._Range.setEnd( curEnd, endOffset ) ;
431		this._UpdateElementInfo() ;
432	},
433
434	MoveToPosition : function( targetElement, position )
435	{
436		this.SetStart( targetElement, position ) ;
437		this.Collapse( true ) ;
438	},
439
440	/*
441	 * Moves the position of the start boundary of the range to a specific position
442	 * relatively to a element.
443	 *		@position:
444	 *			1 = After Start		<target>^contents</target>
445	 *			2 = Before End		<target>contents^</target>
446	 *			3 = Before Start	^<target>contents</target>
447	 *			4 = After End		<target>contents</target>^
448	 */
449	SetStart : function( targetElement, position, noInfoUpdate )
450	{
451		var oRange = this._Range ;
452		if ( !oRange )
453			oRange = this._Range = this.CreateRange() ;
454
455		switch( position )
456		{
457			case 1 :		// After Start		<target>^contents</target>
458				oRange.setStart( targetElement, 0 ) ;
459				break ;
460
461			case 2 :		// Before End		<target>contents^</target>
462				oRange.setStart( targetElement, targetElement.childNodes.length ) ;
463				break ;
464
465			case 3 :		// Before Start		^<target>contents</target>
466				oRange.setStartBefore( targetElement ) ;
467				break ;
468
469			case 4 :		// After End		<target>contents</target>^
470				oRange.setStartAfter( targetElement ) ;
471		}
472
473		if ( !noInfoUpdate )
474			this._UpdateElementInfo() ;
475	},
476
477	/*
478	 * Moves the position of the start boundary of the range to a specific position
479	 * relatively to a element.
480	 *		@position:
481	 *			1 = After Start		<target>^contents</target>
482	 *			2 = Before End		<target>contents^</target>
483	 *			3 = Before Start	^<target>contents</target>
484	 *			4 = After End		<target>contents</target>^
485	 */
486	SetEnd : function( targetElement, position, noInfoUpdate )
487	{
488		var oRange = this._Range ;
489		if ( !oRange )
490			oRange = this._Range = this.CreateRange() ;
491
492		switch( position )
493		{
494			case 1 :		// After Start		<target>^contents</target>
495				oRange.setEnd( targetElement, 0 ) ;
496				break ;
497
498			case 2 :		// Before End		<target>contents^</target>
499				oRange.setEnd( targetElement, targetElement.childNodes.length ) ;
500				break ;
501
502			case 3 :		// Before Start		^<target>contents</target>
503				oRange.setEndBefore( targetElement ) ;
504				break ;
505
506			case 4 :		// After End		<target>contents</target>^
507				oRange.setEndAfter( targetElement ) ;
508		}
509
510		if ( !noInfoUpdate )
511			this._UpdateElementInfo() ;
512	},
513
514	Expand : function( unit )
515	{
516		var oNode, oSibling ;
517
518		switch ( unit )
519		{
520			// Expand the range to include all inline parent elements if we are
521			// are in their boundary limits.
522			// For example (where [ ] are the range limits):
523			//	Before =>		Some <b>[<i>Some sample text]</i></b>.
524			//	After =>		Some [<b><i>Some sample text</i></b>].
525			case 'inline_elements' :
526				// Expand the start boundary.
527				if ( this._Range.startOffset == 0 )
528				{
529					oNode = this._Range.startContainer ;
530
531					if ( oNode.nodeType != 1 )
532						oNode = oNode.previousSibling ? null : oNode.parentNode ;
533
534					if ( oNode )
535					{
536						while ( FCKListsLib.InlineNonEmptyElements[ oNode.nodeName.toLowerCase() ] )
537						{
538							this._Range.setStartBefore( oNode ) ;
539
540							if ( oNode != oNode.parentNode.firstChild )
541								break ;
542
543							oNode = oNode.parentNode ;
544						}
545					}
546				}
547
548				// Expand the end boundary.
549				oNode = this._Range.endContainer ;
550				var offset = this._Range.endOffset ;
551
552				if ( ( oNode.nodeType == 3 && offset >= oNode.nodeValue.length ) || ( oNode.nodeType == 1 && offset >= oNode.childNodes.length ) || ( oNode.nodeType != 1 && oNode.nodeType != 3 ) )
553				{
554					if ( oNode.nodeType != 1 )
555						oNode = oNode.nextSibling ? null : oNode.parentNode ;
556
557					if ( oNode )
558					{
559						while ( FCKListsLib.InlineNonEmptyElements[ oNode.nodeName.toLowerCase() ] )
560						{
561							this._Range.setEndAfter( oNode ) ;
562
563							if ( oNode != oNode.parentNode.lastChild )
564								break ;
565
566							oNode = oNode.parentNode ;
567						}
568					}
569				}
570
571				break ;
572
573			case 'block_contents' :
574			case 'list_contents' :
575				var boundarySet = FCKListsLib.BlockBoundaries ;
576				if ( unit == 'list_contents' || FCKConfig.EnterMode == 'br' )
577					boundarySet = FCKListsLib.ListBoundaries ;
578
579				if ( this.StartBlock && FCKConfig.EnterMode != 'br' && unit == 'block_contents' )
580					this.SetStart( this.StartBlock, 1 ) ;
581				else
582				{
583					// Get the start node for the current range.
584					oNode = this._Range.startContainer ;
585
586					// If it is an element, get the node right before of it (in source order).
587					if ( oNode.nodeType == 1 )
588					{
589						var lastNode = oNode.childNodes[ this._Range.startOffset ] ;
590						if ( lastNode )
591							oNode = FCKDomTools.GetPreviousSourceNode( lastNode, true ) ;
592						else
593							oNode = oNode.lastChild || oNode ;
594					}
595
596					// We must look for the left boundary, relative to the range
597					// start, which is limited by a block element.
598					while ( oNode
599							&& ( oNode.nodeType != 1
600								|| ( oNode != this.StartBlockLimit
601									&& !boundarySet[ oNode.nodeName.toLowerCase() ] ) ) )
602					{
603						this._Range.setStartBefore( oNode ) ;
604						oNode = oNode.previousSibling || oNode.parentNode ;
605					}
606				}
607
608				if ( this.EndBlock && FCKConfig.EnterMode != 'br' && unit == 'block_contents' && this.EndBlock.nodeName.toLowerCase() != 'li' )
609					this.SetEnd( this.EndBlock, 2 ) ;
610				else
611				{
612					oNode = this._Range.endContainer ;
613					if ( oNode.nodeType == 1 )
614						oNode = oNode.childNodes[ this._Range.endOffset ] || oNode.lastChild ;
615
616					// We must look for the right boundary, relative to the range
617					// end, which is limited by a block element.
618					while ( oNode
619							&& ( oNode.nodeType != 1
620								|| ( oNode != this.StartBlockLimit
621									&& !boundarySet[ oNode.nodeName.toLowerCase() ] ) ) )
622					{
623						this._Range.setEndAfter( oNode ) ;
624						oNode = oNode.nextSibling || oNode.parentNode ;
625					}
626
627					// In EnterMode='br', the end <br> boundary element must
628					// be included in the expanded range.
629					if ( oNode && oNode.nodeName.toLowerCase() == 'br' )
630						this._Range.setEndAfter( oNode ) ;
631				}
632
633				this._UpdateElementInfo() ;
634		}
635	},
636
637	/**
638	 * Split the block element for the current range. It deletes the contents
639	 * of the range and splits the block in the collapsed position, resulting
640	 * in two sucessive blocks. The range is then positioned in the middle of
641	 * them.
642	 *
643	 * It returns and object with the following properties:
644	 *		- PreviousBlock	: a reference to the block element that preceeds
645	 *		  the range after the split.
646	 *		- NextBlock : a reference to the block element that preceeds the
647	 *		  range after the split.
648	 *		- WasStartOfBlock : a boolean indicating that the range was
649	 *		  originaly at the start of the block.
650	 *		- WasEndOfBlock : a boolean indicating that the range was originaly
651	 *		  at the end of the block.
652	 *
653	 * If the range was originaly at the start of the block, no split will happen
654	 * and the PreviousBlock value will be null. The same is valid for the
655	 * NextBlock value if the range was at the end of the block.
656	 */
657	SplitBlock : function()
658	{
659		if ( !this._Range )
660			this.MoveToSelection() ;
661
662		// The range boundaries must be in the same "block limit" element.
663		if ( this.StartBlockLimit == this.EndBlockLimit )
664		{
665			// Get the current blocks.
666			var eStartBlock		= this.StartBlock ;
667			var eEndBlock		= this.EndBlock ;
668
669			if ( FCKConfig.EnterMode != 'br' )
670			{
671				if ( !eStartBlock )
672				{
673					eStartBlock = this.FixBlock( true ) ;
674					eEndBlock	= this.EndBlock ;	// FixBlock may have fixed the EndBlock too.
675				}
676
677				if ( !eEndBlock )
678					eEndBlock = this.FixBlock( false ) ;
679			}
680
681			// Get the range position.
682			var bIsStartOfBlock	= ( eStartBlock != null && this.CheckStartOfBlock() ) ;
683			var bIsEndOfBlock	= ( eEndBlock != null && this.CheckEndOfBlock() ) ;
684
685			// Delete the current contents.
686			if ( !this.CheckIsEmpty() )
687				this.DeleteContents() ;
688
689			if ( eStartBlock && eEndBlock && eStartBlock == eEndBlock )
690			{
691				if ( bIsEndOfBlock )
692				{
693					this.MoveToPosition( eEndBlock, 4 ) ;
694					eEndBlock = null ;
695				}
696				else if ( bIsStartOfBlock )
697				{
698					this.MoveToPosition( eStartBlock, 3 ) ;
699					eStartBlock = null ;
700				}
701				else
702				{
703					// Extract the contents of the block from the selection point to the end of its contents.
704					this.SetEnd( eStartBlock, 2 ) ;
705					var eDocFrag = this.ExtractContents() ;
706
707					// Duplicate the block element after it.
708					eEndBlock = eStartBlock.cloneNode( false ) ;
709					eEndBlock.removeAttribute( 'id', false ) ;
710
711					// Place the extracted contents in the duplicated block.
712					eDocFrag.AppendTo( eEndBlock ) ;
713
714					FCKDomTools.InsertAfterNode( eStartBlock, eEndBlock ) ;
715
716					this.MoveToPosition( eStartBlock, 4 ) ;
717
718					// In Gecko, the last child node must be a bogus <br>.
719					// Note: bogus <br> added under <ul> or <ol> would cause lists to be incorrectly rendered.
720					if ( FCKBrowserInfo.IsGecko &&
721							! eStartBlock.nodeName.IEquals( ['ul', 'ol'] ) )
722						FCKTools.AppendBogusBr( eStartBlock ) ;
723				}
724			}
725
726			return {
727				PreviousBlock	: eStartBlock,
728				NextBlock		: eEndBlock,
729				WasStartOfBlock : bIsStartOfBlock,
730				WasEndOfBlock	: bIsEndOfBlock
731			} ;
732		}
733
734		return null ;
735	},
736
737	// Transform a block without a block tag in a valid block (orphan text in the body or td, usually).
738	FixBlock : function( isStart )
739	{
740		// Bookmark the range so we can restore it later.
741		var oBookmark = this.CreateBookmark() ;
742
743		// Collapse the range to the requested ending boundary.
744		this.Collapse( isStart ) ;
745
746		// Expands it to the block contents.
747		this.Expand( 'block_contents' ) ;
748
749		// Create the fixed block.
750		var oFixedBlock = this.Window.document.createElement( FCKConfig.EnterMode ) ;
751
752		// Move the contents of the temporary range to the fixed block.
753		this.ExtractContents().AppendTo( oFixedBlock ) ;
754		FCKDomTools.TrimNode( oFixedBlock ) ;
755
756		// Insert the fixed block into the DOM.
757		this.InsertNode( oFixedBlock ) ;
758
759		// Move the range back to the bookmarked place.
760		this.MoveToBookmark( oBookmark ) ;
761
762		return oFixedBlock ;
763	},
764
765	Release : function( preserveWindow )
766	{
767		if ( !preserveWindow )
768			this.Window = null ;
769
770		this.StartNode = null ;
771		this.StartContainer = null ;
772		this.StartBlock = null ;
773		this.StartBlockLimit = null ;
774		this.EndNode = null ;
775		this.EndContainer = null ;
776		this.EndBlock = null ;
777		this.EndBlockLimit = null ;
778		this._Range = null ;
779		this._Cache = null ;
780	},
781
782	CheckHasRange : function()
783	{
784		return !!this._Range ;
785	},
786
787	GetTouchedStartNode : function()
788	{
789		var range = this._Range ;
790		var container = range.startContainer ;
791
792		if ( range.collapsed || container.nodeType != 1 )
793			return container ;
794
795		return container.childNodes[ range.startOffset ] || container ;
796	},
797
798	GetTouchedEndNode : function()
799	{
800		var range = this._Range ;
801		var container = range.endContainer ;
802
803		if ( range.collapsed || container.nodeType != 1 )
804			return container ;
805
806		return container.childNodes[ range.endOffset - 1 ] || container ;
807	}
808} ;
809