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