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