1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 (function() 7 { 8 // This function is to be called under a "walker" instance scope. 9 function iterate( rtl, breakOnFalse ) 10 { 11 var range = this.range; 12 13 // Return null if we have reached the end. 14 if ( this._.end ) 15 return null; 16 17 // This is the first call. Initialize it. 18 if ( !this._.start ) 19 { 20 this._.start = 1; 21 22 // A collapsed range must return null at first call. 23 if ( range.collapsed ) 24 { 25 this.end(); 26 return null; 27 } 28 29 // Move outside of text node edges. 30 range.optimize(); 31 } 32 33 var node, 34 startCt = range.startContainer, 35 endCt = range.endContainer, 36 startOffset = range.startOffset, 37 endOffset = range.endOffset, 38 guard, 39 userGuard = this.guard, 40 type = this.type, 41 getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' ); 42 43 // Create the LTR guard function, if necessary. 44 if ( !rtl && !this._.guardLTR ) 45 { 46 // The node that stops walker from moving up. 47 var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? 48 endCt : 49 endCt.getParent(); 50 51 // The node that stops the walker from going to next. 52 var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? 53 endCt.getChild( endOffset ) : 54 endCt.getNext(); 55 56 this._.guardLTR = function( node, movingOut ) 57 { 58 return ( ( !movingOut || !limitLTR.equals( node ) ) 59 && ( !blockerLTR || !node.equals( blockerLTR ) ) 60 && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) ); 61 }; 62 } 63 64 // Create the RTL guard function, if necessary. 65 if ( rtl && !this._.guardRTL ) 66 { 67 // The node that stops walker from moving up. 68 var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? 69 startCt : 70 startCt.getParent(); 71 72 // The node that stops the walker from going to next. 73 var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? 74 startOffset ? 75 startCt.getChild( startOffset - 1 ) : null : 76 startCt.getPrevious(); 77 78 this._.guardRTL = function( node, movingOut ) 79 { 80 return ( ( !movingOut || !limitRTL.equals( node ) ) 81 && ( !blockerRTL || !node.equals( blockerRTL ) ) 82 && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) ); 83 }; 84 } 85 86 // Define which guard function to use. 87 var stopGuard = rtl ? this._.guardRTL : this._.guardLTR; 88 89 // Make the user defined guard function participate in the process, 90 // otherwise simply use the boundary guard. 91 if ( userGuard ) 92 { 93 guard = function( node, movingOut ) 94 { 95 if ( stopGuard( node, movingOut ) === false ) 96 return false; 97 98 return userGuard( node, movingOut ); 99 }; 100 } 101 else 102 guard = stopGuard; 103 104 if ( this.current ) 105 node = this.current[ getSourceNodeFn ]( false, type, guard ); 106 else 107 { 108 // Get the first node to be returned. 109 if ( rtl ) 110 { 111 node = endCt; 112 113 if ( node.type == CKEDITOR.NODE_ELEMENT ) 114 { 115 if ( endOffset > 0 ) 116 node = node.getChild( endOffset - 1 ); 117 else 118 node = ( guard ( node, true ) === false ) ? 119 null : node.getPreviousSourceNode( true, type, guard ); 120 } 121 } 122 else 123 { 124 node = startCt; 125 126 if ( node.type == CKEDITOR.NODE_ELEMENT ) 127 { 128 if ( ! ( node = node.getChild( startOffset ) ) ) 129 node = ( guard ( startCt, true ) === false ) ? 130 null : startCt.getNextSourceNode( true, type, guard ) ; 131 } 132 } 133 134 if ( node && guard( node ) === false ) 135 node = null; 136 } 137 138 while ( node && !this._.end ) 139 { 140 this.current = node; 141 142 if ( !this.evaluator || this.evaluator( node ) !== false ) 143 { 144 if ( !breakOnFalse ) 145 return node; 146 } 147 else if ( breakOnFalse && this.evaluator ) 148 return false; 149 150 node = node[ getSourceNodeFn ]( false, type, guard ); 151 } 152 153 this.end(); 154 return this.current = null; 155 } 156 157 function iterateToLast( rtl ) 158 { 159 var node, last = null; 160 161 while ( ( node = iterate.call( this, rtl ) ) ) 162 last = node; 163 164 return last; 165 } 166 167 CKEDITOR.dom.walker = CKEDITOR.tools.createClass( 168 { 169 /** 170 * Utility class to "walk" the DOM inside a range boundaries. If 171 * necessary, partially included nodes (text nodes) are broken to 172 * reflect the boundaries limits, so DOM and range changes may happen. 173 * Outside changes to the range may break the walker. 174 * 175 * The walker may return nodes that are not totaly included into the 176 * range boundaires. Let's take the following range representation, 177 * where the square brackets indicate the boundaries: 178 * 179 * [<p>Some <b>sample] text</b> 180 * 181 * While walking forward into the above range, the following nodes are 182 * returned: <p>, "Some ", <b> and "sample". Going 183 * backwards instead we have: "sample" and "Some ". So note that the 184 * walker always returns nodes when "entering" them, but not when 185 * "leaving" them. The guard function is instead called both when 186 * entering and leaving nodes. 187 * 188 * @constructor 189 * @param {CKEDITOR.dom.range} range The range within which walk. 190 */ 191 $ : function( range ) 192 { 193 this.range = range; 194 195 /** 196 * A function executed for every matched node, to check whether 197 * it's to be considered into the walk or not. If not provided, all 198 * matched nodes are considered good. 199 * If the function returns "false" the node is ignored. 200 * @name CKEDITOR.dom.walker.prototype.evaluator 201 * @property 202 * @type Function 203 */ 204 // this.evaluator = null; 205 206 /** 207 * A function executed for every node the walk pass by to check 208 * whether the walk is to be finished. It's called when both 209 * entering and exiting nodes, as well as for the matched nodes. 210 * If this function returns "false", the walking ends and no more 211 * nodes are evaluated. 212 * @name CKEDITOR.dom.walker.prototype.guard 213 * @property 214 * @type Function 215 */ 216 // this.guard = null; 217 218 /** @private */ 219 this._ = {}; 220 }, 221 222 // statics : 223 // { 224 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes. 225 // * @param {CKEDITOR.dom.node} startNode The node from wich the walk 226 // * will start. 227 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered 228 // * in the walk. No more nodes are retrieved after touching or 229 // * passing it. If not provided, the walker stops at the 230 // * <body> closing boundary. 231 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the 232 // * provided nodes. 233 // */ 234 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive ) 235 // { 236 // var range = new CKEDITOR.dom.range(); 237 // if ( startNode ) 238 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ; 239 // else 240 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ; 241 // 242 // if ( endNode ) 243 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ; 244 // else 245 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ; 246 // 247 // return new CKEDITOR.dom.walker( range ); 248 // } 249 // }, 250 // 251 proto : 252 { 253 /** 254 * Stop walking. No more nodes are retrieved if this function gets 255 * called. 256 */ 257 end : function() 258 { 259 this._.end = 1; 260 }, 261 262 /** 263 * Retrieves the next node (at right). 264 * @returns {CKEDITOR.dom.node} The next node or null if no more 265 * nodes are available. 266 */ 267 next : function() 268 { 269 return iterate.call( this ); 270 }, 271 272 /** 273 * Retrieves the previous node (at left). 274 * @returns {CKEDITOR.dom.node} The previous node or null if no more 275 * nodes are available. 276 */ 277 previous : function() 278 { 279 return iterate.call( this, 1 ); 280 }, 281 282 /** 283 * Check all nodes at right, executing the evaluation fuction. 284 * @returns {Boolean} "false" if the evaluator function returned 285 * "false" for any of the matched nodes. Otherwise "true". 286 */ 287 checkForward : function() 288 { 289 return iterate.call( this, 0, 1 ) !== false; 290 }, 291 292 /** 293 * Check all nodes at left, executing the evaluation fuction. 294 * @returns {Boolean} "false" if the evaluator function returned 295 * "false" for any of the matched nodes. Otherwise "true". 296 */ 297 checkBackward : function() 298 { 299 return iterate.call( this, 1, 1 ) !== false; 300 }, 301 302 /** 303 * Executes a full walk forward (to the right), until no more nodes 304 * are available, returning the last valid node. 305 * @returns {CKEDITOR.dom.node} The last node at the right or null 306 * if no valid nodes are available. 307 */ 308 lastForward : function() 309 { 310 return iterateToLast.call( this ); 311 }, 312 313 /** 314 * Executes a full walk backwards (to the left), until no more nodes 315 * are available, returning the last valid node. 316 * @returns {CKEDITOR.dom.node} The last node at the left or null 317 * if no valid nodes are available. 318 */ 319 lastBackward : function() 320 { 321 return iterateToLast.call( this, 1 ); 322 }, 323 324 reset : function() 325 { 326 delete this.current; 327 this._ = {}; 328 } 329 330 } 331 }); 332 333 /* 334 * Anything whose display computed style is block, list-item, table, 335 * table-row-group, table-header-group, table-footer-group, table-row, 336 * table-column-group, table-column, table-cell, table-caption, or whose node 337 * name is hr, br (when enterMode is br only) is a block boundary. 338 */ 339 var blockBoundaryDisplayMatch = 340 { 341 block : 1, 342 'list-item' : 1, 343 table : 1, 344 'table-row-group' : 1, 345 'table-header-group' : 1, 346 'table-footer-group' : 1, 347 'table-row' : 1, 348 'table-column-group' : 1, 349 'table-column' : 1, 350 'table-cell' : 1, 351 'table-caption' : 1 352 }; 353 354 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) 355 { 356 var nodeNameMatches = customNodeNames ? 357 CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$block, customNodeNames || {} ) : 358 CKEDITOR.dtd.$block; 359 360 // Don't consider floated formatting as block boundary, fall back to dtd check in that case. (#6297) 361 return this.getComputedStyle( 'float' ) == 'none' && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] 362 || nodeNameMatches[ this.getName() ]; 363 }; 364 365 CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) 366 { 367 return function( node , type ) 368 { 369 return ! ( node.type == CKEDITOR.NODE_ELEMENT 370 && node.isBlockBoundary( customNodeNames ) ); 371 }; 372 }; 373 374 CKEDITOR.dom.walker.listItemBoundary = function() 375 { 376 return this.blockBoundary( { br : 1 } ); 377 }; 378 379 /** 380 * Whether the to-be-evaluated node is a bookmark node OR bookmark node 381 * inner contents. 382 * @param {Boolean} contentOnly Whether only test againt the text content of 383 * bookmark node instead of the element itself(default). 384 * @param {Boolean} isReject Whether should return 'false' for the bookmark 385 * node instead of 'true'(default). 386 */ 387 CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) 388 { 389 function isBookmarkNode( node ) 390 { 391 return ( node && node.getName 392 && node.getName() == 'span' 393 && node.data( 'cke-bookmark' ) ); 394 } 395 396 return function( node ) 397 { 398 var isBookmark, parent; 399 // Is bookmark inner text node? 400 isBookmark = ( node && !node.getName && ( parent = node.getParent() ) 401 && isBookmarkNode( parent ) ); 402 // Is bookmark node? 403 isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node ); 404 return !! ( isReject ^ isBookmark ); 405 }; 406 }; 407 408 /** 409 * Whether the node is a text node containing only whitespaces characters. 410 * @param isReject 411 */ 412 CKEDITOR.dom.walker.whitespaces = function( isReject ) 413 { 414 return function( node ) 415 { 416 var isWhitespace; 417 if ( node && node.type == CKEDITOR.NODE_TEXT ) 418 { 419 // whitespace, as well as the text cursor filler node we used in Webkit. (#9384) 420 isWhitespace = !CKEDITOR.tools.trim( node.getText() ) || 421 CKEDITOR.env.webkit && node.getText() == '\u200b'; 422 } 423 424 return !! ( isReject ^ isWhitespace ); 425 }; 426 }; 427 428 /** 429 * Whether the node is invisible in wysiwyg mode. 430 * @param isReject 431 */ 432 CKEDITOR.dom.walker.invisible = function( isReject ) 433 { 434 var whitespace = CKEDITOR.dom.walker.whitespaces(); 435 return function( node ) 436 { 437 var invisible; 438 439 if ( whitespace( node ) ) 440 invisible = 1; 441 else 442 { 443 // Visibility should be checked on element. 444 if ( node.type == CKEDITOR.NODE_TEXT ) 445 node = node.getParent(); 446 447 // Nodes that take no spaces in wysiwyg: 448 // 1. White-spaces but not including NBSP; 449 // 2. Empty inline elements, e.g. <b></b> we're checking here 450 // 'offsetHeight' instead of 'offsetWidth' for properly excluding 451 // all sorts of empty paragraph, e.g. <br />. 452 invisible = !node.$.offsetHeight; 453 } 454 455 return !! ( isReject ^ invisible ); 456 }; 457 }; 458 459 CKEDITOR.dom.walker.nodeType = function( type, isReject ) 460 { 461 return function( node ) 462 { 463 return !! ( isReject ^ ( node.type == type ) ); 464 }; 465 }; 466 467 CKEDITOR.dom.walker.bogus = function( isReject ) 468 { 469 function nonEmpty( node ) 470 { 471 return !isWhitespaces( node ) && !isBookmark( node ); 472 } 473 474 return function( node ) 475 { 476 var isBogus = !CKEDITOR.env.ie ? node.is && node.is( 'br' ) : 477 node.getText && tailNbspRegex.test( node.getText() ); 478 479 if ( isBogus ) 480 { 481 var parent = node.getParent(), next = node.getNext( nonEmpty ); 482 isBogus = parent.isBlockBoundary() && 483 ( !next || 484 next.type == CKEDITOR.NODE_ELEMENT && 485 next.isBlockBoundary() ); 486 } 487 488 return !! ( isReject ^ isBogus ); 489 }; 490 }; 491 492 var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/, 493 isWhitespaces = CKEDITOR.dom.walker.whitespaces(), 494 isBookmark = CKEDITOR.dom.walker.bookmark(), 495 toSkip = function( node ) 496 { 497 return isBookmark( node ) 498 || isWhitespaces( node ) 499 || node.type == CKEDITOR.NODE_ELEMENT 500 && node.getName() in CKEDITOR.dtd.$inline 501 && !( node.getName() in CKEDITOR.dtd.$empty ); 502 }; 503 504 // Check if there's a filler node at the end of an element, and return it. 505 CKEDITOR.dom.element.prototype.getBogus = function() 506 { 507 // Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070). 508 var tail = this; 509 do { tail = tail.getPreviousSourceNode(); } 510 while ( toSkip( tail ) ) 511 512 if ( tail && ( !CKEDITOR.env.ie ? tail.is && tail.is( 'br' ) 513 : tail.getText && tailNbspRegex.test( tail.getText() ) ) ) 514 { 515 return tail; 516 } 517 return false; 518 }; 519 520 })(); 521