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