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