1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 /** 7 * @file DOM iterator, which iterates over list items, lines and paragraphs. 8 */ 9 10 CKEDITOR.plugins.add( 'domiterator' ); 11 12 (function() 13 { 14 // Functions ported over from v2. 15 function getTouchedStartNode( range ) 16 { 17 var container = range.startContainer; 18 19 if ( range.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 20 return container; 21 22 return container.getChildCount() > range.startOffset ? container.getChild( range.startOffset ) : container; 23 } 24 25 function getTouchedEndNode( range ) 26 { 27 var container = range.endContainer; 28 29 if ( range.collapsed || container.type != CKEDITOR.NODE_ELEMENT ) 30 return container; 31 32 return container.getChildCount() > range.endOffset ? container.getChild( range.endOffset ) : container; 33 } 34 35 function getNextSourceNode( currentNode, startFromSibling, nodeType, stopSearchNode ) 36 { 37 if ( !currentNode ) 38 return null; 39 40 var node; 41 42 if ( !startFromSibling && currentNode.getFirst && currentNode.getFirst() ) 43 node = currentNode.getFirst(); 44 else 45 { 46 if ( stopSearchNode && currentNode.equals( stopSearchNode ) ) 47 return null; 48 49 node = currentNode.getNext(); 50 51 if ( !node && ( !stopSearchNode || !stopSearchNode.equals( currentNode.parentNode ) ) ) 52 return getNextSourceNode( currentNode.getParent(), true, nodeType, stopSearchNode ); 53 } 54 55 if ( nodeType && node && node.type != nodeType ) 56 return getNextSourceNode( node, false, nodeType, stopSearchNode ); 57 58 return node; 59 } 60 61 var iterator = function( range ) 62 { 63 if ( arguments.length < 1 ) 64 return; 65 66 this.range = range; 67 this.forceBrBreak = false; 68 this.enforceRealBlocks = false; 69 70 this._ || ( this._ = {} ); 71 }, 72 beginWhitespaceRegex = /^[\r\n\t ]+$/; 73 74 75 iterator.prototype = { 76 getNextParagraph : function( blockTag ) 77 { 78 // The block element to be returned. 79 var block; 80 81 // The range object used to identify the paragraph contents. 82 var range; 83 84 // Indicats that the current element in the loop is the last one. 85 var isLast; 86 87 // Instructs to cleanup remaining BRs. 88 var removePreviousBr, removeLastBr; 89 90 // This is the first iteration. Let's initialize it. 91 if ( !this._.lastNode ) 92 { 93 range = this.range.clone(); 94 range.enlarge( this.forceBrBreak ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS ); 95 96 this._.nextNode = getTouchedStartNode( range ); 97 this._.lastNode = getTouchedEndNode( range ); 98 99 // Let's reuse this variable. 100 range = null; 101 } 102 103 var currentNode = this._.nextNode, 104 lastNode = this._.lastNode; 105 106 this._.nextNode = null; 107 108 while ( currentNode ) 109 { 110 // closeRange indicates that a paragraph boundary has been found, 111 // so the range can be closed. 112 var closeRange = false; 113 114 // includeNode indicates that the current node is good to be part 115 // of the range. By default, any non-element node is ok for it. 116 var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ), 117 continueFromSibling = false; 118 119 // If it is an element node, let's check if it can be part of the 120 // range. 121 if ( !includeNode ) 122 { 123 var nodeName = currentNode.getName(); 124 125 if ( currentNode.isBlockBoundary( this.forceBrBreak && { br : 1 } ) ) 126 { 127 // <br> boundaries must be part of the range. It will 128 // happen only if ForceBrBreak. 129 if ( nodeName == 'br' ) 130 includeNode = true; 131 else if ( !range && currentNode.getChildCount() == 0 && nodeName != 'hr' ) 132 { 133 // If we have found an empty block, and haven't started 134 // the range yet, it means we must return this block. 135 block = currentNode; 136 isLast = currentNode.equals( lastNode ); 137 break; 138 } 139 140 // The range must finish right before the boundary, 141 // including possibly skipped empty spaces. (#1603) 142 if ( range ) 143 { 144 range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START ); 145 146 // The found boundary must be set as the next one at this 147 // point. (#1717) 148 if ( nodeName != 'br' ) 149 this._.nextNode = getNextSourceNode( currentNode, true, null, lastNode ) || currentNode; 150 } 151 152 closeRange = true; 153 } 154 else 155 { 156 // If we have child nodes, let's check them. 157 if ( currentNode.getFirst() ) 158 { 159 // If we don't have a range yet, let's start it. 160 if ( !range ) 161 { 162 range = new CKEDITOR.dom.range( this.range.document ); 163 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START ); 164 } 165 166 currentNode = currentNode.getFirst(); 167 continue; 168 } 169 includeNode = true; 170 } 171 } 172 else if ( currentNode.type == CKEDITOR.NODE_TEXT ) 173 { 174 // Ignore normal whitespaces (i.e. not including or 175 // other unicode whitespaces) before/after a block node. 176 if ( beginWhitespaceRegex.test( currentNode.getText() ) ) 177 includeNode = false; 178 } 179 180 // The current node is good to be part of the range and we are 181 // starting a new range, initialize it first. 182 if ( includeNode && !range ) 183 { 184 range = new CKEDITOR.dom.range( this.range.document ); 185 range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START ); 186 } 187 188 // The last node has been found. 189 isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) ); 190 191 // If we are in an element boundary, let's check if it is time 192 // to close the range, otherwise we include the parent within it. 193 if ( range && !closeRange ) 194 { 195 while ( !currentNode.getNext() && !isLast ) 196 { 197 var parentNode = currentNode.getParent(); 198 199 if ( parentNode.isBlockBoundary( this.forceBrBreak && { br : 1 } ) ) 200 { 201 closeRange = true; 202 isLast = isLast || ( parentNode.equals( lastNode) ); 203 break; 204 } 205 206 currentNode = parentNode; 207 includeNode = true; 208 isLast = ( currentNode.equals( lastNode ) ); 209 continueFromSibling = true; 210 } 211 } 212 213 // Now finally include the node. 214 if ( includeNode ) 215 range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END ); 216 217 // We have found a block boundary. Let's close the range and move out of the 218 // loop. 219 if ( ( closeRange || isLast ) && range ) 220 { 221 var boundaryNodes = range.getBoundaryNodes(), 222 startPath = new CKEDITOR.dom.elementPath( range.startContainer ), 223 endPath = new CKEDITOR.dom.elementPath( range.endContainer ); 224 if ( boundaryNodes.startNode.equals( boundaryNodes.endNode ) 225 && boundaryNodes.startNode.getParent().equals( startPath.blockLimit ) 226 && boundaryNodes.startNode.type == CKEDITOR.NODE_ELEMENT && boundaryNodes.startNode.getAttribute( '_fck_bookmark' ) ) 227 range = null; 228 else 229 break; 230 } 231 232 if ( isLast ) 233 break; 234 235 currentNode = getNextSourceNode( currentNode, continueFromSibling, null, lastNode ); 236 } 237 238 // Now, based on the processed range, look for (or create) the block to be returned. 239 if ( !block ) 240 { 241 // If no range has been found, this is the end. 242 if ( !range ) 243 { 244 this._.nextNode = null; 245 return null; 246 } 247 248 var startPath = new CKEDITOR.dom.elementPath( range.startContainer ), 249 startBlockLimit = startPath.blockLimit, 250 checkLimits = { div : 1, th : 1, td : 1 }; 251 block = startPath.block; 252 253 if ( !block 254 && !this.enforceRealBlocks 255 && checkLimits[ startBlockLimit.getName() ] 256 && range.checkStartOfBlock() 257 && range.checkEndOfBlock() ) 258 block = startBlockLimit; 259 else if ( !block || ( this.enforceRealBlocks && block.getName() == 'li' ) ) 260 { 261 // Create the fixed block. 262 block = this.range.document.createElement( blockTag || 'p' ); 263 264 // Move the contents of the temporary range to the fixed block. 265 range.extractContents().appendTo( block ); 266 block.trim(); 267 268 // Insert the fixed block into the DOM. 269 range.insertNode( block ); 270 271 removePreviousBr = removeLastBr = true; 272 } 273 else if ( block.getName() != 'li' ) 274 { 275 // If the range doesn't includes the entire contents of the 276 // block, we must split it, isolating the range in a dedicated 277 // block. 278 if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) 279 { 280 // The resulting block will be a clone of the current one. 281 block = block.clone( false ); 282 283 // Extract the range contents, moving it to the new block. 284 range.extractContents().appendTo( block ); 285 block.trim(); 286 287 // Split the block. At this point, the range will be in the 288 // right position for our intents. 289 var splitInfo = range.splitBlock(); 290 291 removePreviousBr = !splitInfo.wasStartOfBlock; 292 removeLastBr = !splitInfo.wasEndOfBlock; 293 294 // Insert the new block into the DOM. 295 range.insertNode( block ); 296 } 297 } 298 else if ( !isLast ) 299 { 300 // LIs are returned as is, with all their children (due to the 301 // nested lists). But, the next node is the node right after 302 // the current range, which could be an <li> child (nested 303 // lists) or the next sibling <li>. 304 305 this._.nextNode = ( block.equals( lastNode ) ? null : 306 getNextSourceNode( range.getBoundaryNodes().endNode, true, null, lastNode ) ); 307 } 308 } 309 310 if ( removePreviousBr ) 311 { 312 var previousSibling = block.getPrevious(); 313 if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) 314 { 315 if ( previousSibling.getName() == 'br' ) 316 previousSibling.remove(); 317 else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' ) 318 previousSibling.getLast().remove(); 319 } 320 } 321 322 if ( removeLastBr ) 323 { 324 var lastChild = block.getLast(); 325 if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) 326 lastChild.remove(); 327 } 328 329 // Get a reference for the next element. This is important because the 330 // above block can be removed or changed, so we can rely on it for the 331 // next interation. 332 if ( !this._.nextNode ) 333 this._.nextNode = ( isLast || block.equals( lastNode ) ) ? null : getNextSourceNode( block, true, null, lastNode ); 334 335 return block; 336 } 337 }; 338 339 CKEDITOR.dom.range.prototype.createIterator = function() 340 { 341 return new iterator( this ); 342 }; 343 })(); 344