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 * @fileOverview Defines the {@link CKEDITOR.dom.node} class, which is the base 8 * class for classes that represent DOM nodes. 9 */ 10 11 /** 12 * Base class for classes representing DOM nodes. This constructor may return 13 * and instance of classes that inherits this class, like 14 * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}. 15 * @augments CKEDITOR.dom.domObject 16 * @param {Object} domNode A native DOM node. 17 * @constructor 18 * @see CKEDITOR.dom.element 19 * @see CKEDITOR.dom.text 20 * @example 21 */ 22 CKEDITOR.dom.node = function( domNode ) 23 { 24 if ( domNode ) 25 { 26 switch ( domNode.nodeType ) 27 { 28 case CKEDITOR.NODE_ELEMENT : 29 return new CKEDITOR.dom.element( domNode ); 30 31 case CKEDITOR.NODE_TEXT : 32 return new CKEDITOR.dom.text( domNode ); 33 } 34 35 // Call the base constructor. 36 CKEDITOR.dom.domObject.call( this, domNode ); 37 } 38 39 return this; 40 }; 41 42 CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject(); 43 44 /** 45 * Element node type. 46 * @constant 47 * @example 48 */ 49 CKEDITOR.NODE_ELEMENT = 1; 50 51 /** 52 * Text node type. 53 * @constant 54 * @example 55 */ 56 CKEDITOR.NODE_TEXT = 3; 57 58 /** 59 * Comment node type. 60 * @constant 61 * @example 62 */ 63 CKEDITOR.NODE_COMMENT = 8; 64 65 CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11; 66 67 CKEDITOR.POSITION_IDENTICAL = 0; 68 CKEDITOR.POSITION_DISCONNECTED = 1; 69 CKEDITOR.POSITION_FOLLOWING = 2; 70 CKEDITOR.POSITION_PRECEDING = 4; 71 CKEDITOR.POSITION_IS_CONTAINED = 8; 72 CKEDITOR.POSITION_CONTAINS = 16; 73 74 CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, 75 /** @lends CKEDITOR.dom.node.prototype */ 76 { 77 /** 78 * Makes this node child of another element. 79 * @param {CKEDITOR.dom.element} element The target element to which append 80 * this node. 81 * @returns {CKEDITOR.dom.element} The target element. 82 * @example 83 * var p = new CKEDITOR.dom.element( 'p' ); 84 * var strong = new CKEDITOR.dom.element( 'strong' ); 85 * strong.appendTo( p ); 86 * 87 * // result: "<p><strong></strong></p>" 88 */ 89 appendTo : function( element, toStart ) 90 { 91 element.append( this, toStart ); 92 return element; 93 }, 94 95 clone : function( includeChildren, cloneId ) 96 { 97 var $clone = this.$.cloneNode( includeChildren ); 98 99 if ( this.type == CKEDITOR.NODE_ELEMENT && !cloneId ) 100 { 101 // The "id" attribute should never be cloned to avoid duplication. 102 $clone.removeAttribute( 'id', false ) ; 103 $clone.removeAttribute( '_cke_expando', false ) ; 104 } 105 106 return new CKEDITOR.dom.node( $clone ); 107 }, 108 109 hasPrevious : function() 110 { 111 return !!this.$.previousSibling; 112 }, 113 114 hasNext : function() 115 { 116 return !!this.$.nextSibling; 117 }, 118 119 /** 120 * Inserts this element after a node. 121 * @param {CKEDITOR.dom.node} node The that will preceed this element. 122 * @returns {CKEDITOR.dom.node} The node preceeding this one after 123 * insertion. 124 * @example 125 * var em = new CKEDITOR.dom.element( 'em' ); 126 * var strong = new CKEDITOR.dom.element( 'strong' ); 127 * strong.insertAfter( em ); 128 * 129 * // result: "<em></em><strong></strong>" 130 */ 131 insertAfter : function( node ) 132 { 133 node.$.parentNode.insertBefore( this.$, node.$.nextSibling ); 134 return node; 135 }, 136 137 /** 138 * Inserts this element before a node. 139 * @param {CKEDITOR.dom.node} node The that will be after this element. 140 * @returns {CKEDITOR.dom.node} The node being inserted. 141 * @example 142 * var em = new CKEDITOR.dom.element( 'em' ); 143 * var strong = new CKEDITOR.dom.element( 'strong' ); 144 * strong.insertBefore( em ); 145 * 146 * // result: "<strong></strong><em></em>" 147 */ 148 insertBefore : function( node ) 149 { 150 node.$.parentNode.insertBefore( this.$, node.$ ); 151 return node; 152 }, 153 154 insertBeforeMe : function( node ) 155 { 156 this.$.parentNode.insertBefore( node.$, this.$ ); 157 return node; 158 }, 159 160 /** 161 * Retrieves a uniquely identifiable tree address for this node. 162 * The tree address returns is an array of integers, with each integer 163 * indicating a child index of a DOM node, starting from 164 * document.documentElement. 165 * 166 * For example, assuming <body> is the second child from <html> (<head> 167 * being the first), and we'd like to address the third child under the 168 * fourth child of body, the tree address returned would be: 169 * [1, 3, 2] 170 * 171 * The tree address cannot be used for finding back the DOM tree node once 172 * the DOM tree structure has been modified. 173 */ 174 getAddress : function( normalized ) 175 { 176 var address = []; 177 var $documentElement = this.getDocument().$.documentElement; 178 var node = this.$; 179 180 while ( node && node != $documentElement ) 181 { 182 var parentNode = node.parentNode; 183 var currentIndex = -1; 184 185 for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) 186 { 187 var candidate = parentNode.childNodes[i]; 188 189 if ( normalized && 190 candidate.nodeType == 3 && 191 candidate.previousSibling && 192 candidate.previousSibling.nodeType == 3 ) 193 { 194 continue; 195 } 196 197 currentIndex++; 198 199 if ( candidate == node ) 200 break; 201 } 202 203 address.unshift( currentIndex ); 204 205 node = node.parentNode; 206 } 207 208 return address; 209 }, 210 211 /** 212 * Gets a DOM tree descendant under the current node. 213 * @param {Array|Number} indices The child index or array of child indices under the node. 214 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist. 215 * @example 216 * var strong = p.getChild(0); 217 */ 218 getChild : function( indices ) 219 { 220 var rawNode = this.$; 221 222 if ( !indices.slice ) 223 rawNode = rawNode.childNodes[ indices ]; 224 else 225 { 226 while ( indices.length > 0 && rawNode ) 227 rawNode = rawNode.childNodes[ indices.shift() ]; 228 } 229 230 return rawNode ? new CKEDITOR.dom.node( rawNode ) : null; 231 }, 232 233 getChildCount : function() 234 { 235 return this.$.childNodes.length; 236 }, 237 238 /** 239 * Gets the document containing this element. 240 * @returns {CKEDITOR.dom.document} The document. 241 * @example 242 * var element = CKEDITOR.document.getById( 'example' ); 243 * alert( <b>element.getDocument().equals( CKEDITOR.document )</b> ); // "true" 244 */ 245 getDocument : function() 246 { 247 var document = new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument ); 248 249 return ( 250 /** @ignore */ 251 this.getDocument = function() 252 { 253 return document; 254 })(); 255 }, 256 257 getIndex : function() 258 { 259 var $ = this.$; 260 261 var currentNode = $.parentNode && $.parentNode.firstChild; 262 var currentIndex = -1; 263 264 while ( currentNode ) 265 { 266 currentIndex++; 267 268 if ( currentNode == $ ) 269 return currentIndex; 270 271 currentNode = currentNode.nextSibling; 272 } 273 274 return -1; 275 }, 276 277 /** 278 * Gets the node following this node (next sibling). 279 * @returns {CKEDITOR.dom.node} The next node. 280 */ 281 getNext : function() 282 { 283 var next = this.$.nextSibling; 284 return next ? new CKEDITOR.dom.node( next ) : null; 285 }, 286 287 getNextSourceNode : function( startFromSibling, nodeType ) 288 { 289 var $ = this.$; 290 291 var node = ( !startFromSibling && $.firstChild ) ? 292 $.firstChild : 293 $.nextSibling; 294 295 var parent; 296 297 while ( !node && ( parent = ( parent || $ ).parentNode ) ) 298 node = parent.nextSibling; 299 300 if ( !node ) 301 return null; 302 303 if ( nodeType && nodeType != node.nodeType ) 304 return arguments.callee.call( { $ : node }, false, nodeType ); 305 306 return new CKEDITOR.dom.node( node ); 307 }, 308 309 getPreviousSourceNode : function( startFromSibling, nodeType ) 310 { 311 var $ = this.$; 312 313 var node = ( !startFromSibling && $.lastChild ) ? 314 $.lastChild : 315 $.previousSibling; 316 317 var parent; 318 319 while ( !node && ( parent = ( parent || $ ).parentNode ) ) 320 node = parent.previousSibling; 321 322 if ( !node ) 323 return null; 324 325 if ( nodeType && node.nodeType != nodeType ) 326 return arguments.callee.call( { $ : node }, false, nodeType ); 327 328 return new CKEDITOR.dom.node( node ); 329 }, 330 331 getPrevious : function() 332 { 333 var previous = this.$.previousSibling; 334 return previous ? new CKEDITOR.dom.node( previous ) : null; 335 }, 336 337 /** 338 * Gets the parent element for this node. 339 * @returns {CKEDITOR.dom.element} The parent element. 340 * @example 341 * var node = editor.document.getBody().getFirst(); 342 * var parent = node.<b>getParent()</b>; 343 * alert( node.getName() ); // "body" 344 */ 345 getParent : function() 346 { 347 var parent = this.$.parentNode; 348 return ( parent && parent.nodeType == 1 ) ? new CKEDITOR.dom.node( parent ) : null; 349 }, 350 351 getParents : function() 352 { 353 var node = this; 354 var parents = []; 355 356 do 357 { 358 parents.unshift( node ); 359 } 360 while ( ( node = node.getParent() ) ) 361 362 return parents; 363 }, 364 365 getCommonAncestor : function( node ) 366 { 367 if ( node.equals( this ) ) 368 return this; 369 370 if ( node.contains && node.contains( this ) ) 371 return node; 372 373 var start = this.contains ? this : this.getParent(); 374 375 do 376 { 377 if ( start.contains( node ) ) 378 return start; 379 } 380 while ( ( start = start.getParent() ) ); 381 382 return null; 383 }, 384 385 getPosition : function( otherNode ) 386 { 387 var $ = this.$; 388 var $other = otherNode.$; 389 390 if ( $.compareDocumentPosition ) 391 return $.compareDocumentPosition( $other ); 392 393 // IE and Safari have no support for compareDocumentPosition. 394 395 if ( $ == $other ) 396 return CKEDITOR.POSITION_IDENTICAL; 397 398 // Handle non element nodes (don't support contains nor sourceIndex). 399 if ( this.type != CKEDITOR.NODE_ELEMENT || otherNode.type != CKEDITOR.NODE_ELEMENT ) 400 { 401 if ( $.parentNode == $other ) 402 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 403 else if ( $other.parentNode == $ ) 404 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; 405 else if ( $.parentNode == $other.parentNode ) 406 return this.getIndex() < otherNode.getIndex() ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; 407 else 408 { 409 $ = $.parentNode; 410 $other = $other.parentNode; 411 } 412 } 413 414 if ( $.contains( $other ) ) 415 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; 416 417 if ( $other.contains( $ ) ) 418 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 419 420 if ( 'sourceIndex' in $ ) 421 { 422 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : 423 ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : 424 CKEDITOR.POSITION_FOLLOWING; 425 } 426 427 // WebKit has no support for sourceIndex. 428 429 var doc = this.getDocument().$; 430 431 var range1 = doc.createRange(); 432 var range2 = doc.createRange(); 433 434 range1.selectNode( $ ); 435 range2.selectNode( $other ); 436 437 return range1.compareBoundaryPoints( 1, range2 ) > 0 ? 438 CKEDITOR.POSITION_FOLLOWING : 439 CKEDITOR.POSITION_PRECEDING; 440 }, 441 442 /** 443 * Gets the closes ancestor node of a specified node name. 444 * @param {String} name Node name of ancestor node. 445 * @param {Boolean} includeSelf (Optional) Whether to include the current 446 * node in the calculation or not. 447 * @returns {CKEDITOR.dom.node} Ancestor node. 448 */ 449 getAscendant : function( name, includeSelf ) 450 { 451 var $ = this.$; 452 453 if ( !includeSelf ) 454 $ = $.parentNode; 455 456 while ( $ ) 457 { 458 if ( $.nodeName && $.nodeName.toLowerCase() == name ) 459 return new CKEDITOR.dom.node( $ ); 460 461 $ = $.parentNode; 462 } 463 return null; 464 }, 465 466 hasAscendant : function( name, includeSelf ) 467 { 468 var $ = this.$; 469 470 if ( !includeSelf ) 471 $ = $.parentNode; 472 473 while ( $ ) 474 { 475 if ( $.nodeName && $.nodeName.toLowerCase() == name ) 476 return true; 477 478 $ = $.parentNode; 479 } 480 return false; 481 }, 482 483 move : function( target, toStart ) 484 { 485 target.append( this.remove(), toStart ); 486 }, 487 488 /** 489 * Removes this node from the document DOM. 490 * @param {Boolean} [preserveChildren] Indicates that the children 491 * elements must remain in the document, removing only the outer 492 * tags. 493 * @example 494 * var element = CKEDITOR.dom.element.getById( 'MyElement' ); 495 * <b>element.remove()</b>; 496 */ 497 remove : function( preserveChildren ) 498 { 499 var $ = this.$; 500 var parent = $.parentNode; 501 502 if ( parent ) 503 { 504 if ( preserveChildren ) 505 { 506 // Move all children before the node. 507 for ( var child ; ( child = $.firstChild ) ; ) 508 { 509 parent.insertBefore( $.removeChild( child ), $ ); 510 } 511 } 512 513 parent.removeChild( $ ); 514 } 515 516 return this; 517 }, 518 519 replace : function( nodeToReplace ) 520 { 521 this.insertBefore( nodeToReplace ); 522 nodeToReplace.remove(); 523 }, 524 525 trim : function() 526 { 527 this.ltrim(); 528 this.rtrim(); 529 }, 530 531 ltrim : function() 532 { 533 var child; 534 while ( this.getFirst && ( child = this.getFirst() ) ) 535 { 536 if ( child.type == CKEDITOR.NODE_TEXT ) 537 { 538 var trimmed = CKEDITOR.tools.ltrim( child.getText() ), 539 originalLength = child.getLength(); 540 541 if ( trimmed.length == 0 ) 542 { 543 child.remove(); 544 continue; 545 } 546 else if ( trimmed.length < originalLength ) 547 { 548 child.split( originalLength - trimmed.length ); 549 550 // IE BUG: child.remove() may raise JavaScript errors here. (#81) 551 this.$.removeChild( this.$.firstChild ); 552 } 553 } 554 break; 555 } 556 }, 557 558 rtrim : function() 559 { 560 var child; 561 while ( this.getLast && ( child = this.getLast() ) ) 562 { 563 if ( child.type == CKEDITOR.NODE_TEXT ) 564 { 565 var trimmed = CKEDITOR.tools.rtrim( child.getText() ), 566 originalLength = child.getLength(); 567 568 if ( trimmed.length == 0 ) 569 { 570 child.remove(); 571 continue; 572 } 573 else if ( trimmed.length < originalLength ) 574 { 575 child.split( trimmed.length ); 576 577 // IE BUG: child.getNext().remove() may raise JavaScript errors here. 578 // (#81) 579 this.$.lastChild.parentNode.removeChild( this.$.lastChild ); 580 } 581 } 582 break; 583 } 584 585 if ( !CKEDITOR.env.ie && !CKEDITOR.env.opera ) 586 { 587 child = this.$.lastChild; 588 589 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) 590 { 591 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). 592 child.parentNode.removeChild( child ) ; 593 } 594 } 595 } 596 } 597 ); 598