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