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 (function() 7 { 8 // This function is to be called under a "walker" instance scope. 9 function iterate( rtl, breakOnFalse ) 10 { 11 // Return null if we have reached the end. 12 if ( this._.end ) 13 return null; 14 15 var node, 16 range = this.range, 17 guard, 18 userGuard = this.guard, 19 type = this.type, 20 getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' ); 21 22 // This is the first call. Initialize it. 23 if ( !this._.start ) 24 { 25 this._.start = 1; 26 27 // Trim text nodes and optmize the range boundaries. DOM changes 28 // may happen at this point. 29 range.trim(); 30 31 // A collapsed range must return null at first call. 32 if ( range.collapsed ) 33 { 34 this.end(); 35 return null; 36 } 37 } 38 39 // Create the LTR guard function, if necessary. 40 if ( !rtl && !this._.guardLTR ) 41 { 42 // Gets the node that stops the walker when going LTR. 43 var limitLTR = range.endContainer, 44 blockerLTR = limitLTR.getChild( range.endOffset ); 45 46 this._.guardLTR = function( node, movingOut ) 47 { 48 return ( ( !movingOut || !limitLTR.equals( node ) ) 49 && ( !blockerLTR || !node.equals( blockerLTR ) ) 50 && ( node.type != CKEDITOR.NODE_ELEMENT || node.getName() != 'body' ) ); 51 }; 52 } 53 54 // Create the RTL guard function, if necessary. 55 if ( rtl && !this._.guardRTL ) 56 { 57 // Gets the node that stops the walker when going LTR. 58 var limitRTL = range.startContainer, 59 blockerRTL = ( range.startOffset > 0 ) && limitRTL.getChild( range.startOffset - 1 ); 60 61 this._.guardRTL = function( node, movingOut ) 62 { 63 return ( ( !movingOut || !limitRTL.equals( node ) ) 64 && ( !blockerRTL || !node.equals( blockerRTL ) ) 65 && ( node.type != CKEDITOR.NODE_ELEMENT || node.getName() != 'body' ) ); 66 }; 67 } 68 69 // Define which guard function to use. 70 var stopGuard = rtl ? this._.guardRTL : this._.guardLTR; 71 72 // Make the user defined guard function participate in the process, 73 // otherwise simply use the boundary guard. 74 if ( userGuard ) 75 { 76 guard = function( node, movingOut ) 77 { 78 if ( stopGuard( node, movingOut ) === false ) 79 return false; 80 81 return userGuard( node ); 82 }; 83 } 84 else 85 guard = stopGuard; 86 87 if ( this.current ) 88 node = this.current[ getSourceNodeFn ]( false, type, guard ); 89 else 90 { 91 // Get the first node to be returned. 92 93 if ( rtl ) 94 { 95 node = range.endContainer; 96 97 if ( range.endOffset > 0 ) 98 { 99 node = node.getChild( range.endOffset - 1 ); 100 if ( guard( node ) === false ) 101 node = null; 102 } 103 else 104 node = ( guard ( node ) === false ) ? 105 null : node.getPreviousSourceNode( true, type, guard ); 106 } 107 else 108 { 109 node = range.startContainer; 110 node = node.getChild( range.startOffset ); 111 112 if ( node ) 113 { 114 if ( guard( node ) === false ) 115 node = null; 116 } 117 else 118 node = ( guard ( range.startContainer ) === false ) ? 119 null : range.startContainer.getNextSourceNode( true, type, guard ) ; 120 } 121 } 122 123 while ( node && !this._.end ) 124 { 125 this.current = node; 126 127 if ( !this.evaluator || this.evaluator( node ) !== false ) 128 { 129 if ( !breakOnFalse ) 130 return node; 131 } 132 else if ( breakOnFalse && this.evaluator ) 133 return false; 134 135 node = node[ getSourceNodeFn ]( false, type, guard ); 136 } 137 138 this.end(); 139 return this.current = null; 140 } 141 142 function iterateToLast( rtl ) 143 { 144 var node, last = null; 145 146 while ( ( node = iterate.call( this, rtl ) ) ) 147 last = node; 148 149 return last; 150 } 151 152 CKEDITOR.dom.walker = CKEDITOR.tools.createClass( 153 { 154 /** 155 * Utility class to "walk" the DOM inside a range boundaries. If 156 * necessary, partially included nodes (text nodes) are broken to 157 * reflect the boundaries limits, so DOM and range changes may happen. 158 * Outside changes to the range may break the walker. 159 * 160 * The walker may return nodes that are not totaly included into the 161 * range boundaires. Let's take the following range representation, 162 * where the square brackets indicate the boundaries: 163 * 164 * [<p>Some <b>sample] text</b> 165 * 166 * While walking forward into the above range, the following nodes are 167 * returned: <p>, "Some ", <b> and "sample". Going 168 * backwards instead we have: "sample" and "Some ". So note that the 169 * walker always returns nodes when "entering" them, but not when 170 * "leaving" them. The guard function is instead called both when 171 * entering and leaving nodes. 172 * 173 * @constructor 174 * @param {CKEDITOR.dom.range} range The range within which walk. 175 */ 176 $ : function( range ) 177 { 178 this.range = range; 179 180 /** 181 * A function executed for every matched node, to check whether 182 * it's to be considered into the walk or not. If not provided, all 183 * matched nodes are considered good. 184 * If the function returns "false" the node is ignored. 185 * @name CKEDITOR.dom.walker.prototype.evaluator 186 * @property 187 * @type Function 188 */ 189 // this.evaluator = null; 190 191 /** 192 * A function executed for every node the walk pass by to check 193 * whether the walk is to be finished. It's called when both 194 * entering and exiting nodes, as well as for the matched nodes. 195 * If this function returns "false", the walking ends and no more 196 * nodes are evaluated. 197 * @name CKEDITOR.dom.walker.prototype.guard 198 * @property 199 * @type Function 200 */ 201 // this.guard = null; 202 203 /** @private */ 204 this._ = {}; 205 }, 206 207 // statics : 208 // { 209 // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes. 210 // * @param {CKEDITOR.dom.node} startNode The node from wich the walk 211 // * will start. 212 // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered 213 // * in the walk. No more nodes are retrieved after touching or 214 // * passing it. If not provided, the walker stops at the 215 // * <body> closing boundary. 216 // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the 217 // * provided nodes. 218 // */ 219 // createOnNodes : function( startNode, endNode, startInclusive, endInclusive ) 220 // { 221 // var range = new CKEDITOR.dom.range(); 222 // if ( startNode ) 223 // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ; 224 // else 225 // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ; 226 // 227 // if ( endNode ) 228 // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ; 229 // else 230 // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ; 231 // 232 // return new CKEDITOR.dom.walker( range ); 233 // } 234 // }, 235 // 236 proto : 237 { 238 /** 239 * Stop walking. No more nodes are retrieved if this function gets 240 * called. 241 */ 242 end : function() 243 { 244 this._.end = 1; 245 }, 246 247 /** 248 * Retrieves the next node (at right). 249 * @returns {CKEDITOR.dom.node} The next node or null if no more 250 * nodes are available. 251 */ 252 next : function() 253 { 254 return iterate.call( this ); 255 }, 256 257 /** 258 * Retrieves the previous node (at left). 259 * @returns {CKEDITOR.dom.node} The previous node or null if no more 260 * nodes are available. 261 */ 262 previous : function() 263 { 264 return iterate.call( this, true ); 265 }, 266 267 /** 268 * Check all nodes at right, executing the evaluation fuction. 269 * @returns {Boolean} "false" if the evaluator function returned 270 * "false" for any of the matched nodes. Otherwise "true". 271 */ 272 checkForward : function() 273 { 274 return iterate.call( this, false, true ) !== false; 275 }, 276 277 /** 278 * Check all nodes at left, executing the evaluation fuction. 279 * @returns {Boolean} "false" if the evaluator function returned 280 * "false" for any of the matched nodes. Otherwise "true". 281 */ 282 checkBackward : function() 283 { 284 return iterate.call( this, true, true ) !== false; 285 }, 286 287 /** 288 * Executes a full walk forward (to the right), until no more nodes 289 * are available, returning the last valid node. 290 * @returns {CKEDITOR.dom.node} The last node at the right or null 291 * if no valid nodes are available. 292 */ 293 lastForward : function() 294 { 295 return iterateToLast.call( this ); 296 }, 297 298 /** 299 * Executes a full walk backwards (to the left), until no more nodes 300 * are available, returning the last valid node. 301 * @returns {CKEDITOR.dom.node} The last node at the left or null 302 * if no valid nodes are available. 303 */ 304 lastBackward : function() 305 { 306 return iterateToLast.call( this, true ); 307 } 308 309 } 310 }); 311 312 /* 313 * Anything whose display computed style is block, list-item, table, 314 * table-row-group, table-header-group, table-footer-group, table-row, 315 * table-column-group, table-column, table-cell, table-caption, or whose node 316 * name is hr, br (when enterMode is br only) is a block boundary. 317 */ 318 var blockBoundaryDisplayMatch = 319 { 320 block : 1, 321 'list-item' : 1, 322 table : 1, 323 'table-row-group' : 1, 324 'table-header-group' : 1, 325 'table-footer-group' : 1, 326 'table-row' : 1, 327 'table-column-group' : 1, 328 'table-column' : 1, 329 'table-cell' : 1, 330 'table-caption' : 1 331 }, 332 blockBoundaryNodeNameMatch = { hr : 1 }; 333 334 CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) 335 { 336 var nodeNameMatches = CKEDITOR.tools.extend( {}, 337 blockBoundaryNodeNameMatch, customNodeNames || {} ); 338 339 return blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] || 340 nodeNameMatches[ this.getName() ]; 341 }; 342 343 CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) 344 { 345 return function( node , type ) 346 { 347 return ! ( node.type == CKEDITOR.NODE_ELEMENT 348 && node.isBlockBoundary( customNodeNames ) ); 349 }; 350 }; 351 352 CKEDITOR.dom.walker.listItemBoundary = function() 353 { 354 return this.blockBoundary( { br : 1 } ); 355 }; 356 /** 357 * Whether the node is a bookmark node's inner text node. 358 */ 359 CKEDITOR.dom.walker.bookmarkContents = function( node ) 360 { 361 }, 362 363 /** 364 * Whether the to-be-evaluated node is a bookmark node OR bookmark node 365 * inner contents. 366 * @param {Boolean} contentOnly Whether only test againt the text content of 367 * bookmark node instead of the element itself(default). 368 * @param {Boolean} isReject Whether should return 'false' for the bookmark 369 * node instead of 'true'(default). 370 */ 371 CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) 372 { 373 function isBookmarkNode( node ) 374 { 375 return ( node && node.getName 376 && node.getName() == 'span' 377 && node.hasAttribute('_fck_bookmark') ); 378 } 379 380 return function( node ) 381 { 382 var retval, parent; 383 // Is bookmark inner text node? 384 retval = ( node && !node.getName && ( parent = node.getParent() ) 385 && isBookmarkNode( parent ) ); 386 // Is bookmark node? 387 retval = contentOnly ? retval : retval || isBookmarkNode( node ); 388 return isReject ? !retval : !!retval; 389 }; 390 }; 391 392 })(); 393