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 /** 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 * an instance of a class that inherits from 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 var type = domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' 27 : domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' 28 : domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' 29 : domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' 30 : 'domObject'; // Call the base constructor otherwise. 31 32 return new CKEDITOR.dom[ type ]( domNode ); 33 } 34 35 return this; 36 }; 37 38 CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject(); 39 40 /** 41 * Element node type. 42 * @constant 43 * @example 44 */ 45 CKEDITOR.NODE_ELEMENT = 1; 46 47 /** 48 * Document node type. 49 * @constant 50 * @example 51 */ 52 CKEDITOR.NODE_DOCUMENT = 9; 53 54 /** 55 * Text node type. 56 * @constant 57 * @example 58 */ 59 CKEDITOR.NODE_TEXT = 3; 60 61 /** 62 * Comment node type. 63 * @constant 64 * @example 65 */ 66 CKEDITOR.NODE_COMMENT = 8; 67 68 CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11; 69 70 CKEDITOR.POSITION_IDENTICAL = 0; 71 CKEDITOR.POSITION_DISCONNECTED = 1; 72 CKEDITOR.POSITION_FOLLOWING = 2; 73 CKEDITOR.POSITION_PRECEDING = 4; 74 CKEDITOR.POSITION_IS_CONTAINED = 8; 75 CKEDITOR.POSITION_CONTAINS = 16; 76 77 CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, 78 /** @lends CKEDITOR.dom.node.prototype */ 79 { 80 /** 81 * Makes this node a child of another element. 82 * @param {CKEDITOR.dom.element} element The target element to which 83 * this node will be appended. 84 * @returns {CKEDITOR.dom.element} The target element. 85 * @example 86 * var p = new CKEDITOR.dom.element( 'p' ); 87 * var strong = new CKEDITOR.dom.element( 'strong' ); 88 * strong.appendTo( p ); 89 * 90 * // result: "<p><strong></strong></p>" 91 */ 92 appendTo : function( element, toStart ) 93 { 94 element.append( this, toStart ); 95 return element; 96 }, 97 98 clone : function( includeChildren, cloneId ) 99 { 100 var $clone = this.$.cloneNode( includeChildren ); 101 102 var removeIds = function( node ) 103 { 104 if ( node.nodeType != CKEDITOR.NODE_ELEMENT ) 105 return; 106 107 if ( !cloneId ) 108 node.removeAttribute( 'id', false ); 109 110 node[ 'data-cke-expando' ] = undefined; 111 112 if ( includeChildren ) 113 { 114 var childs = node.childNodes; 115 for ( var i=0; i < childs.length; i++ ) 116 removeIds( childs[ i ] ); 117 } 118 }; 119 120 // The "id" attribute should never be cloned to avoid duplication. 121 removeIds( $clone ); 122 123 return new CKEDITOR.dom.node( $clone ); 124 }, 125 126 hasPrevious : function() 127 { 128 return !!this.$.previousSibling; 129 }, 130 131 hasNext : function() 132 { 133 return !!this.$.nextSibling; 134 }, 135 136 /** 137 * Inserts this element after a node. 138 * @param {CKEDITOR.dom.node} node The node that will precede this element. 139 * @returns {CKEDITOR.dom.node} The node preceding this one after 140 * insertion. 141 * @example 142 * var em = new CKEDITOR.dom.element( 'em' ); 143 * var strong = new CKEDITOR.dom.element( 'strong' ); 144 * strong.insertAfter( em ); 145 * 146 * // result: "<em></em><strong></strong>" 147 */ 148 insertAfter : function( node ) 149 { 150 node.$.parentNode.insertBefore( this.$, node.$.nextSibling ); 151 return node; 152 }, 153 154 /** 155 * Inserts this element before a node. 156 * @param {CKEDITOR.dom.node} node The node that will succeed this element. 157 * @returns {CKEDITOR.dom.node} The node being inserted. 158 * @example 159 * var em = new CKEDITOR.dom.element( 'em' ); 160 * var strong = new CKEDITOR.dom.element( 'strong' ); 161 * strong.insertBefore( em ); 162 * 163 * // result: "<strong></strong><em></em>" 164 */ 165 insertBefore : function( node ) 166 { 167 node.$.parentNode.insertBefore( this.$, node.$ ); 168 return node; 169 }, 170 171 insertBeforeMe : function( node ) 172 { 173 this.$.parentNode.insertBefore( node.$, this.$ ); 174 return node; 175 }, 176 177 /** 178 * Retrieves a uniquely identifiable tree address for this node. 179 * The tree address returned is an array of integers, with each integer 180 * indicating a child index of a DOM node, starting from 181 * <code>document.documentElement</code>. 182 * 183 * For example, assuming <code><body></code> is the second child 184 * of <code><html></code> (<code><head></code> being the first), 185 * and we would like to address the third child under the 186 * fourth child of <code><body></code>, the tree address returned would be: 187 * [1, 3, 2] 188 * 189 * The tree address cannot be used for finding back the DOM tree node once 190 * the DOM tree structure has been modified. 191 */ 192 getAddress : function( normalized ) 193 { 194 var address = []; 195 var $documentElement = this.getDocument().$.documentElement; 196 var node = this.$; 197 198 while ( node && node != $documentElement ) 199 { 200 var parentNode = node.parentNode; 201 202 if ( parentNode ) 203 { 204 // Get the node index. For performance, call getIndex 205 // directly, instead of creating a new node object. 206 address.unshift( this.getIndex.call( { $ : node }, normalized ) ); 207 } 208 209 node = parentNode; 210 } 211 212 return address; 213 }, 214 215 /** 216 * Gets the document containing this element. 217 * @returns {CKEDITOR.dom.document} The document. 218 * @example 219 * var element = CKEDITOR.document.getById( 'example' ); 220 * alert( <strong>element.getDocument().equals( CKEDITOR.document )</strong> ); // "true" 221 */ 222 getDocument : function() 223 { 224 return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument ); 225 }, 226 227 getIndex : function( normalized ) 228 { 229 // Attention: getAddress depends on this.$ 230 231 var current = this.$, 232 index = 0; 233 234 while ( ( current = current.previousSibling ) ) 235 { 236 // When normalizing, do not count it if this is an 237 // empty text node or if it's a text node following another one. 238 if ( normalized && current.nodeType == 3 && 239 ( !current.nodeValue.length || 240 ( current.previousSibling && current.previousSibling.nodeType == 3 ) ) ) 241 { 242 continue; 243 } 244 245 index++; 246 } 247 248 return index; 249 }, 250 251 getNextSourceNode : function( startFromSibling, nodeType, guard ) 252 { 253 // If "guard" is a node, transform it in a function. 254 if ( guard && !guard.call ) 255 { 256 var guardNode = guard; 257 guard = function( node ) 258 { 259 return !node.equals( guardNode ); 260 }; 261 } 262 263 var node = ( !startFromSibling && this.getFirst && this.getFirst() ), 264 parent; 265 266 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 267 // send the 'moving out' signal even we don't actually dive into. 268 if ( !node ) 269 { 270 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) 271 return null; 272 node = this.getNext(); 273 } 274 275 while ( !node && ( parent = ( parent || this ).getParent() ) ) 276 { 277 // The guard check sends the "true" paramenter to indicate that 278 // we are moving "out" of the element. 279 if ( guard && guard( parent, true ) === false ) 280 return null; 281 282 node = parent.getNext(); 283 } 284 285 if ( !node ) 286 return null; 287 288 if ( guard && guard( node ) === false ) 289 return null; 290 291 if ( nodeType && nodeType != node.type ) 292 return node.getNextSourceNode( false, nodeType, guard ); 293 294 return node; 295 }, 296 297 getPreviousSourceNode : function( startFromSibling, nodeType, guard ) 298 { 299 if ( guard && !guard.call ) 300 { 301 var guardNode = guard; 302 guard = function( node ) 303 { 304 return !node.equals( guardNode ); 305 }; 306 } 307 308 var node = ( !startFromSibling && this.getLast && this.getLast() ), 309 parent; 310 311 // Guarding when we're skipping the current element( no children or 'startFromSibling' ). 312 // send the 'moving out' signal even we don't actually dive into. 313 if ( !node ) 314 { 315 if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) 316 return null; 317 node = this.getPrevious(); 318 } 319 320 while ( !node && ( parent = ( parent || this ).getParent() ) ) 321 { 322 // The guard check sends the "true" paramenter to indicate that 323 // we are moving "out" of the element. 324 if ( guard && guard( parent, true ) === false ) 325 return null; 326 327 node = parent.getPrevious(); 328 } 329 330 if ( !node ) 331 return null; 332 333 if ( guard && guard( node ) === false ) 334 return null; 335 336 if ( nodeType && node.type != nodeType ) 337 return node.getPreviousSourceNode( false, nodeType, guard ); 338 339 return node; 340 }, 341 342 getPrevious : function( evaluator ) 343 { 344 var previous = this.$, retval; 345 do 346 { 347 previous = previous.previousSibling; 348 349 // Avoid returning the doc type node. 350 // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927 351 retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous ); 352 } 353 while ( retval && evaluator && !evaluator( retval ) ) 354 return retval; 355 }, 356 357 /** 358 * Gets the node that follows this element in its parent's child list. 359 * @param {Function} evaluator Filtering the result node. 360 * @returns {CKEDITOR.dom.node} The next node or null if not available. 361 * @example 362 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b> <i>next</i></div>' ); 363 * var first = <strong>element.getFirst().getNext()</strong>; 364 * alert( first.getName() ); // "i" 365 */ 366 getNext : function( evaluator ) 367 { 368 var next = this.$, retval; 369 do 370 { 371 next = next.nextSibling; 372 retval = next && new CKEDITOR.dom.node( next ); 373 } 374 while ( retval && evaluator && !evaluator( retval ) ) 375 return retval; 376 }, 377 378 /** 379 * Gets the parent element for this node. 380 * @returns {CKEDITOR.dom.element} The parent element. 381 * @example 382 * var node = editor.document.getBody().getFirst(); 383 * var parent = node.<strong>getParent()</strong>; 384 * alert( node.getName() ); // "body" 385 */ 386 getParent : function() 387 { 388 var parent = this.$.parentNode; 389 return ( parent && parent.nodeType == 1 ) ? new CKEDITOR.dom.node( parent ) : null; 390 }, 391 392 getParents : function( closerFirst ) 393 { 394 var node = this; 395 var parents = []; 396 397 do 398 { 399 parents[ closerFirst ? 'push' : 'unshift' ]( node ); 400 } 401 while ( ( node = node.getParent() ) ) 402 403 return parents; 404 }, 405 406 getCommonAncestor : function( node ) 407 { 408 if ( node.equals( this ) ) 409 return this; 410 411 if ( node.contains && node.contains( this ) ) 412 return node; 413 414 var start = this.contains ? this : this.getParent(); 415 416 do 417 { 418 if ( start.contains( node ) ) 419 return start; 420 } 421 while ( ( start = start.getParent() ) ); 422 423 return null; 424 }, 425 426 getPosition : function( otherNode ) 427 { 428 var $ = this.$; 429 var $other = otherNode.$; 430 431 if ( $.compareDocumentPosition ) 432 return $.compareDocumentPosition( $other ); 433 434 // IE and Safari have no support for compareDocumentPosition. 435 436 if ( $ == $other ) 437 return CKEDITOR.POSITION_IDENTICAL; 438 439 // Only element nodes support contains and sourceIndex. 440 if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) 441 { 442 if ( $.contains ) 443 { 444 if ( $.contains( $other ) ) 445 return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; 446 447 if ( $other.contains( $ ) ) 448 return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 449 } 450 451 if ( 'sourceIndex' in $ ) 452 { 453 return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : 454 ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : 455 CKEDITOR.POSITION_FOLLOWING; 456 } 457 } 458 459 // For nodes that don't support compareDocumentPosition, contains 460 // or sourceIndex, their "address" is compared. 461 462 var addressOfThis = this.getAddress(), 463 addressOfOther = otherNode.getAddress(), 464 minLevel = Math.min( addressOfThis.length, addressOfOther.length ); 465 466 // Determinate preceed/follow relationship. 467 for ( var i = 0 ; i <= minLevel - 1 ; i++ ) 468 { 469 if ( addressOfThis[ i ] != addressOfOther[ i ] ) 470 { 471 if ( i < minLevel ) 472 { 473 return addressOfThis[ i ] < addressOfOther[ i ] ? 474 CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; 475 } 476 break; 477 } 478 } 479 480 // Determinate contains/contained relationship. 481 return ( addressOfThis.length < addressOfOther.length ) ? 482 CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : 483 CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; 484 }, 485 486 /** 487 * Gets the closest ancestor node of this node, specified by its name. 488 * @param {String} reference The name of the ancestor node to search or 489 * an object with the node names to search for. 490 * @param {Boolean} [includeSelf] Whether to include the current 491 * node in the search. 492 * @returns {CKEDITOR.dom.node} The located ancestor node or null if not found. 493 * @since 3.6.1 494 * @example 495 * // Suppose we have the following HTML structure: 496 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div> 497 * // If node == <b> 498 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner"> 499 * ascendant = node.getAscendant( 'b' ); // ascendant == null 500 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b> 501 * ascendant = node.getAscendant( { div: 1, p: 1} ); // Searches for the first 'div' or 'p': ascendant == <div id="inner"> 502 */ 503 getAscendant : function( reference, includeSelf ) 504 { 505 var $ = this.$, 506 name; 507 508 if ( !includeSelf ) 509 $ = $.parentNode; 510 511 while ( $ ) 512 { 513 if ( $.nodeName && ( name = $.nodeName.toLowerCase(), ( typeof reference == 'string' ? name == reference : name in reference ) ) ) 514 return new CKEDITOR.dom.node( $ ); 515 516 $ = $.parentNode; 517 } 518 return null; 519 }, 520 521 hasAscendant : function( name, includeSelf ) 522 { 523 var $ = this.$; 524 525 if ( !includeSelf ) 526 $ = $.parentNode; 527 528 while ( $ ) 529 { 530 if ( $.nodeName && $.nodeName.toLowerCase() == name ) 531 return true; 532 533 $ = $.parentNode; 534 } 535 return false; 536 }, 537 538 move : function( target, toStart ) 539 { 540 target.append( this.remove(), toStart ); 541 }, 542 543 /** 544 * Removes this node from the document DOM. 545 * @param {Boolean} [preserveChildren] Indicates that the children 546 * elements must remain in the document, removing only the outer 547 * tags. 548 * @example 549 * var element = CKEDITOR.dom.element.getById( 'MyElement' ); 550 * <strong>element.remove()</strong>; 551 */ 552 remove : function( preserveChildren ) 553 { 554 var $ = this.$; 555 var parent = $.parentNode; 556 557 if ( parent ) 558 { 559 if ( preserveChildren ) 560 { 561 // Move all children before the node. 562 for ( var child ; ( child = $.firstChild ) ; ) 563 { 564 parent.insertBefore( $.removeChild( child ), $ ); 565 } 566 } 567 568 parent.removeChild( $ ); 569 } 570 571 return this; 572 }, 573 574 replace : function( nodeToReplace ) 575 { 576 this.insertBefore( nodeToReplace ); 577 nodeToReplace.remove(); 578 }, 579 580 trim : function() 581 { 582 this.ltrim(); 583 this.rtrim(); 584 }, 585 586 ltrim : function() 587 { 588 var child; 589 while ( this.getFirst && ( child = this.getFirst() ) ) 590 { 591 if ( child.type == CKEDITOR.NODE_TEXT ) 592 { 593 var trimmed = CKEDITOR.tools.ltrim( child.getText() ), 594 originalLength = child.getLength(); 595 596 if ( !trimmed ) 597 { 598 child.remove(); 599 continue; 600 } 601 else if ( trimmed.length < originalLength ) 602 { 603 child.split( originalLength - trimmed.length ); 604 605 // IE BUG: child.remove() may raise JavaScript errors here. (#81) 606 this.$.removeChild( this.$.firstChild ); 607 } 608 } 609 break; 610 } 611 }, 612 613 rtrim : function() 614 { 615 var child; 616 while ( this.getLast && ( child = this.getLast() ) ) 617 { 618 if ( child.type == CKEDITOR.NODE_TEXT ) 619 { 620 var trimmed = CKEDITOR.tools.rtrim( child.getText() ), 621 originalLength = child.getLength(); 622 623 if ( !trimmed ) 624 { 625 child.remove(); 626 continue; 627 } 628 else if ( trimmed.length < originalLength ) 629 { 630 child.split( trimmed.length ); 631 632 // IE BUG: child.getNext().remove() may raise JavaScript errors here. 633 // (#81) 634 this.$.lastChild.parentNode.removeChild( this.$.lastChild ); 635 } 636 } 637 break; 638 } 639 640 if ( !CKEDITOR.env.ie && !CKEDITOR.env.opera ) 641 { 642 child = this.$.lastChild; 643 644 if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) 645 { 646 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). 647 child.parentNode.removeChild( child ) ; 648 } 649 } 650 }, 651 652 /** 653 * Checks if this node is read-only (should not be changed). 654 * @returns {Boolean} 655 * @since 3.5 656 * @example 657 * // For the following HTML: 658 * // <div contenteditable="false">Some <b>text</b></div> 659 * 660 * // If "ele" is the above <div> 661 * ele.isReadOnly(); // true 662 */ 663 isReadOnly : function() 664 { 665 var element = this; 666 if ( this.type != CKEDITOR.NODE_ELEMENT ) 667 element = this.getParent(); 668 669 if ( element && typeof element.$.isContentEditable != 'undefined' ) 670 return ! ( element.$.isContentEditable || element.data( 'cke-editable' ) ); 671 else 672 { 673 // Degrade for old browsers which don't support "isContentEditable", e.g. FF3 674 var current = element; 675 while( current ) 676 { 677 if ( current.is( 'body' ) || !!current.data( 'cke-editable' ) ) 678 break; 679 680 if ( current.getAttribute( 'contentEditable' ) == 'false' ) 681 return true; 682 else if ( current.getAttribute( 'contentEditable' ) == 'true' ) 683 break; 684 685 current = current.getParent(); 686 } 687 688 return false; 689 } 690 } 691 } 692 ); 693