JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.0
[ckeditor.git] / _source / core / dom / range.js
1 /*\r
2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 CKEDITOR.dom.range = function( document )\r
7 {\r
8         this.startContainer     = null;\r
9         this.startOffset        = null;\r
10         this.endContainer       = null;\r
11         this.endOffset          = null;\r
12         this.collapsed          = true;\r
13 \r
14         this.document = document;\r
15 };\r
16 \r
17 (function()\r
18 {\r
19         // Updates the "collapsed" property for the given range object.\r
20         var updateCollapsed = function( range )\r
21         {\r
22                 range.collapsed = (\r
23                         range.startContainer &&\r
24                         range.endContainer &&\r
25                         range.startContainer.equals( range.endContainer ) &&\r
26                         range.startOffset == range.endOffset );\r
27         };\r
28 \r
29         // This is a shared function used to delete, extract and clone the range\r
30         // contents.\r
31         // V2\r
32         var execContentsAction = function( range, action, docFrag )\r
33         {\r
34                 range.optimizeBookmark();\r
35 \r
36                 var startNode   = range.startContainer;\r
37                 var endNode             = range.endContainer;\r
38 \r
39                 var startOffset = range.startOffset;\r
40                 var endOffset   = range.endOffset;\r
41 \r
42                 var removeStartNode;\r
43                 var removeEndNode;\r
44 \r
45                 // For text containers, we must simply split the node and point to the\r
46                 // second part. The removal will be handled by the rest of the code .\r
47                 if ( endNode.type == CKEDITOR.NODE_TEXT )\r
48                         endNode = endNode.split( endOffset );\r
49                 else\r
50                 {\r
51                         // If the end container has children and the offset is pointing\r
52                         // to a child, then we should start from it.\r
53                         if ( endNode.getChildCount() > 0 )\r
54                         {\r
55                                 // If the offset points after the last node.\r
56                                 if ( endOffset >= endNode.getChildCount() )\r
57                                 {\r
58                                         // Let's create a temporary node and mark it for removal.\r
59                                         endNode = endNode.append( range.document.createText( '' ) );\r
60                                         removeEndNode = true;\r
61                                 }\r
62                                 else\r
63                                         endNode = endNode.getChild( endOffset );\r
64                         }\r
65                 }\r
66 \r
67                 // For text containers, we must simply split the node. The removal will\r
68                 // be handled by the rest of the code .\r
69                 if ( startNode.type == CKEDITOR.NODE_TEXT )\r
70                 {\r
71                         startNode.split( startOffset );\r
72 \r
73                         // In cases the end node is the same as the start node, the above\r
74                         // splitting will also split the end, so me must move the end to\r
75                         // the second part of the split.\r
76                         if ( startNode.equals( endNode ) )\r
77                                 endNode = startNode.getNext();\r
78                 }\r
79                 else\r
80                 {\r
81                         // If the start container has children and the offset is pointing\r
82                         // to a child, then we should start from its previous sibling.\r
83 \r
84                         // If the offset points to the first node, we don't have a\r
85                         // sibling, so let's use the first one, but mark it for removal.\r
86                         if ( !startOffset )\r
87                         {\r
88                                 // Let's create a temporary node and mark it for removal.\r
89                                 startNode = startNode.getFirst().insertBeforeMe( range.document.createText( '' ) );\r
90                                 removeStartNode = true;\r
91                         }\r
92                         else if ( startOffset >= startNode.getChildCount() )\r
93                         {\r
94                                 // Let's create a temporary node and mark it for removal.\r
95                                 startNode = startNode.append( range.document.createText( '' ) );\r
96                                 removeStartNode = true;\r
97                         }\r
98                         else\r
99                                 startNode = startNode.getChild( startOffset ).getPrevious();\r
100                 }\r
101 \r
102                 // Get the parent nodes tree for the start and end boundaries.\r
103                 var startParents        = startNode.getParents();\r
104                 var endParents          = endNode.getParents();\r
105 \r
106                 // Compare them, to find the top most siblings.\r
107                 var i, topStart, topEnd;\r
108 \r
109                 for ( i = 0 ; i < startParents.length ; i++ )\r
110                 {\r
111                         topStart = startParents[ i ];\r
112                         topEnd = endParents[ i ];\r
113 \r
114                         // The compared nodes will match until we find the top most\r
115                         // siblings (different nodes that have the same parent).\r
116                         // "i" will hold the index in the parents array for the top\r
117                         // most element.\r
118                         if ( !topStart.equals( topEnd ) )\r
119                                 break;\r
120                 }\r
121 \r
122                 var clone = docFrag, levelStartNode, levelClone, currentNode, currentSibling;\r
123 \r
124                 // Remove all successive sibling nodes for every node in the\r
125                 // startParents tree.\r
126                 for ( var j = i ; j < startParents.length ; j++ )\r
127                 {\r
128                         levelStartNode = startParents[j];\r
129 \r
130                         // For Extract and Clone, we must clone this level.\r
131                         if ( clone && !levelStartNode.equals( startNode ) )             // action = 0 = Delete\r
132                                 levelClone = clone.append( levelStartNode.clone() );\r
133 \r
134                         currentNode = levelStartNode.getNext();\r
135 \r
136                         while( currentNode )\r
137                         {\r
138                                 // Stop processing when the current node matches a node in the\r
139                                 // endParents tree or if it is the endNode.\r
140                                 if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )\r
141                                         break;\r
142 \r
143                                 // Cache the next sibling.\r
144                                 currentSibling = currentNode.getNext();\r
145 \r
146                                 // If cloning, just clone it.\r
147                                 if ( action == 2 )      // 2 = Clone\r
148                                         clone.append( currentNode.clone( true ) );\r
149                                 else\r
150                                 {\r
151                                         // Both Delete and Extract will remove the node.\r
152                                         currentNode.remove();\r
153 \r
154                                         // When Extracting, move the removed node to the docFrag.\r
155                                         if ( action == 1 )      // 1 = Extract\r
156                                                 clone.append( currentNode );\r
157                                 }\r
158 \r
159                                 currentNode = currentSibling;\r
160                         }\r
161 \r
162                         if ( clone )\r
163                                 clone = levelClone;\r
164                 }\r
165 \r
166                 clone = docFrag;\r
167 \r
168                 // Remove all previous sibling nodes for every node in the\r
169                 // endParents tree.\r
170                 for ( var k = i ; k < endParents.length ; k++ )\r
171                 {\r
172                         levelStartNode = endParents[ k ];\r
173 \r
174                         // For Extract and Clone, we must clone this level.\r
175                         if ( action > 0 && !levelStartNode.equals( endNode ) )          // action = 0 = Delete\r
176                                 levelClone = clone.append( levelStartNode.clone() );\r
177 \r
178                         // The processing of siblings may have already been done by the parent.\r
179                         if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode )\r
180                         {\r
181                                 currentNode = levelStartNode.getPrevious();\r
182 \r
183                                 while( currentNode )\r
184                                 {\r
185                                         // Stop processing when the current node matches a node in the\r
186                                         // startParents tree or if it is the startNode.\r
187                                         if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )\r
188                                                 break;\r
189 \r
190                                         // Cache the next sibling.\r
191                                         currentSibling = currentNode.getPrevious();\r
192 \r
193                                         // If cloning, just clone it.\r
194                                         if ( action == 2 )      // 2 = Clone\r
195                                                 clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild ) ;\r
196                                         else\r
197                                         {\r
198                                                 // Both Delete and Extract will remove the node.\r
199                                                 currentNode.remove();\r
200 \r
201                                                 // When Extracting, mode the removed node to the docFrag.\r
202                                                 if ( action == 1 )      // 1 = Extract\r
203                                                         clone.$.insertBefore( currentNode.$, clone.$.firstChild );\r
204                                         }\r
205 \r
206                                         currentNode = currentSibling;\r
207                                 }\r
208                         }\r
209 \r
210                         if ( clone )\r
211                                 clone = levelClone;\r
212                 }\r
213 \r
214                 if ( action == 2 )              // 2 = Clone.\r
215                 {\r
216                         // No changes in the DOM should be done, so fix the split text (if any).\r
217 \r
218                         var startTextNode = range.startContainer;\r
219                         if ( startTextNode.type == CKEDITOR.NODE_TEXT )\r
220                         {\r
221                                 startTextNode.$.data += startTextNode.$.nextSibling.data;\r
222                                 startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );\r
223                         }\r
224 \r
225                         var endTextNode = range.endContainer;\r
226                         if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling )\r
227                         {\r
228                                 endTextNode.$.data += endTextNode.$.nextSibling.data;\r
229                                 endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );\r
230                         }\r
231                 }\r
232                 else\r
233                 {\r
234                         // Collapse the range.\r
235 \r
236                         // If a node has been partially selected, collapse the range between\r
237                         // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).\r
238                         if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) )\r
239                         {\r
240                                 var endIndex = topEnd.getIndex();\r
241 \r
242                                 // If the start node is to be removed, we must correct the\r
243                                 // index to reflect the removal.\r
244                                 if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )\r
245                                         endIndex--;\r
246 \r
247                                 range.setStart( topEnd.getParent(), endIndex );\r
248                         }\r
249 \r
250                         // Collapse it to the start.\r
251                         range.collapse( true );\r
252                 }\r
253 \r
254                 // Cleanup any marked node.\r
255                 if( removeStartNode )\r
256                         startNode.remove();\r
257 \r
258                 if( removeEndNode && endNode.$.parentNode )\r
259                         endNode.remove();\r
260         };\r
261 \r
262         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 };\r
263 \r
264         // Creates the appropriate node evaluator for the dom walker used inside\r
265         // check(Start|End)OfBlock.\r
266         function getCheckStartEndBlockEvalFunction( isStart )\r
267         {\r
268                 var hadBr = false, bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true );\r
269                 return function( node )\r
270                 {\r
271                         // First ignore bookmark nodes.\r
272                         if ( bookmarkEvaluator( node ) )\r
273                                 return true;\r
274 \r
275                         if ( node.type == CKEDITOR.NODE_TEXT )\r
276                         {\r
277                                 // If there's any visible text, then we're not at the start.\r
278                                 if ( CKEDITOR.tools.trim( node.getText() ).length )\r
279                                         return false;\r
280                                 }\r
281                         else\r
282                         {\r
283                                 // If there are non-empty inline elements (e.g. <img />), then we're not\r
284                                 // at the start.\r
285                                 if ( !inlineChildReqElements[ node.getName() ] )\r
286                                 {\r
287                                         // If we're working at the end-of-block, forgive the first <br /> in non-IE\r
288                                         // browsers.\r
289                                         if ( !isStart && !CKEDITOR.env.ie && node.getName() == 'br' && !hadBr )\r
290                                                 hadBr = true;\r
291                                         else\r
292                                                 return false;\r
293                                 }\r
294                         }\r
295                         return true;\r
296                 };\r
297         }\r
298 \r
299         // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any\r
300         // text node and non-empty elements unless it's being bookmark text.\r
301         function elementBoundaryEval( node )\r
302         {\r
303                 // Reject any text node unless it's being bookmark\r
304                 // OR it's spaces. (#3883)\r
305                 return node.type != CKEDITOR.NODE_TEXT\r
306                             && node.getName() in CKEDITOR.dtd.$removeEmpty\r
307                             || !CKEDITOR.tools.trim( node.getText() )\r
308                             || node.getParent().hasAttribute( '_fck_bookmark' );\r
309         }\r
310 \r
311         CKEDITOR.dom.range.prototype =\r
312         {\r
313                 clone : function()\r
314                 {\r
315                         var clone = new CKEDITOR.dom.range( this.document );\r
316 \r
317                         clone.startContainer = this.startContainer;\r
318                         clone.startOffset = this.startOffset;\r
319                         clone.endContainer = this.endContainer;\r
320                         clone.endOffset = this.endOffset;\r
321                         clone.collapsed = this.collapsed;\r
322 \r
323                         return clone;\r
324                 },\r
325 \r
326                 collapse : function( toStart )\r
327                 {\r
328                         if ( toStart )\r
329                         {\r
330                                 this.endContainer       = this.startContainer;\r
331                                 this.endOffset          = this.startOffset;\r
332                         }\r
333                         else\r
334                         {\r
335                                 this.startContainer     = this.endContainer;\r
336                                 this.startOffset        = this.endOffset;\r
337                         }\r
338 \r
339                         this.collapsed = true;\r
340                 },\r
341 \r
342                 // The selection may be lost when cloning (due to the splitText() call).\r
343                 cloneContents : function()\r
344                 {\r
345                         var docFrag = new CKEDITOR.dom.documentFragment( this.document );\r
346 \r
347                         if ( !this.collapsed )\r
348                                 execContentsAction( this, 2, docFrag );\r
349 \r
350                         return docFrag;\r
351                 },\r
352 \r
353                 deleteContents : function()\r
354                 {\r
355                         if ( this.collapsed )\r
356                                 return;\r
357 \r
358                         execContentsAction( this, 0 );\r
359                 },\r
360 \r
361                 extractContents : function()\r
362                 {\r
363                         var docFrag = new CKEDITOR.dom.documentFragment( this.document );\r
364 \r
365                         if ( !this.collapsed )\r
366                                 execContentsAction( this, 1, docFrag );\r
367 \r
368                         return docFrag;\r
369                 },\r
370 \r
371                 /**\r
372                  * Creates a bookmark object, which can be later used to restore the\r
373                  * range by using the moveToBookmark function.\r
374                  * This is an "intrusive" way to create a bookmark. It includes <span> tags\r
375                  * in the range boundaries. The advantage of it is that it is possible to\r
376                  * handle DOM mutations when moving back to the bookmark.\r
377                  * Attention: the inclusion of nodes in the DOM is a design choice and\r
378                  * should not be changed as there are other points in the code that may be\r
379                  * using those nodes to perform operations. See GetBookmarkNode.\r
380                  * @param {Boolean} [serializable] Indicates that the bookmark nodes\r
381                  *              must contain ids, which can be used to restore the range even\r
382                  *              when these nodes suffer mutations (like a clonation or innerHTML\r
383                  *              change).\r
384                  * @returns {Object} And object representing a bookmark.\r
385                  */\r
386                 createBookmark : function( serializable )\r
387                 {\r
388                         var startNode, endNode;\r
389                         var baseId;\r
390                         var clone;\r
391 \r
392                         startNode = this.document.createElement( 'span' );\r
393                         startNode.setAttribute( '_fck_bookmark', 1 );\r
394                         startNode.setStyle( 'display', 'none' );\r
395 \r
396                         // For IE, it must have something inside, otherwise it may be\r
397                         // removed during DOM operations.\r
398                         startNode.setHtml( '&nbsp;' );\r
399 \r
400                         if ( serializable )\r
401                         {\r
402                                 baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();\r
403                                 startNode.setAttribute( 'id', baseId + 'S' );\r
404                         }\r
405 \r
406                         // If collapsed, the endNode will not be created.\r
407                         if ( !this.collapsed )\r
408                         {\r
409                                 endNode = startNode.clone();\r
410                                 endNode.setHtml( '&nbsp;' );\r
411 \r
412                                 if ( serializable )\r
413                                         endNode.setAttribute( 'id', baseId + 'E' );\r
414 \r
415                                 clone = this.clone();\r
416                                 clone.collapse();\r
417                                 clone.insertNode( endNode );\r
418                         }\r
419 \r
420                         clone = this.clone();\r
421                         clone.collapse( true );\r
422                         clone.insertNode( startNode );\r
423 \r
424                         // Update the range position.\r
425                         if ( endNode )\r
426                         {\r
427                                 this.setStartAfter( startNode );\r
428                                 this.setEndBefore( endNode );\r
429                         }\r
430                         else\r
431                                 this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );\r
432 \r
433                         return {\r
434                                 startNode : serializable ? baseId + 'S' : startNode,\r
435                                 endNode : serializable ? baseId + 'E' : endNode,\r
436                                 serializable : serializable\r
437                         };\r
438                 },\r
439 \r
440                 /**\r
441                  * Creates a "non intrusive" and "mutation sensible" bookmark. This\r
442                  * kind of bookmark should be used only when the DOM is supposed to\r
443                  * remain stable after its creation.\r
444                  * @param {Boolean} [normalized] Indicates that the bookmark must\r
445                  *              normalized. When normalized, the successive text nodes are\r
446                  *              considered a single node. To sucessful load a normalized\r
447                  *              bookmark, the DOM tree must be also normalized before calling\r
448                  *              moveToBookmark.\r
449                  * @returns {Object} An object representing the bookmark.\r
450                  */\r
451                 createBookmark2 : function( normalized )\r
452                 {\r
453                         var startContainer      = this.startContainer,\r
454                                 endContainer    = this.endContainer;\r
455 \r
456                         var startOffset = this.startOffset,\r
457                                 endOffset       = this.endOffset;\r
458 \r
459                         var child, previous;\r
460 \r
461                         // If there is no range then get out of here.\r
462                         // It happens on initial load in Safari #962 and if the editor it's\r
463                         // hidden also in Firefox\r
464                         if ( !startContainer || !endContainer )\r
465                                 return { start : 0, end : 0 };\r
466 \r
467                         if ( normalized )\r
468                         {\r
469                                 // Find out if the start is pointing to a text node that will\r
470                                 // be normalized.\r
471                                 if ( startContainer.type == CKEDITOR.NODE_ELEMENT )\r
472                                 {\r
473                                         child = startContainer.getChild( startOffset );\r
474 \r
475                                         // In this case, move the start information to that text\r
476                                         // node.\r
477                                         if ( child && child.type == CKEDITOR.NODE_TEXT\r
478                                                         && startOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )\r
479                                         {\r
480                                                 startContainer = child;\r
481                                                 startOffset = 0;\r
482                                         }\r
483                                 }\r
484 \r
485                                 // Normalize the start.\r
486                                 while ( startContainer.type == CKEDITOR.NODE_TEXT\r
487                                                 && ( previous = startContainer.getPrevious() )\r
488                                                 && previous.type == CKEDITOR.NODE_TEXT )\r
489                                 {\r
490                                         startContainer = previous;\r
491                                         startOffset += previous.getLength();\r
492                                 }\r
493 \r
494                                 // Process the end only if not normalized.\r
495                                 if ( !this.isCollapsed )\r
496                                 {\r
497                                         // Find out if the start is pointing to a text node that\r
498                                         // will be normalized.\r
499                                         if ( endContainer.type == CKEDITOR.NODE_ELEMENT )\r
500                                         {\r
501                                                 child = endContainer.getChild( endOffset );\r
502 \r
503                                                 // In this case, move the start information to that\r
504                                                 // text node.\r
505                                                 if ( child && child.type == CKEDITOR.NODE_TEXT\r
506                                                                 && endOffset > 0 && child.getPrevious().type == CKEDITOR.NODE_TEXT )\r
507                                                 {\r
508                                                         endContainer = child;\r
509                                                         endOffset = 0;\r
510                                                 }\r
511                                         }\r
512 \r
513                                         // Normalize the end.\r
514                                         while ( endContainer.type == CKEDITOR.NODE_TEXT\r
515                                                         && ( previous = endContainer.getPrevious() )\r
516                                                         && previous.type == CKEDITOR.NODE_TEXT )\r
517                                         {\r
518                                                 endContainer = previous;\r
519                                                 endOffset += previous.getLength();\r
520                                         }\r
521                                 }\r
522                         }\r
523 \r
524                         return {\r
525                                 start           : startContainer.getAddress( normalized ),\r
526                                 end                     : this.isCollapsed ? null : endContainer.getAddress( normalized ),\r
527                                 startOffset     : startOffset,\r
528                                 endOffset       : endOffset,\r
529                                 normalized      : normalized,\r
530                                 is2                     : true          // It's a createBookmark2 bookmark.\r
531                         };\r
532                 },\r
533 \r
534                 moveToBookmark : function( bookmark )\r
535                 {\r
536                         if ( bookmark.is2 )             // Created with createBookmark2().\r
537                         {\r
538                                 // Get the start information.\r
539                                 var startContainer      = this.document.getByAddress( bookmark.start, bookmark.normalized ),\r
540                                         startOffset     = bookmark.startOffset;\r
541 \r
542                                 // Get the end information.\r
543                                 var endContainer        = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),\r
544                                         endOffset       = bookmark.endOffset;\r
545 \r
546                                 // Set the start boundary.\r
547                                 this.setStart( startContainer, startOffset );\r
548 \r
549                                 // Set the end boundary. If not available, collapse it.\r
550                                 if ( endContainer )\r
551                                         this.setEnd( endContainer, endOffset );\r
552                                 else\r
553                                         this.collapse( true );\r
554                         }\r
555                         else                                    // Created with createBookmark().\r
556                         {\r
557                                 var serializable = bookmark.serializable,\r
558                                         startNode       = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,\r
559                                         endNode         = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;\r
560 \r
561                                 // Set the range start at the bookmark start node position.\r
562                                 this.setStartBefore( startNode );\r
563 \r
564                                 // Remove it, because it may interfere in the setEndBefore call.\r
565                                 startNode.remove();\r
566 \r
567                                 // Set the range end at the bookmark end node position, or simply\r
568                                 // collapse it if it is not available.\r
569                                 if ( endNode )\r
570                                 {\r
571                                         this.setEndBefore( endNode );\r
572                                         endNode.remove();\r
573                                 }\r
574                                 else\r
575                                         this.collapse( true );\r
576                         }\r
577                 },\r
578 \r
579                 getBoundaryNodes : function()\r
580                 {\r
581                         var startNode = this.startContainer,\r
582                                 endNode = this.endContainer,\r
583                                 startOffset = this.startOffset,\r
584                                 endOffset = this.endOffset,\r
585                                 childCount;\r
586 \r
587                         if ( startNode.type == CKEDITOR.NODE_ELEMENT )\r
588                         {\r
589                                 childCount = startNode.getChildCount();\r
590                                 if ( childCount > startOffset )\r
591                                         startNode = startNode.getChild( startOffset );\r
592                                 else if ( childCount < 1 )\r
593                                         startNode = startNode.getPreviousSourceNode();\r
594                                 else            // startOffset > childCount but childCount is not 0\r
595                                 {\r
596                                         // Try to take the node just after the current position.\r
597                                         startNode = startNode.$;\r
598                                         while ( startNode.lastChild )\r
599                                                 startNode = startNode.lastChild;\r
600                                         startNode = new CKEDITOR.dom.node( startNode );\r
601 \r
602                                         // Normally we should take the next node in DFS order. But it\r
603                                         // is also possible that we've already reached the end of\r
604                                         // document.\r
605                                         startNode = startNode.getNextSourceNode() || startNode;\r
606                                 }\r
607                         }\r
608                         if ( endNode.type == CKEDITOR.NODE_ELEMENT )\r
609                         {\r
610                                 childCount = endNode.getChildCount();\r
611                                 if ( childCount > endOffset )\r
612                                         endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );\r
613                                 else if ( childCount < 1 )\r
614                                         endNode = endNode.getPreviousSourceNode();\r
615                                 else            // endOffset > childCount but childCount is not 0\r
616                                 {\r
617                                         // Try to take the node just before the current position.\r
618                                         endNode = endNode.$;\r
619                                         while ( endNode.lastChild )\r
620                                                 endNode = endNode.lastChild;\r
621                                         endNode = new CKEDITOR.dom.node( endNode );\r
622                                 }\r
623                         }\r
624 \r
625                         // Sometimes the endNode will come right before startNode for collapsed\r
626                         // ranges. Fix it. (#3780)\r
627                         if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )\r
628                                 startNode = endNode;\r
629 \r
630                         return { startNode : startNode, endNode : endNode };\r
631                 },\r
632 \r
633                 /**\r
634                  * Find the node which fully contains the range.\r
635                  * @param includeSelf\r
636                  * @param {Boolean} ignoreTextNode Whether ignore CKEDITOR.NODE_TEXT type.\r
637                  */\r
638                 getCommonAncestor : function( includeSelf , ignoreTextNode )\r
639                 {\r
640                         var start = this.startContainer,\r
641                                 end = this.endContainer,\r
642                                 ancestor;\r
643 \r
644                         if ( start.equals( end ) )\r
645                         {\r
646                                 if ( includeSelf\r
647                                                 && start.type == CKEDITOR.NODE_ELEMENT\r
648                                                 && this.startOffset == this.endOffset - 1 )\r
649                                         ancestor = start.getChild( this.startOffset );\r
650                                 else\r
651                                         ancestor = start;\r
652                         }\r
653                         else\r
654                                 ancestor = start.getCommonAncestor( end );\r
655 \r
656                         return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;\r
657                 },\r
658 \r
659                 /**\r
660                  * Transforms the startContainer and endContainer properties from text\r
661                  * nodes to element nodes, whenever possible. This is actually possible\r
662                  * if either of the boundary containers point to a text node, and its\r
663                  * offset is set to zero, or after the last char in the node.\r
664                  */\r
665                 optimize : function()\r
666                 {\r
667                         var container = this.startContainer;\r
668                         var offset = this.startOffset;\r
669 \r
670                         if ( container.type != CKEDITOR.NODE_ELEMENT )\r
671                         {\r
672                                 if ( !offset )\r
673                                         this.setStartBefore( container );\r
674                                 else if ( offset >= container.getLength() )\r
675                                         this.setStartAfter( container );\r
676                         }\r
677 \r
678                         container = this.endContainer;\r
679                         offset = this.endOffset;\r
680 \r
681                         if ( container.type != CKEDITOR.NODE_ELEMENT )\r
682                         {\r
683                                 if ( !offset )\r
684                                         this.setEndBefore( container );\r
685                                 else if ( offset >= container.getLength() )\r
686                                         this.setEndAfter( container );\r
687                         }\r
688                 },\r
689 \r
690                 /**\r
691                  * Move the range out of bookmark nodes if they're been the container.\r
692                  */\r
693                 optimizeBookmark: function()\r
694                 {\r
695                         var startNode = this.startContainer,\r
696                                 endNode = this.endContainer;\r
697 \r
698                         if ( startNode.is && startNode.is( 'span' )\r
699                                 && startNode.hasAttribute( '_fck_bookmark' ) )\r
700                                 this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );\r
701                         if ( endNode && endNode.is && endNode.is( 'span' )\r
702                                 && endNode.hasAttribute( '_fck_bookmark' ) )\r
703                                 this.setEndAt( endNode,  CKEDITOR.POSITION_AFTER_END );\r
704                 },\r
705 \r
706                 trim : function( ignoreStart, ignoreEnd )\r
707                 {\r
708                         var startContainer = this.startContainer,\r
709                                 startOffset = this.startOffset,\r
710                                 collapsed = this.collapsed;\r
711                         if ( ( !ignoreStart || collapsed )\r
712                                  && startContainer && startContainer.type == CKEDITOR.NODE_TEXT )\r
713                         {\r
714                                 // If the offset is zero, we just insert the new node before\r
715                                 // the start.\r
716                                 if ( !startOffset )\r
717                                 {\r
718                                         startOffset = startContainer.getIndex();\r
719                                         startContainer = startContainer.getParent();\r
720                                 }\r
721                                 // If the offset is at the end, we'll insert it after the text\r
722                                 // node.\r
723                                 else if ( startOffset >= startContainer.getLength() )\r
724                                 {\r
725                                         startOffset = startContainer.getIndex() + 1;\r
726                                         startContainer = startContainer.getParent();\r
727                                 }\r
728                                 // In other case, we split the text node and insert the new\r
729                                 // node at the split point.\r
730                                 else\r
731                                 {\r
732                                         var nextText = startContainer.split( startOffset );\r
733 \r
734                                         startOffset = startContainer.getIndex() + 1;\r
735                                         startContainer = startContainer.getParent();\r
736                                         // Check if it is necessary to update the end boundary.\r
737                                         if ( !collapsed && this.startContainer.equals( this.endContainer ) )\r
738                                                 this.setEnd( nextText, this.endOffset - this.startOffset );\r
739                                 }\r
740 \r
741                                 this.setStart( startContainer, startOffset );\r
742 \r
743                                 if ( collapsed )\r
744                                         this.collapse( true );\r
745                         }\r
746 \r
747                         var endContainer = this.endContainer;\r
748                         var endOffset = this.endOffset;\r
749 \r
750                         if ( !( ignoreEnd || collapsed )\r
751                                  && endContainer && endContainer.type == CKEDITOR.NODE_TEXT )\r
752                         {\r
753                                 // If the offset is zero, we just insert the new node before\r
754                                 // the start.\r
755                                 if ( !endOffset )\r
756                                 {\r
757                                         endOffset = endContainer.getIndex();\r
758                                         endContainer = endContainer.getParent();\r
759                                 }\r
760                                 // If the offset is at the end, we'll insert it after the text\r
761                                 // node.\r
762                                 else if ( endOffset >= endContainer.getLength() )\r
763                                 {\r
764                                         endOffset = endContainer.getIndex() + 1;\r
765                                         endContainer = endContainer.getParent();\r
766                                 }\r
767                                 // In other case, we split the text node and insert the new\r
768                                 // node at the split point.\r
769                                 else\r
770                                 {\r
771                                         endContainer.split( endOffset );\r
772 \r
773                                         endOffset = endContainer.getIndex() + 1;\r
774                                         endContainer = endContainer.getParent();\r
775                                 }\r
776 \r
777                                 this.setEnd( endContainer, endOffset );\r
778                         }\r
779                 },\r
780 \r
781                 enlarge : function( unit )\r
782                 {\r
783                         switch ( unit )\r
784                         {\r
785                                 case CKEDITOR.ENLARGE_ELEMENT :\r
786 \r
787                                         if ( this.collapsed )\r
788                                                 return;\r
789 \r
790                                         // Get the common ancestor.\r
791                                         var commonAncestor = this.getCommonAncestor();\r
792 \r
793                                         var body = this.document.getBody();\r
794 \r
795                                         // For each boundary\r
796                                         //              a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.\r
797                                         //              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.\r
798 \r
799                                         var startTop, endTop;\r
800 \r
801                                         var enlargeable, sibling, commonReached;\r
802 \r
803                                         // Indicates that the node can be added only if whitespace\r
804                                         // is available before it.\r
805                                         var needsWhiteSpace = false;\r
806                                         var isWhiteSpace;\r
807                                         var siblingText;\r
808 \r
809                                         // Process the start boundary.\r
810 \r
811                                         var container = this.startContainer;\r
812                                         var offset = this.startOffset;\r
813 \r
814                                         if ( container.type == CKEDITOR.NODE_TEXT )\r
815                                         {\r
816                                                 if ( offset )\r
817                                                 {\r
818                                                         // Check if there is any non-space text before the\r
819                                                         // offset. Otherwise, container is null.\r
820                                                         container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;\r
821 \r
822                                                         // If we found only whitespace in the node, it\r
823                                                         // means that we'll need more whitespace to be able\r
824                                                         // to expand. For example, <i> can be expanded in\r
825                                                         // "A <i> [B]</i>", but not in "A<i> [B]</i>".\r
826                                                         needsWhiteSpace = !!container;\r
827                                                 }\r
828 \r
829                                                 if ( container )\r
830                                                 {\r
831                                                         if ( !( sibling = container.getPrevious() ) )\r
832                                                                 enlargeable = container.getParent();\r
833                                                 }\r
834                                         }\r
835                                         else\r
836                                         {\r
837                                                 // If we have offset, get the node preceeding it as the\r
838                                                 // first sibling to be checked.\r
839                                                 if ( offset )\r
840                                                         sibling = container.getChild( offset - 1 ) || container.getLast();\r
841 \r
842                                                 // If there is no sibling, mark the container to be\r
843                                                 // enlarged.\r
844                                                 if ( !sibling )\r
845                                                         enlargeable = container;\r
846                                         }\r
847 \r
848                                         while ( enlargeable || sibling )\r
849                                         {\r
850                                                 if ( enlargeable && !sibling )\r
851                                                 {\r
852                                                         // If we reached the common ancestor, mark the flag\r
853                                                         // for it.\r
854                                                         if ( !commonReached && enlargeable.equals( commonAncestor ) )\r
855                                                                 commonReached = true;\r
856 \r
857                                                         if ( !body.contains( enlargeable ) )\r
858                                                                 break;\r
859 \r
860                                                         // If we don't need space or this element breaks\r
861                                                         // the line, then enlarge it.\r
862                                                         if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )\r
863                                                         {\r
864                                                                 needsWhiteSpace = false;\r
865 \r
866                                                                 // If the common ancestor has been reached,\r
867                                                                 // we'll not enlarge it immediately, but just\r
868                                                                 // mark it to be enlarged later if the end\r
869                                                                 // boundary also enlarges it.\r
870                                                                 if ( commonReached )\r
871                                                                         startTop = enlargeable;\r
872                                                                 else\r
873                                                                         this.setStartBefore( enlargeable );\r
874                                                         }\r
875 \r
876                                                         sibling = enlargeable.getPrevious();\r
877                                                 }\r
878 \r
879                                                 // Check all sibling nodes preceeding the enlargeable\r
880                                                 // node. The node wil lbe enlarged only if none of them\r
881                                                 // blocks it.\r
882                                                 while ( sibling )\r
883                                                 {\r
884                                                         // This flag indicates that this node has\r
885                                                         // whitespaces at the end.\r
886                                                         isWhiteSpace = false;\r
887 \r
888                                                         if ( sibling.type == CKEDITOR.NODE_TEXT )\r
889                                                         {\r
890                                                                 siblingText = sibling.getText();\r
891 \r
892                                                                 if ( /[^\s\ufeff]/.test( siblingText ) )\r
893                                                                         sibling = null;\r
894 \r
895                                                                 isWhiteSpace = /[\s\ufeff]$/.test( siblingText );\r
896                                                         }\r
897                                                         else\r
898                                                         {\r
899                                                                 // If this is a visible element.\r
900                                                                 // We need to check for the bookmark attribute because IE insists on\r
901                                                                 // rendering the display:none nodes we use for bookmarks. (#3363)\r
902                                                                 if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) )\r
903                                                                 {\r
904                                                                         // We'll accept it only if we need\r
905                                                                         // whitespace, and this is an inline\r
906                                                                         // element with whitespace only.\r
907                                                                         if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )\r
908                                                                         {\r
909                                                                                 // It must contains spaces and inline elements only.\r
910 \r
911                                                                                 siblingText = sibling.getText();\r
912 \r
913                                                                                 if ( !(/[^\s\ufeff]/).test( siblingText ) )     // Spaces + Zero Width No-Break Space (U+FEFF)\r
914                                                                                         sibling = null;\r
915                                                                                 else\r
916                                                                                 {\r
917                                                                                         var allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
918                                                                                         for ( var i = 0, child ; child = allChildren[ i++ ] ; )\r
919                                                                                         {\r
920                                                                                                 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
921                                                                                                 {\r
922                                                                                                         sibling = null;\r
923                                                                                                         break;\r
924                                                                                                 }\r
925                                                                                         }\r
926                                                                                 }\r
927 \r
928                                                                                 if ( sibling )\r
929                                                                                         isWhiteSpace = !!siblingText.length;\r
930                                                                         }\r
931                                                                         else\r
932                                                                                 sibling = null;\r
933                                                                 }\r
934                                                         }\r
935 \r
936                                                         // A node with whitespaces has been found.\r
937                                                         if ( isWhiteSpace )\r
938                                                         {\r
939                                                                 // Enlarge the last enlargeable node, if we\r
940                                                                 // were waiting for spaces.\r
941                                                                 if ( needsWhiteSpace )\r
942                                                                 {\r
943                                                                         if ( commonReached )\r
944                                                                                 startTop = enlargeable;\r
945                                                                         else if ( enlargeable )\r
946                                                                                 this.setStartBefore( enlargeable );\r
947                                                                 }\r
948                                                                 else\r
949                                                                         needsWhiteSpace = true;\r
950                                                         }\r
951 \r
952                                                         if ( sibling )\r
953                                                         {\r
954                                                                 var next = sibling.getPrevious();\r
955 \r
956                                                                 if ( !enlargeable && !next )\r
957                                                                 {\r
958                                                                         // Set the sibling as enlargeable, so it's\r
959                                                                         // parent will be get later outside this while.\r
960                                                                         enlargeable = sibling;\r
961                                                                         sibling = null;\r
962                                                                         break;\r
963                                                                 }\r
964 \r
965                                                                 sibling = next;\r
966                                                         }\r
967                                                         else\r
968                                                         {\r
969                                                                 // If sibling has been set to null, then we\r
970                                                                 // need to stop enlarging.\r
971                                                                 enlargeable = null;\r
972                                                         }\r
973                                                 }\r
974 \r
975                                                 if ( enlargeable )\r
976                                                         enlargeable = enlargeable.getParent();\r
977                                         }\r
978 \r
979                                         // Process the end boundary. This is basically the same\r
980                                         // code used for the start boundary, with small changes to\r
981                                         // make it work in the oposite side (to the right). This\r
982                                         // makes it difficult to reuse the code here. So, fixes to\r
983                                         // the above code are likely to be replicated here.\r
984 \r
985                                         container = this.endContainer;\r
986                                         offset = this.endOffset;\r
987 \r
988                                         // Reset the common variables.\r
989                                         enlargeable = sibling = null;\r
990                                         commonReached = needsWhiteSpace = false;\r
991 \r
992                                         if ( container.type == CKEDITOR.NODE_TEXT )\r
993                                         {\r
994                                                 // Check if there is any non-space text after the\r
995                                                 // offset. Otherwise, container is null.\r
996                                                 container = !CKEDITOR.tools.trim( container.substring( offset ) ).length && container;\r
997 \r
998                                                 // If we found only whitespace in the node, it\r
999                                                 // means that we'll need more whitespace to be able\r
1000                                                 // to expand. For example, <i> can be expanded in\r
1001                                                 // "A <i> [B]</i>", but not in "A<i> [B]</i>".\r
1002                                                 needsWhiteSpace = !( container && container.getLength() );\r
1003 \r
1004                                                 if ( container )\r
1005                                                 {\r
1006                                                         if ( !( sibling = container.getNext() ) )\r
1007                                                                 enlargeable = container.getParent();\r
1008                                                 }\r
1009                                         }\r
1010                                         else\r
1011                                         {\r
1012                                                 // Get the node right after the boudary to be checked\r
1013                                                 // first.\r
1014                                                 sibling = container.getChild( offset );\r
1015 \r
1016                                                 if ( !sibling )\r
1017                                                         enlargeable = container;\r
1018                                         }\r
1019 \r
1020                                         while ( enlargeable || sibling )\r
1021                                         {\r
1022                                                 if ( enlargeable && !sibling )\r
1023                                                 {\r
1024                                                         if ( !commonReached && enlargeable.equals( commonAncestor ) )\r
1025                                                                 commonReached = true;\r
1026 \r
1027                                                         if ( !body.contains( enlargeable ) )\r
1028                                                                 break;\r
1029 \r
1030                                                         if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' )\r
1031                                                         {\r
1032                                                                 needsWhiteSpace = false;\r
1033 \r
1034                                                                 if ( commonReached )\r
1035                                                                         endTop = enlargeable;\r
1036                                                                 else if ( enlargeable )\r
1037                                                                         this.setEndAfter( enlargeable );\r
1038                                                         }\r
1039 \r
1040                                                         sibling = enlargeable.getNext();\r
1041                                                 }\r
1042 \r
1043                                                 while ( sibling )\r
1044                                                 {\r
1045                                                         isWhiteSpace = false;\r
1046 \r
1047                                                         if ( sibling.type == CKEDITOR.NODE_TEXT )\r
1048                                                         {\r
1049                                                                 siblingText = sibling.getText();\r
1050 \r
1051                                                                 if ( /[^\s\ufeff]/.test( siblingText ) )\r
1052                                                                         sibling = null;\r
1053 \r
1054                                                                 isWhiteSpace = /^[\s\ufeff]/.test( siblingText );\r
1055                                                         }\r
1056                                                         else\r
1057                                                         {\r
1058                                                                 // If this is a visible element.\r
1059                                                                 // We need to check for the bookmark attribute because IE insists on\r
1060                                                                 // rendering the display:none nodes we use for bookmarks. (#3363)\r
1061                                                                 if ( sibling.$.offsetWidth > 0 && !sibling.getAttribute( '_fck_bookmark' ) )\r
1062                                                                 {\r
1063                                                                         // We'll accept it only if we need\r
1064                                                                         // whitespace, and this is an inline\r
1065                                                                         // element with whitespace only.\r
1066                                                                         if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] )\r
1067                                                                         {\r
1068                                                                                 // It must contains spaces and inline elements only.\r
1069 \r
1070                                                                                 siblingText = sibling.getText();\r
1071 \r
1072                                                                                 if ( !(/[^\s\ufeff]/).test( siblingText ) )\r
1073                                                                                         sibling = null;\r
1074                                                                                 else\r
1075                                                                                 {\r
1076                                                                                         allChildren = sibling.$.all || sibling.$.getElementsByTagName( '*' );\r
1077                                                                                         for ( i = 0 ; child = allChildren[ i++ ] ; )\r
1078                                                                                         {\r
1079                                                                                                 if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] )\r
1080                                                                                                 {\r
1081                                                                                                         sibling = null;\r
1082                                                                                                         break;\r
1083                                                                                                 }\r
1084                                                                                         }\r
1085                                                                                 }\r
1086 \r
1087                                                                                 if ( sibling )\r
1088                                                                                         isWhiteSpace = !!siblingText.length;\r
1089                                                                         }\r
1090                                                                         else\r
1091                                                                                 sibling = null;\r
1092                                                                 }\r
1093                                                         }\r
1094 \r
1095                                                         if ( isWhiteSpace )\r
1096                                                         {\r
1097                                                                 if ( needsWhiteSpace )\r
1098                                                                 {\r
1099                                                                         if ( commonReached )\r
1100                                                                                 endTop = enlargeable;\r
1101                                                                         else\r
1102                                                                                 this.setEndAfter( enlargeable );\r
1103                                                                 }\r
1104                                                         }\r
1105 \r
1106                                                         if ( sibling )\r
1107                                                         {\r
1108                                                                 next = sibling.getNext();\r
1109 \r
1110                                                                 if ( !enlargeable && !next )\r
1111                                                                 {\r
1112                                                                         enlargeable = sibling;\r
1113                                                                         sibling = null;\r
1114                                                                         break;\r
1115                                                                 }\r
1116 \r
1117                                                                 sibling = next;\r
1118                                                         }\r
1119                                                         else\r
1120                                                         {\r
1121                                                                 // If sibling has been set to null, then we\r
1122                                                                 // need to stop enlarging.\r
1123                                                                 enlargeable = null;\r
1124                                                         }\r
1125                                                 }\r
1126 \r
1127                                                 if ( enlargeable )\r
1128                                                         enlargeable = enlargeable.getParent();\r
1129                                         }\r
1130 \r
1131                                         // If the common ancestor can be enlarged by both boundaries, then include it also.\r
1132                                         if ( startTop && endTop )\r
1133                                         {\r
1134                                                 commonAncestor = startTop.contains( endTop ) ? endTop : startTop;\r
1135 \r
1136                                                 this.setStartBefore( commonAncestor );\r
1137                                                 this.setEndAfter( commonAncestor );\r
1138                                         }\r
1139                                         break;\r
1140 \r
1141                                 case CKEDITOR.ENLARGE_BLOCK_CONTENTS:\r
1142                                 case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:\r
1143 \r
1144                                         // Enlarging the start boundary.\r
1145                                         var walkerRange = new CKEDITOR.dom.range( this.document );\r
1146 \r
1147                                         body = this.document.getBody();\r
1148 \r
1149                                         walkerRange.setStartAt( body, CKEDITOR.POSITION_AFTER_START );\r
1150                                         walkerRange.setEnd( this.startContainer, this.startOffset );\r
1151 \r
1152                                         var walker = new CKEDITOR.dom.walker( walkerRange ),\r
1153                                             blockBoundary,  // The node on which the enlarging should stop.\r
1154                                                 tailBr, //\r
1155                                             defaultGuard = CKEDITOR.dom.walker.blockBoundary(\r
1156                                                                 ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br : 1 } : null ),\r
1157                                                 // Record the encountered 'blockBoundary' for later use.\r
1158                                                 boundaryGuard = function( node )\r
1159                                                 {\r
1160                                                         var retval = defaultGuard( node );\r
1161                                                         if ( !retval )\r
1162                                                                 blockBoundary = node;\r
1163                                                         return retval;\r
1164                                                 },\r
1165                                                 // Record the encounted 'tailBr' for later use.\r
1166                                                 tailBrGuard = function( node )\r
1167                                                 {\r
1168                                                         var retval = boundaryGuard( node );\r
1169                                                         if ( !retval && node.is && node.is( 'br' ) )\r
1170                                                                 tailBr = node;\r
1171                                                         return retval;\r
1172                                                 };\r
1173 \r
1174                                         walker.guard = boundaryGuard;\r
1175 \r
1176                                         enlargeable = walker.lastBackward();\r
1177 \r
1178                                         // It's the body which stop the enlarging if no block boundary found.\r
1179                                         blockBoundary = blockBoundary || body;\r
1180 \r
1181                                         // Start the range at different position by comparing\r
1182                                         // the document position of it with 'enlargeable' node.\r
1183                                         this.setStartAt(\r
1184                                                         blockBoundary,\r
1185                                                         !blockBoundary.is( 'br' ) &&\r
1186                                                         ( !enlargeable || blockBoundary.contains( enlargeable ) ) ?\r
1187                                                                 CKEDITOR.POSITION_AFTER_START :\r
1188                                                                 CKEDITOR.POSITION_AFTER_END );\r
1189 \r
1190                                         // Enlarging the end boundary.\r
1191                                         walkerRange = this.clone();\r
1192                                         walkerRange.collapse();\r
1193                                         walkerRange.setEndAt( body, CKEDITOR.POSITION_BEFORE_END );\r
1194                                         walker = new CKEDITOR.dom.walker( walkerRange );\r
1195 \r
1196                                         // tailBrGuard only used for on range end.\r
1197                                         walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ?\r
1198                                                 tailBrGuard : boundaryGuard;\r
1199                                         blockBoundary = null;\r
1200                                         // End the range right before the block boundary node.\r
1201 \r
1202                                         enlargeable = walker.lastForward();\r
1203 \r
1204                                         // It's the body which stop the enlarging if no block boundary found.\r
1205                                         blockBoundary = blockBoundary || body;\r
1206 \r
1207                                         // Start the range at different position by comparing\r
1208                                         // the document position of it with 'enlargeable' node.\r
1209                                         this.setEndAt(\r
1210                                                         blockBoundary,\r
1211                                                         !blockBoundary.is( 'br' ) &&\r
1212                                                         ( !enlargeable || blockBoundary.contains( enlargeable ) ) ?\r
1213                                                                 CKEDITOR.POSITION_BEFORE_END :\r
1214                                                                 CKEDITOR.POSITION_BEFORE_START );\r
1215                                         // We must include the <br> at the end of range if there's\r
1216                                         // one and we're expanding list item contents\r
1217                                         if ( tailBr )\r
1218                                                 this.setEndAfter( tailBr );\r
1219                         }\r
1220                 },\r
1221 \r
1222                 /**\r
1223                  * Inserts a node at the start of the range. The range will be expanded\r
1224                  * the contain the node.\r
1225                  */\r
1226                 insertNode : function( node )\r
1227                 {\r
1228                         this.optimizeBookmark();\r
1229                         this.trim( false, true );\r
1230 \r
1231                         var startContainer = this.startContainer;\r
1232                         var startOffset = this.startOffset;\r
1233 \r
1234                         var nextNode = startContainer.getChild( startOffset );\r
1235 \r
1236                         if ( nextNode )\r
1237                                 node.insertBefore( nextNode );\r
1238                         else\r
1239                                 startContainer.append( node );\r
1240 \r
1241                         // Check if we need to update the end boundary.\r
1242                         if ( node.getParent().equals( this.endContainer ) )\r
1243                                 this.endOffset++;\r
1244 \r
1245                         // Expand the range to embrace the new node.\r
1246                         this.setStartBefore( node );\r
1247                 },\r
1248 \r
1249                 moveToPosition : function( node, position )\r
1250                 {\r
1251                         this.setStartAt( node, position );\r
1252                         this.collapse( true );\r
1253                 },\r
1254 \r
1255                 selectNodeContents : function( node )\r
1256                 {\r
1257                         this.setStart( node, 0 );\r
1258                         this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );\r
1259                 },\r
1260 \r
1261                 /**\r
1262                  * Sets the start position of a Range.\r
1263                  * @param {CKEDITOR.dom.node} startNode The node to start the range.\r
1264                  * @param {Number} startOffset An integer greater than or equal to zero\r
1265                  *              representing the offset for the start of the range from the start\r
1266                  *              of startNode.\r
1267                  */\r
1268                 setStart : function( startNode, startOffset )\r
1269                 {\r
1270                         // W3C requires a check for the new position. If it is after the end\r
1271                         // boundary, the range should be collapsed to the new start. It seams\r
1272                         // we will not need this check for our use of this class so we can\r
1273                         // ignore it for now.\r
1274 \r
1275                         this.startContainer     = startNode;\r
1276                         this.startOffset        = startOffset;\r
1277 \r
1278                         if ( !this.endContainer )\r
1279                         {\r
1280                                 this.endContainer       = startNode;\r
1281                                 this.endOffset          = startOffset;\r
1282                         }\r
1283 \r
1284                         updateCollapsed( this );\r
1285                 },\r
1286 \r
1287                 /**\r
1288                  * Sets the end position of a Range.\r
1289                  * @param {CKEDITOR.dom.node} endNode The node to end the range.\r
1290                  * @param {Number} endOffset An integer greater than or equal to zero\r
1291                  *              representing the offset for the end of the range from the start\r
1292                  *              of endNode.\r
1293                  */\r
1294                 setEnd : function( endNode, endOffset )\r
1295                 {\r
1296                         // W3C requires a check for the new position. If it is before the start\r
1297                         // boundary, the range should be collapsed to the new end. It seams we\r
1298                         // will not need this check for our use of this class so we can ignore\r
1299                         // it for now.\r
1300 \r
1301                         this.endContainer       = endNode;\r
1302                         this.endOffset          = endOffset;\r
1303 \r
1304                         if ( !this.startContainer )\r
1305                         {\r
1306                                 this.startContainer     = endNode;\r
1307                                 this.startOffset        = endOffset;\r
1308                         }\r
1309 \r
1310                         updateCollapsed( this );\r
1311                 },\r
1312 \r
1313                 setStartAfter : function( node )\r
1314                 {\r
1315                         this.setStart( node.getParent(), node.getIndex() + 1 );\r
1316                 },\r
1317 \r
1318                 setStartBefore : function( node )\r
1319                 {\r
1320                         this.setStart( node.getParent(), node.getIndex() );\r
1321                 },\r
1322 \r
1323                 setEndAfter : function( node )\r
1324                 {\r
1325                         this.setEnd( node.getParent(), node.getIndex() + 1 );\r
1326                 },\r
1327 \r
1328                 setEndBefore : function( node )\r
1329                 {\r
1330                         this.setEnd( node.getParent(), node.getIndex() );\r
1331                 },\r
1332 \r
1333                 setStartAt : function( node, position )\r
1334                 {\r
1335                         switch( position )\r
1336                         {\r
1337                                 case CKEDITOR.POSITION_AFTER_START :\r
1338                                         this.setStart( node, 0 );\r
1339                                         break;\r
1340 \r
1341                                 case CKEDITOR.POSITION_BEFORE_END :\r
1342                                         if ( node.type == CKEDITOR.NODE_TEXT )\r
1343                                                 this.setStart( node, node.getLength() );\r
1344                                         else\r
1345                                                 this.setStart( node, node.getChildCount() );\r
1346                                         break;\r
1347 \r
1348                                 case CKEDITOR.POSITION_BEFORE_START :\r
1349                                         this.setStartBefore( node );\r
1350                                         break;\r
1351 \r
1352                                 case CKEDITOR.POSITION_AFTER_END :\r
1353                                         this.setStartAfter( node );\r
1354                         }\r
1355 \r
1356                         updateCollapsed( this );\r
1357                 },\r
1358 \r
1359                 setEndAt : function( node, position )\r
1360                 {\r
1361                         switch( position )\r
1362                         {\r
1363                                 case CKEDITOR.POSITION_AFTER_START :\r
1364                                         this.setEnd( node, 0 );\r
1365                                         break;\r
1366 \r
1367                                 case CKEDITOR.POSITION_BEFORE_END :\r
1368                                         if ( node.type == CKEDITOR.NODE_TEXT )\r
1369                                                 this.setEnd( node, node.getLength() );\r
1370                                         else\r
1371                                                 this.setEnd( node, node.getChildCount() );\r
1372                                         break;\r
1373 \r
1374                                 case CKEDITOR.POSITION_BEFORE_START :\r
1375                                         this.setEndBefore( node );\r
1376                                         break;\r
1377 \r
1378                                 case CKEDITOR.POSITION_AFTER_END :\r
1379                                         this.setEndAfter( node );\r
1380                         }\r
1381 \r
1382                         updateCollapsed( this );\r
1383                 },\r
1384 \r
1385                 fixBlock : function( isStart, blockTag )\r
1386                 {\r
1387                         var bookmark = this.createBookmark(),\r
1388                                 fixedBlock = this.document.createElement( blockTag );\r
1389 \r
1390                         this.collapse( isStart );\r
1391 \r
1392                         this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );\r
1393 \r
1394                         this.extractContents().appendTo( fixedBlock );\r
1395                         fixedBlock.trim();\r
1396 \r
1397                         if ( !CKEDITOR.env.ie )\r
1398                                 fixedBlock.appendBogus();\r
1399 \r
1400                         this.insertNode( fixedBlock );\r
1401 \r
1402                         this.moveToBookmark( bookmark );\r
1403 \r
1404                         return fixedBlock;\r
1405                 },\r
1406 \r
1407                 splitBlock : function( blockTag )\r
1408                 {\r
1409                         var startPath   = new CKEDITOR.dom.elementPath( this.startContainer ),\r
1410                                 endPath         = new CKEDITOR.dom.elementPath( this.endContainer );\r
1411 \r
1412                         var startBlockLimit     = startPath.blockLimit,\r
1413                                 endBlockLimit   = endPath.blockLimit;\r
1414 \r
1415                         var startBlock  = startPath.block,\r
1416                                 endBlock        = endPath.block;\r
1417 \r
1418                         var elementPath = null;\r
1419                         // Do nothing if the boundaries are in different block limits.\r
1420                         if ( !startBlockLimit.equals( endBlockLimit ) )\r
1421                                 return null;\r
1422 \r
1423                         // Get or fix current blocks.\r
1424                         if ( blockTag != 'br' )\r
1425                         {\r
1426                                 if ( !startBlock )\r
1427                                 {\r
1428                                         startBlock = this.fixBlock( true, blockTag );\r
1429                                         endBlock = new CKEDITOR.dom.elementPath( this.endContainer ).block;\r
1430                                 }\r
1431 \r
1432                                 if ( !endBlock )\r
1433                                         endBlock = this.fixBlock( false, blockTag );\r
1434                         }\r
1435 \r
1436                         // Get the range position.\r
1437                         var isStartOfBlock = startBlock && this.checkStartOfBlock(),\r
1438                                 isEndOfBlock = endBlock && this.checkEndOfBlock();\r
1439 \r
1440                         // Delete the current contents.\r
1441                         // TODO: Why is 2.x doing CheckIsEmpty()?\r
1442                         this.deleteContents();\r
1443 \r
1444                         if ( startBlock && startBlock.equals( endBlock ) )\r
1445                         {\r
1446                                 if ( isEndOfBlock )\r
1447                                 {\r
1448                                         elementPath = new CKEDITOR.dom.elementPath( this.startContainer );\r
1449                                         this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );\r
1450                                         endBlock = null;\r
1451                                 }\r
1452                                 else if ( isStartOfBlock )\r
1453                                 {\r
1454                                         elementPath = new CKEDITOR.dom.elementPath( this.startContainer );\r
1455                                         this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );\r
1456                                         startBlock = null;\r
1457                                 }\r
1458                                 else\r
1459                                 {\r
1460                                         // Extract the contents of the block from the selection point to the end\r
1461                                         // of its contents.\r
1462                                         this.setEndAt( startBlock, CKEDITOR.POSITION_BEFORE_END );\r
1463                                         var documentFragment = this.extractContents();\r
1464 \r
1465                                         // Duplicate the block element after it.\r
1466                                         endBlock = startBlock.clone( false );\r
1467 \r
1468                                         // Place the extracted contents into the duplicated block.\r
1469                                         documentFragment.appendTo( endBlock );\r
1470                                         endBlock.insertAfter( startBlock );\r
1471                                         this.moveToPosition( startBlock, CKEDITOR.POSITION_AFTER_END );\r
1472 \r
1473                                         // In Gecko, the last child node must be a bogus <br>.\r
1474                                         // Note: bogus <br> added under <ul> or <ol> would cause\r
1475                                         // lists to be incorrectly rendered.\r
1476                                         if ( !CKEDITOR.env.ie && !startBlock.is( 'ul', 'ol') )\r
1477                                                 startBlock.appendBogus() ;\r
1478                                 }\r
1479                         }\r
1480 \r
1481                         return {\r
1482                                 previousBlock : startBlock,\r
1483                                 nextBlock : endBlock,\r
1484                                 wasStartOfBlock : isStartOfBlock,\r
1485                                 wasEndOfBlock : isEndOfBlock,\r
1486                                 elementPath : elementPath\r
1487                         };\r
1488                 },\r
1489 \r
1490                 /**\r
1491                  * Check whether current range is on the inner edge of the specified element.\r
1492                  * @param {Number} checkType ( CKEDITOR.START | CKEDITOR.END ) The checking side.\r
1493                  * @param {CKEDITOR.dom.element} element The target element to check.\r
1494                  */\r
1495                 checkBoundaryOfElement : function( element, checkType )\r
1496                 {\r
1497                         var walkerRange = this.clone();\r
1498                         // Expand the range to element boundary.\r
1499                         walkerRange[ checkType == CKEDITOR.START ?\r
1500                          'setStartAt' : 'setEndAt' ]\r
1501                          ( element, checkType == CKEDITOR.START ?\r
1502                            CKEDITOR.POSITION_AFTER_START\r
1503                            : CKEDITOR.POSITION_BEFORE_END );\r
1504 \r
1505                         var walker = new CKEDITOR.dom.walker( walkerRange ),\r
1506                          retval = false;\r
1507                         walker.evaluator = elementBoundaryEval;\r
1508                         return walker[ checkType == CKEDITOR.START ?\r
1509                                 'checkBackward' : 'checkForward' ]();\r
1510                 },\r
1511                 // Calls to this function may produce changes to the DOM. The range may\r
1512                 // be updated to reflect such changes.\r
1513                 checkStartOfBlock : function()\r
1514                 {\r
1515                         var startContainer = this.startContainer,\r
1516                                 startOffset = this.startOffset;\r
1517 \r
1518                         // If the starting node is a text node, and non-empty before the offset,\r
1519                         // then we're surely not at the start of block.\r
1520                         if ( startOffset && startContainer.type == CKEDITOR.NODE_TEXT )\r
1521                         {\r
1522                                 var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );\r
1523                                 if ( textBefore.length )\r
1524                                         return false;\r
1525                         }\r
1526 \r
1527                         // Antecipate the trim() call here, so the walker will not make\r
1528                         // changes to the DOM, which would not get reflected into this\r
1529                         // range otherwise.\r
1530                         this.trim();\r
1531 \r
1532                         // We need to grab the block element holding the start boundary, so\r
1533                         // let's use an element path for it.\r
1534                         var path = new CKEDITOR.dom.elementPath( this.startContainer );\r
1535 \r
1536                         // Creates a range starting at the block start until the range start.\r
1537                         var walkerRange = this.clone();\r
1538                         walkerRange.collapse( true );\r
1539                         walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );\r
1540 \r
1541                         var walker = new CKEDITOR.dom.walker( walkerRange );\r
1542                         walker.evaluator = getCheckStartEndBlockEvalFunction( true );\r
1543 \r
1544                         return walker.checkBackward();\r
1545                 },\r
1546 \r
1547                 checkEndOfBlock : function()\r
1548                 {\r
1549                         var endContainer = this.endContainer,\r
1550                                 endOffset = this.endOffset;\r
1551 \r
1552                         // If the ending node is a text node, and non-empty after the offset,\r
1553                         // then we're surely not at the end of block.\r
1554                         if ( endContainer.type == CKEDITOR.NODE_TEXT )\r
1555                         {\r
1556                                 var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );\r
1557                                 if ( textAfter.length )\r
1558                                         return false;\r
1559                         }\r
1560 \r
1561                         // Antecipate the trim() call here, so the walker will not make\r
1562                         // changes to the DOM, which would not get reflected into this\r
1563                         // range otherwise.\r
1564                         this.trim();\r
1565 \r
1566                         // We need to grab the block element holding the start boundary, so\r
1567                         // let's use an element path for it.\r
1568                         var path = new CKEDITOR.dom.elementPath( this.endContainer );\r
1569 \r
1570                         // Creates a range starting at the block start until the range start.\r
1571                         var walkerRange = this.clone();\r
1572                         walkerRange.collapse( false );\r
1573                         walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );\r
1574 \r
1575                         var walker = new CKEDITOR.dom.walker( walkerRange );\r
1576                         walker.evaluator = getCheckStartEndBlockEvalFunction( false );\r
1577 \r
1578                         return walker.checkForward();\r
1579                 },\r
1580 \r
1581                 /**\r
1582                  * Moves the range boundaries to the first editing point inside an\r
1583                  * element. For example, in an element tree like\r
1584                  * "&lt;p&gt;&lt;b&gt;&lt;i&gt;&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;", the start editing point is\r
1585                  * "&lt;p&gt;&lt;b&gt;&lt;i&gt;^&lt;/i&gt;&lt;/b&gt; Text&lt;/p&gt;" (inside &lt;i&gt;).\r
1586                  * @param {CKEDITOR.dom.element} targetElement The element into which\r
1587                  *              look for the editing spot.\r
1588                  */\r
1589                 moveToElementEditStart : function( targetElement )\r
1590                 {\r
1591                         var editableElement;\r
1592 \r
1593                         while ( targetElement && targetElement.type == CKEDITOR.NODE_ELEMENT )\r
1594                         {\r
1595                                 if ( targetElement.isEditable() )\r
1596                                         editableElement = targetElement;\r
1597                                 else if ( editableElement )\r
1598                                         break ;         // If we already found an editable element, stop the loop.\r
1599 \r
1600                                 targetElement = targetElement.getFirst();\r
1601                         }\r
1602 \r
1603                         if ( editableElement )\r
1604                         {\r
1605                                 this.moveToPosition(editableElement, CKEDITOR.POSITION_AFTER_START);\r
1606                                 return true;\r
1607                         }\r
1608                         else\r
1609                                 return false;\r
1610                 },\r
1611 \r
1612                 /**\r
1613                  * Get the single node enclosed within the range if there's one.\r
1614                  */\r
1615                 getEnclosedNode : function()\r
1616                 {\r
1617                         var walkerRange = this.clone(),\r
1618                                 walker = new CKEDITOR.dom.walker( walkerRange ),\r
1619                                 isNotBookmarks = CKEDITOR.dom.walker.bookmark( true ),\r
1620                                 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),\r
1621                                 evaluator = function( node )\r
1622                                 {\r
1623                                         return isNotWhitespaces( node ) && isNotBookmarks( node );\r
1624                                 };\r
1625                         walkerRange.evaluator = evaluator;\r
1626                         var node = walker.next();\r
1627                         walker.reset();\r
1628                         return node && node.equals( walker.previous() ) ? node : null;\r
1629                 },\r
1630 \r
1631                 getTouchedStartNode : function()\r
1632                 {\r
1633                         var container = this.startContainer ;\r
1634 \r
1635                         if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )\r
1636                                 return container ;\r
1637 \r
1638                         return container.getChild( this.startOffset ) || container ;\r
1639                 },\r
1640 \r
1641                 getTouchedEndNode : function()\r
1642                 {\r
1643                         var container = this.endContainer ;\r
1644 \r
1645                         if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )\r
1646                                 return container ;\r
1647 \r
1648                         return container.getChild( this.endOffset - 1 ) || container ;\r
1649                 }\r
1650         };\r
1651 })();\r
1652 \r
1653 CKEDITOR.POSITION_AFTER_START   = 1;    // <element>^contents</element>         "^text"\r
1654 CKEDITOR.POSITION_BEFORE_END    = 2;    // <element>contents^</element>         "text^"\r
1655 CKEDITOR.POSITION_BEFORE_START  = 3;    // ^<element>contents</element>         ^"text"\r
1656 CKEDITOR.POSITION_AFTER_END             = 4;    // <element>contents</element>^         "text"\r
1657 \r
1658 CKEDITOR.ENLARGE_ELEMENT = 1;\r
1659 CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;\r
1660 CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;\r
1661 \r
1662 /**\r
1663  * Check boundary types.\r
1664  * @see CKEDITOR.dom.range::checkBoundaryOfElement\r
1665  */\r
1666 CKEDITOR.START = 1;\r
1667 CKEDITOR.END = 2;\r
1668 CKEDITOR.STARTEND = 3;\r