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 ( !cloneId ) 100 { 101 var removeIds = function( node ) 102 { 103 if ( node.nodeType != CKEDITOR.NODE_ELEMENT ) 104 return; 105 106 node.removeAttribute( 'id', false ) ; 107 node.removeAttribute( '_cke_expando', false ) ; 108 109 var childs = node.childNodes; 110 for ( var i=0 ; i < childs.length ; i++ ) 111 removeIds( childs[ i ] ); 112 }; 113 114 // The "id" attribute should never be cloned to avoid duplication. 115 removeIds( $clone ); 116 } 117 118 return new CKEDITOR.dom.node( $clone ); 119 }, 120 121 hasPrevious : function() 122 { 123 return !!this.$.previousSibling; 124 }, 125 126 hasNext : function() 127 { 128 return !!this.$.nextSibling; 129 }, 130 131 /** 132 * Inserts this element after a node. 133 * @param {CKEDITOR.dom.node} node The that will preceed this element. 134 * @returns {CKEDITOR.dom.node} The node preceeding this one after 135 * insertion. 136 * @example 137 * var em = new CKEDITOR.dom.element( 'em' ); 138 * var strong = new CKEDITOR.dom.element( 'strong' ); 139 * strong.insertAfter( em ); 140 * 141 * // result: "<em></em><strong></strong>" 142 */ 143 insertAfter : function( node ) 144 { 145 node.$.parentNode.insertBefore( this.$, node.$.nextSibling ); 146 return node; 147 }, 148 149 /** 150 * Inserts this element before a node. 151 * @param {CKEDITOR.dom.node} node The that will be after this element. 152 * @returns {CKEDITOR.dom.node} The node being inserted. 153 * @example 154 * var em = new CKEDITOR.dom.element( 'em' ); 155 * var strong = new CKEDITOR.dom.element( 'strong' ); 156 * strong.insertBefore( em ); 157 * 158 * // result: "<strong></strong><em></em>" 159 */ 160 insertBefore : function( node ) 161 { 162 node.$.parentNode.insertBefore( this.$, node.$ ); 163 return node; 164 }, 165 166 insertBeforeMe : function( node ) 167 { 168 this.$.parentNode.insertBefore( node.$, this.$ ); 169 return node; 170 }, 171 172 /** 173 * Retrieves a uniquely identifiable tree address for this node. 174 * The tree address returns is an array of integers, with each integer 175 * indicating a child index of a DOM node, starting from 176 * document.documentElement. 177 * 178 * For example, assuming <body> is the second child from <html> (<head> 179 * being the first), and we'd like to address the third child under the 180 * fourth child of body, the tree address returned would be: 181 * [1, 3, 2] 182 * 183 * The tree address cannot be used for finding back the DOM tree node once 184 * the DOM tree structure has been modified. 185 */ 186 getAddress : function( normalized ) 187 { 188 var address = []; 189 var $documentElement = this.getDocument().$.documentElement; 190 var node = this.$; 191 192 while ( node && node != $documentElement ) 193 { 194 var parentNode = node.parentNode; 195 var currentIndex = -1; 196 197 for ( var i = 0 ; i < parentNode.childNodes.length ; i++ ) 198 { 199 var candidate = parentNode.childNodes[i]; 200 201 if ( normalized && 202 candidate.nodeType == 3 && 203 candidate.previousSibling && 204 candidate.previousSibling.nodeType == 3 ) 205 { 206 continue; 207 } 208 209 currentIndex++; 210 211 if ( candidate == node ) 212 break; 213 } 214 215 address.unshift( currentIndex ); 216 217 node = node.parentNode; 218 } 219 220 return address; 221 }, 222 223 /** 224 * Gets the document containing this element. 225 * @returns {CKEDITOR.dom.document} The document. 226 * @example 227 * var element = CKEDITOR.document.getById( 'example' ); 228 * alert( <b>element.getDocument().equals( CKEDITOR.document )</b> ); // "true" 229 */ 230 getDocument : function() 231 { 232 var document = new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument ); 233 234 return ( 235 /** @ignore */ 236 this.getDocument = function() 237 { 238 return document; 239 })(); 240 }, 241 242 getIndex : function() 243 { 244 var $ = this.$; 245 246 var currentNode = $.parentNode && $.parentNode.firstChild; 247 var currentIndex = -1; 248 249 while ( currentNode ) 250 { 251 currentIndex++; 252 253 if ( currentNode == $ ) 254 return currentIndex; 255 256 currentNode = currentNode.nextSibling; 257 } 258 259 return -1; 260 }, 261 262 getNextSourceNode : function( startFromSibling, nodeType, guard ) 263 { 264 // If "guard" is a node, transform it in a function. 265 if ( guard && !guard.call ) 266 { 267 var guardNode = guard; 268 guard = function( node ) 269 { 270 return !node.equals( guardNode ); 271 }; 272 } 273 274 var node = ( !startFromSibling && this.getFirst && this.getFirst() ), 275 parent; 276 277 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 278 // send the 'moving out' signal even we don't actually dive into. 279 if ( !node ) 280 { 281 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) 282 return null; 283 node = this.getNext(); 284 } 285 286 while ( !node && ( parent = ( parent || this ).getParent() ) ) 287 { 288 // The guard check sends the "true" paramenter to indicate that 289 // we are moving "out" of the element. 290 if ( guard && guard( parent, true ) === false ) 291 return null; 292 293 node = parent.getNext(); 294 } 295 296 if ( !node ) 297 return null; 298 299 if ( guard && guard( node ) === false ) 300 return null; 301 302 if ( nodeType && nodeType != node.type ) 303 return node.getNextSourceNode( false, nodeType, guard ); 304 305 return node; 306 }, 307 308 getPreviousSourceNode : function( startFromSibling, nodeType, guard ) 309 { 310 if ( guard && !guard.call ) 311 { 312 var guardNode = guard; 313 guard = function( node ) 314 { 315 return !node.equals( guardNode ); 316 }; 317 } 318 319 var node = ( !startFromSibling && this.getLast && this.getLast() ), 320 parent; 321 322 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 323 // send the 'moving out' signal even we don't actually dive into. 324 if ( !node ) 325 { 326 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) 327 return null; 328 node = this.getPrevious(); 329 } 330 331 while ( !node && ( parent = ( parent || this ).getParent() ) ) 332 { 333 // The guard check sends the "true" paramenter to indicate that 334 // we are moving "out" of the element. 335 if ( guard && guard( parent, true ) === false ) 336 return null; 337 338 node = parent.getPrevious(); 339 } 340 341 if ( !node ) 342 return null; 343 344 if ( guard && guard( node ) === false ) 345 return null; 346 347 if ( nodeType && node.type != nodeType ) 348 return node.getPreviousSourceNode( false, nodeType, guard ); 349 350 return node; 351 }, 352 353 getPrevious : function( ignoreSpaces ) 354 { 355 var previous = this.$.previousSibling; 356 while ( ignoreSpaces && previous && ( previous.nodeType == CKEDITOR.NODE_TEXT ) 357 && !CKEDITOR.tools.trim( previous.nodeValue ) ) 358 previous = previous.previousSibling; 359 360 return previous ? new CKEDITOR.dom.node( previous ) : null; 361 }, 362 363 /** 364 * Gets the node that follows this element in its parent's child list. 365 * @param {Boolean} ignoreSpaces Whether should ignore empty text nodes. 366 * @returns {CKEDITOR.dom.node} The next node or null if not 367 * available. 368 * @example 369 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b> <i>next</i></div>' ); 370 * var first = <b>element.getFirst().getNext()</b>; 371 * alert( first.getName() ); // "i" 372 */ 373 getNext : function( ignoreSpaces ) 374 { 375 var next = this.$.nextSibling; 376 while ( ignoreSpaces && next && ( next.nodeType == CKEDITOR.NODE_TEXT ) 377 && !CKEDITOR.tools.trim( next.nodeValue ) ) 378 next = next.nextSibling; 379 380 return next ? new CKEDITOR.dom.node( next ) : null; 381 }, 382 383 /** 384 * Gets the parent element for this node. 385 * @returns {CKEDITOR.dom.element} The parent element. 386 * @example 387 * var node = editor.document.getBody().getFirst(); 388 * var parent = node.<b>getParent()</b>; 389 * alert( node.getName() ); // "body" 390 */ 391 getParent : function() 392 { 393 var parent = this.$.parentNode; 394 return ( parent && parent.nodeType == 1 ) ? new CKEDITOR.dom.node( parent ) : null; 395 }, 396 397 getParents : function() 398 { 399 var node = this; 400 var parents = []; 401 402 do 403 { 404 parents.unshift( node ); 405 } 406 while ( ( node = node.getParent() ) ) 407 408 return parents; 409 }, 410 411 getCommonAncestor : function( node ) 412 { 413 if ( node.equals( this ) ) 414 return this; 415 416 if ( node.contains && node.contains( this ) ) 417 return node; 418 419 var start = this.contains ? this : this.getParent(); 420 421 do 422 { 423 if ( start.contains( node ) ) 424 return start; 425 } 426 while ( ( start = start.getParent() ) ); 427 428 return null; 429 }, 430 431 getPosition : function( otherNode ) 432 { 433 var $ = this.$; 434 var $other = otherNode.$; 435 436 if ( $.compareDocumentPosition ) 437 return $.compareDocumentPosition( $other ); 438 439 // IE and Safari have no support for compareDocumentPosition. 440 441 if ( $ == $other ) 442 return CKEDITOR.POSITION_IDENTICAL; 443 444 // Only element nodes support contains and sourceIndex. 445 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) 446 { 447 if ( $.contains ) 448 { 449 if ( $.contains( $other ) ) 450 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; 451 452 if ( $other.contains( $ ) ) 453 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 454 } 455 456 if ( 'sourceIndex' in $ ) 457 { 458 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : 459 ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : 460 CKEDITOR.POSITION_FOLLOWING; 461 } 462 } 463 464 // For nodes that don't support compareDocumentPosition, contains 465 // or sourceIndex, their "address" is compared. 466 467 var addressOfThis = this.getAddress(), 468 addressOfOther = otherNode.getAddress(), 469 minLevel = Math.min( addressOfThis.length, addressOfOther.length ); 470 471 // Determinate preceed/follow relationship. 472 for ( var i = 0 ; i <= minLevel - 1 ; i++ ) 473 { 474 if ( addressOfThis[ i ] != addressOfOther[ i ] ) 475 { 476 if ( i < minLevel ) 477 { 478 return addressOfThis[ i ] < addressOfOther[ i ] ? 479 CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; 480 } 481 break; 482 } 483 } 484 485 // Determinate contains/contained relationship. 486 return ( addressOfThis.length < addressOfOther.length ) ? 487 CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : 488 CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 489 }, 490 491 /** 492 * Gets the closes ancestor node of a specified node name. 493 * @param {String} name Node name of ancestor node. 494 * @param {Boolean} includeSelf (Optional) Whether to include the current 495 * node in the calculation or not. 496 * @returns {CKEDITOR.dom.node} Ancestor node. 497 */ 498 getAscendant : function( name, includeSelf ) 499 { 500 var $ = this.$; 501 502 if ( !includeSelf ) 503 $ = $.parentNode; 504 505 while ( $ ) 506 { 507 if ( $.nodeName && $.nodeName.toLowerCase() == name ) 508 return new CKEDITOR.dom.node( $ ); 509 510 $ = $.parentNode; 511 } 512 return null; 513 }, 514 515 hasAscendant : function( name, includeSelf ) 516 { 517 var $ = this.$; 518 519 if ( !includeSelf ) 520 $ = $.parentNode; 521 522 while ( $ ) 523 { 524 if ( $.nodeName && $.nodeName.toLowerCase() == name ) 525 return true; 526 527 $ = $.parentNode; 528 } 529 return false; 530 }, 531 532 move : function( target, toStart ) 533 { 534 target.append( this.remove(), toStart ); 535 }, 536 537 /** 538 * Removes this node from the document DOM. 539 * @param {Boolean} [preserveChildren] Indicates that the children 540 * elements must remain in the document, removing only the outer 541 * tags. 542 * @example 543 * var element = CKEDITOR.dom.element.getById( 'MyElement' ); 544 * <b>element.remove()</b>; 545 */ 546 remove : function( preserveChildren ) 547 { 548 var $ = this.$; 549 var parent = $.parentNode; 550 551 if ( parent ) 552 { 553 if ( preserveChildren ) 554 { 555 // Move all children before the node. 556 for ( var child ; ( child = $.firstChild ) ; ) 557 { 558 parent.insertBefore( $.removeChild( child ), $ ); 559 } 560 } 561 562 parent.removeChild( $ ); 563 } 564 565 return this; 566 }, 567 568 replace : function( nodeToReplace ) 569 { 570 this.insertBefore( nodeToReplace ); 571 nodeToReplace.remove(); 572 }, 573 574 trim : function() 575 { 576 this.ltrim(); 577 this.rtrim(); 578 }, 579 580 ltrim : function() 581 { 582 var child; 583 while ( this.getFirst && ( child = this.getFirst() ) ) 584 { 585 if ( child.type == CKEDITOR.NODE_TEXT ) 586 { 587 var trimmed = CKEDITOR.tools.ltrim( child.getText() ), 588 originalLength = child.getLength(); 589 590 if ( !trimmed ) 591 { 592 child.remove(); 593 continue; 594 } 595 else if ( trimmed.length < originalLength ) 596 { 597 child.split( originalLength - trimmed.length ); 598 599 // IE BUG: child.remove() may raise JavaScript errors here. (#81) 600 this.$.removeChild( this.$.firstChild ); 601 } 602 } 603 break; 604 } 605 }, 606 607 rtrim : function() 608 { 609 var child; 610 while ( this.getLast && ( child = this.getLast() ) ) 611 { 612 if ( child.type == CKEDITOR.NODE_TEXT ) 613 { 614 var trimmed = CKEDITOR.tools.rtrim( child.getText() ), 615 originalLength = child.getLength(); 616 617 if ( !trimmed ) 618 { 619 child.remove(); 620 continue; 621 } 622 else if ( trimmed.length < originalLength ) 623 { 624 child.split( trimmed.length ); 625 626 // IE BUG: child.getNext().remove() may raise JavaScript errors here. 627 // (#81) 628 this.$.lastChild.parentNode.removeChild( this.$.lastChild ); 629 } 630 } 631 break; 632 } 633 634 if ( !CKEDITOR.env.ie && !CKEDITOR.env.opera ) 635 { 636 child = this.$.lastChild; 637 638 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) 639 { 640 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). 641 child.parentNode.removeChild( child ) ; 642 } 643 } 644 } 645 } 646 ); 647