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 * This class partially implements the W3C DOM Range for browser that don't
22 * support the standards (like IE):
23 * http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
24 */
25
26var FCKW3CRange = function( parentDocument )
27{
28	this._Document = parentDocument ;
29
30	this.startContainer	= null ;
31	this.startOffset	= null ;
32	this.endContainer	= null ;
33	this.endOffset		= null ;
34	this.collapsed		= true ;
35}
36
37FCKW3CRange.CreateRange = function( parentDocument )
38{
39	// We could opt to use the Range implementation of the browsers. The problem
40	// is that every browser have different bugs on their implementations,
41	// mostly related to different interpretations of the W3C specifications.
42	// So, for now, let's use our implementation and pray for browsers fixings
43	// soon. Otherwise will go crazy on trying to find out workarounds.
44	/*
45	// Get the browser implementation of the range, if available.
46	if ( parentDocument.createRange )
47	{
48		var range = parentDocument.createRange() ;
49		if ( typeof( range.startContainer ) != 'undefined' )
50			return range ;
51	}
52	*/
53	return new FCKW3CRange( parentDocument ) ;
54}
55
56FCKW3CRange.CreateFromRange = function( parentDocument, sourceRange )
57{
58	var range = FCKW3CRange.CreateRange( parentDocument ) ;
59	range.setStart( sourceRange.startContainer, sourceRange.startOffset ) ;
60	range.setEnd( sourceRange.endContainer, sourceRange.endOffset ) ;
61	return range ;
62}
63
64FCKW3CRange.prototype =
65{
66
67	_UpdateCollapsed : function()
68	{
69      this.collapsed = ( this.startContainer == this.endContainer && this.startOffset == this.endOffset ) ;
70	},
71
72	// W3C requires a check for the new position. If it is after the end
73	// boundary, the range should be collapsed to the new start. It seams we
74	// will not need this check for our use of this class so we can ignore it for now.
75	setStart : function( refNode, offset )
76	{
77		this.startContainer	= refNode ;
78		this.startOffset	= offset ;
79
80		if ( !this.endContainer )
81		{
82			this.endContainer	= refNode ;
83			this.endOffset		= offset ;
84		}
85
86		this._UpdateCollapsed() ;
87	},
88
89	// W3C requires a check for the new position. If it is before the start
90	// boundary, the range should be collapsed to the new end. It seams we
91	// will not need this check for our use of this class so we can ignore it for now.
92	setEnd : function( refNode, offset )
93	{
94		this.endContainer	= refNode ;
95		this.endOffset		= offset ;
96
97		if ( !this.startContainer )
98		{
99			this.startContainer	= refNode ;
100			this.startOffset	= offset ;
101		}
102
103		this._UpdateCollapsed() ;
104	},
105
106	setStartAfter : function( refNode )
107	{
108		this.setStart( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) + 1 ) ;
109	},
110
111	setStartBefore : function( refNode )
112	{
113		this.setStart( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) ) ;
114	},
115
116	setEndAfter : function( refNode )
117	{
118		this.setEnd( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) + 1 ) ;
119	},
120
121	setEndBefore : function( refNode )
122	{
123		this.setEnd( refNode.parentNode, FCKDomTools.GetIndexOf( refNode ) ) ;
124	},
125
126	collapse : function( toStart )
127	{
128		if ( toStart )
129		{
130			this.endContainer	= this.startContainer ;
131			this.endOffset		= this.startOffset ;
132		}
133		else
134		{
135			this.startContainer	= this.endContainer ;
136			this.startOffset	= this.endOffset ;
137		}
138
139		this.collapsed = true ;
140	},
141
142	selectNodeContents : function( refNode )
143	{
144		this.setStart( refNode, 0 ) ;
145		this.setEnd( refNode, refNode.nodeType == 3 ? refNode.data.length : refNode.childNodes.length ) ;
146	},
147
148	insertNode : function( newNode )
149	{
150		var startContainer = this.startContainer ;
151		var startOffset = this.startOffset ;
152
153		// If we are in a text node.
154		if ( startContainer.nodeType == 3 )
155		{
156			startContainer.splitText( startOffset ) ;
157
158			// Check if it is necessary to update the end boundary.
159			if ( startContainer == this.endContainer )
160				this.setEnd( startContainer.nextSibling, this.endOffset - this.startOffset ) ;
161
162			// Insert the new node it after the text node.
163			FCKDomTools.InsertAfterNode( startContainer, newNode ) ;
164
165			return ;
166		}
167		else
168		{
169			// Simply insert the new node before the current start node.
170			startContainer.insertBefore( newNode, startContainer.childNodes[ startOffset ] || null ) ;
171
172			// Check if it is necessary to update the end boundary.
173			if ( startContainer == this.endContainer )
174			{
175				this.endOffset++ ;
176				this.collapsed = false ;
177			}
178		}
179	},
180
181	deleteContents : function()
182	{
183		if ( this.collapsed )
184			return ;
185
186		this._ExecContentsAction( 0 ) ;
187	},
188
189	extractContents : function()
190	{
191		var docFrag = new FCKDocumentFragment( this._Document ) ;
192
193		if ( !this.collapsed )
194			this._ExecContentsAction( 1, docFrag ) ;
195
196		return docFrag ;
197	},
198
199	// The selection may be lost when cloning (due to the splitText() call).
200	cloneContents : function()
201	{
202		var docFrag = new FCKDocumentFragment( this._Document ) ;
203
204		if ( !this.collapsed )
205			this._ExecContentsAction( 2, docFrag ) ;
206
207		return docFrag ;
208	},
209
210	_ExecContentsAction : function( action, docFrag )
211	{
212		var startNode	= this.startContainer ;
213		var endNode		= this.endContainer ;
214
215		var startOffset	= this.startOffset ;
216		var endOffset	= this.endOffset ;
217
218		var removeStartNode	= false ;
219		var removeEndNode	= false ;
220
221		// Check the start and end nodes and make the necessary removals or changes.
222
223		// Start from the end, otherwise DOM mutations (splitText) made in the
224		// start boundary may interfere on the results here.
225
226		// For text containers, we must simply split the node and point to the
227		// second part. The removal will be handled by the rest of the code .
228		if ( endNode.nodeType == 3 )
229			endNode = endNode.splitText( endOffset ) ;
230		else
231		{
232			// If the end container has children and the offset is pointing
233			// to a child, then we should start from it.
234			if ( endNode.childNodes.length > 0 )
235			{
236				// If the offset points after the last node.
237				if ( endOffset > endNode.childNodes.length - 1 )
238				{
239					// Let's create a temporary node and mark it for removal.
240					endNode = FCKDomTools.InsertAfterNode( endNode.lastChild, this._Document.createTextNode('') ) ;
241					removeEndNode = true ;
242				}
243				else
244					endNode = endNode.childNodes[ endOffset ] ;
245			}
246		}
247
248		// For text containers, we must simply split the node. The removal will
249		// be handled by the rest of the code .
250		if ( startNode.nodeType == 3 )
251		{
252			startNode.splitText( startOffset ) ;
253
254			// In cases the end node is the same as the start node, the above
255			// splitting will also split the end, so me must move the end to
256			// the second part of the split.
257			if ( startNode == endNode )
258				endNode = startNode.nextSibling ;
259		}
260		else
261		{
262			// If the start container has children and the offset is pointing
263			// to a child, then we should start from its previous sibling.
264			if ( startNode.childNodes.length > 0 &&  startOffset <= startNode.childNodes.length - 1 )
265			{
266				// If the offset points to the first node, we don't have a
267				// sibling, so let's use the first one, but mark it for removal.
268				if ( startOffset == 0 )
269				{
270					// Let's create a temporary node and mark it for removal.
271					startNode = startNode.insertBefore( this._Document.createTextNode(''), startNode.firstChild ) ;
272					removeStartNode = true ;
273				}
274				else
275					startNode = startNode.childNodes[ startOffset ].previousSibling ;
276			}
277		}
278
279		// Get the parent nodes tree for the start and end boundaries.
280		var startParents	= FCKDomTools.GetParents( startNode ) ;
281		var endParents		= FCKDomTools.GetParents( endNode ) ;
282
283		// Compare them, to find the top most siblings.
284		var i, topStart, topEnd ;
285
286		for ( i = 0 ; i < startParents.length ; i++ )
287		{
288			topStart	= startParents[i] ;
289			topEnd		= endParents[i] ;
290
291			// The compared nodes will match until we find the top most
292			// siblings (different nodes that have the same parent).
293			// "i" will hold the index in the parents array for the top
294			// most element.
295			if ( topStart != topEnd )
296				break ;
297		}
298
299		var clone, levelStartNode, levelClone, currentNode, currentSibling ;
300
301		if ( docFrag )
302			clone = docFrag.RootNode ;
303
304		// Remove all successive sibling nodes for every node in the
305		// startParents tree.
306		for ( var j = i ; j < startParents.length ; j++ )
307		{
308			levelStartNode = startParents[j] ;
309
310			// For Extract and Clone, we must clone this level.
311			if ( clone && levelStartNode != startNode )		// action = 0 = Delete
312				levelClone = clone.appendChild( levelStartNode.cloneNode( levelStartNode == startNode ) ) ;
313
314			currentNode = levelStartNode.nextSibling ;
315
316			while( currentNode )
317			{
318				// Stop processing when the current node matches a node in the
319				// endParents tree or if it is the endNode.
320				if ( currentNode == endParents[j] || currentNode == endNode )
321					break ;
322
323				// Cache the next sibling.
324				currentSibling = currentNode.nextSibling ;
325
326				// If cloning, just clone it.
327				if ( action == 2 )	// 2 = Clone
328					clone.appendChild( currentNode.cloneNode( true ) ) ;
329				else
330				{
331					// Both Delete and Extract will remove the node.
332					currentNode.parentNode.removeChild( currentNode ) ;
333
334					// When Extracting, move the removed node to the docFrag.
335					if ( action == 1 )	// 1 = Extract
336						clone.appendChild( currentNode ) ;
337				}
338
339				currentNode = currentSibling ;
340			}
341
342			if ( clone )
343				clone = levelClone ;
344		}
345
346		if ( docFrag )
347			clone = docFrag.RootNode ;
348
349		// Remove all previous sibling nodes for every node in the
350		// endParents tree.
351		for ( var k = i ; k < endParents.length ; k++ )
352		{
353			levelStartNode = endParents[k] ;
354
355			// For Extract and Clone, we must clone this level.
356			if ( action > 0 && levelStartNode != endNode )		// action = 0 = Delete
357				levelClone = clone.appendChild( levelStartNode.cloneNode( levelStartNode == endNode ) ) ;
358
359			// The processing of siblings may have already been done by the parent.
360			if ( !startParents[k] || levelStartNode.parentNode != startParents[k].parentNode )
361			{
362				currentNode = levelStartNode.previousSibling ;
363
364				while( currentNode )
365				{
366					// Stop processing when the current node matches a node in the
367					// startParents tree or if it is the startNode.
368					if ( currentNode == startParents[k] || currentNode == startNode )
369						break ;
370
371					// Cache the next sibling.
372					currentSibling = currentNode.previousSibling ;
373
374					// If cloning, just clone it.
375					if ( action == 2 )	// 2 = Clone
376						clone.insertBefore( currentNode.cloneNode( true ), clone.firstChild ) ;
377					else
378					{
379						// Both Delete and Extract will remove the node.
380						currentNode.parentNode.removeChild( currentNode ) ;
381
382						// When Extracting, mode the removed node to the docFrag.
383						if ( action == 1 )	// 1 = Extract
384							clone.insertBefore( currentNode, clone.firstChild ) ;
385					}
386
387					currentNode = currentSibling ;
388				}
389			}
390
391			if ( clone )
392				clone = levelClone ;
393		}
394
395		if ( action == 2 )		// 2 = Clone.
396		{
397			// No changes in the DOM should be done, so fix the split text (if any).
398
399			var startTextNode = this.startContainer ;
400			if ( startTextNode.nodeType == 3 )
401			{
402				startTextNode.data += startTextNode.nextSibling.data ;
403				startTextNode.parentNode.removeChild( startTextNode.nextSibling ) ;
404			}
405
406			var endTextNode = this.endContainer ;
407			if ( endTextNode.nodeType == 3 && endTextNode.nextSibling )
408			{
409				endTextNode.data += endTextNode.nextSibling.data ;
410				endTextNode.parentNode.removeChild( endTextNode.nextSibling ) ;
411			}
412		}
413		else
414		{
415			// Collapse the range.
416
417			// If a node has been partially selected, collapse the range between
418			// topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
419			if ( topStart && topEnd && ( startNode.parentNode != topStart.parentNode || endNode.parentNode != topEnd.parentNode ) )
420			{
421				var endIndex = FCKDomTools.GetIndexOf( topEnd ) ;
422
423				// If the start node is to be removed, we must correct the
424				// index to reflect the removal.
425				if ( removeStartNode && topEnd.parentNode == startNode.parentNode )
426					endIndex-- ;
427
428				this.setStart( topEnd.parentNode, endIndex ) ;
429			}
430
431			// Collapse it to the start.
432			this.collapse( true ) ;
433		}
434
435		// Cleanup any marked node.
436		if( removeStartNode )
437			startNode.parentNode.removeChild( startNode ) ;
438
439		if( removeEndNode && endNode.parentNode )
440			endNode.parentNode.removeChild( endNode ) ;
441	},
442
443	cloneRange : function()
444	{
445		return FCKW3CRange.CreateFromRange( this._Document, this ) ;
446	}
447} ;
448