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