      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