2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
7 * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
8 * class for classes that represent DOM nodes.
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.
18 * @see CKEDITOR.dom.element
19 * @see CKEDITOR.dom.text
22 CKEDITOR
.dom
.node = function( domNode
)
26 switch ( domNode
.nodeType
)
28 // Safari don't consider document as element node type. (#3389)
29 case CKEDITOR
.NODE_DOCUMENT
:
30 return new CKEDITOR
.dom
.document( domNode
);
32 case CKEDITOR
.NODE_ELEMENT
:
33 return new CKEDITOR
.dom
.element( domNode
);
35 case CKEDITOR
.NODE_TEXT
:
36 return new CKEDITOR
.dom
.text( domNode
);
39 // Call the base constructor.
40 CKEDITOR
.dom
.domObject
.call( this, domNode
);
46 CKEDITOR
.dom
.node
.prototype = new CKEDITOR
.dom
.domObject();
53 CKEDITOR
.NODE_ELEMENT
= 1;
60 CKEDITOR
.NODE_DOCUMENT
= 9;
67 CKEDITOR
.NODE_TEXT
= 3;
74 CKEDITOR
.NODE_COMMENT
= 8;
76 CKEDITOR
.NODE_DOCUMENT_FRAGMENT
= 11;
78 CKEDITOR
.POSITION_IDENTICAL
= 0;
79 CKEDITOR
.POSITION_DISCONNECTED
= 1;
80 CKEDITOR
.POSITION_FOLLOWING
= 2;
81 CKEDITOR
.POSITION_PRECEDING
= 4;
82 CKEDITOR
.POSITION_IS_CONTAINED
= 8;
83 CKEDITOR
.POSITION_CONTAINS
= 16;
85 CKEDITOR
.tools
.extend( CKEDITOR
.dom
.node
.prototype,
86 /** @lends CKEDITOR.dom.node.prototype */
89 * Makes this node a child of another element.
90 * @param {CKEDITOR.dom.element} element The target element to which
91 * this node will be appended.
92 * @returns {CKEDITOR.dom.element} The target element.
94 * var p = new CKEDITOR.dom.element( 'p' );
95 * var strong = new CKEDITOR.dom.element( 'strong' );
96 * strong.appendTo( p );
98 * // result: "<p><strong></strong></p>"
100 appendTo : function( element
, toStart
)
102 element
.append( this, toStart
);
106 clone : function( includeChildren
, cloneId
)
108 var $clone
= this.$.cloneNode( includeChildren
);
110 var removeIds = function( node
)
112 if ( node
.nodeType
!= CKEDITOR
.NODE_ELEMENT
)
116 node
.removeAttribute( 'id', false );
117 node
.removeAttribute( 'data-cke-expando', false );
119 if ( includeChildren
)
121 var childs
= node
.childNodes
;
122 for ( var i
=0; i
< childs
.length
; i
++ )
123 removeIds( childs
[ i
] );
127 // The "id" attribute should never be cloned to avoid duplication.
130 return new CKEDITOR
.dom
.node( $clone
);
133 hasPrevious : function()
135 return !!this.$.previousSibling
;
140 return !!this.$.nextSibling
;
144 * Inserts this element after a node.
145 * @param {CKEDITOR.dom.node} node The node that will precede this element.
146 * @returns {CKEDITOR.dom.node} The node preceding this one after
149 * var em = new CKEDITOR.dom.element( 'em' );
150 * var strong = new CKEDITOR.dom.element( 'strong' );
151 * strong.insertAfter( em );
153 * // result: "<em></em><strong></strong>"
155 insertAfter : function( node
)
157 node
.$.parentNode
.insertBefore( this.$, node
.$.nextSibling
);
162 * Inserts this element before a node.
163 * @param {CKEDITOR.dom.node} node The node that will succeed this element.
164 * @returns {CKEDITOR.dom.node} The node being inserted.
166 * var em = new CKEDITOR.dom.element( 'em' );
167 * var strong = new CKEDITOR.dom.element( 'strong' );
168 * strong.insertBefore( em );
170 * // result: "<strong></strong><em></em>"
172 insertBefore : function( node
)
174 node
.$.parentNode
.insertBefore( this.$, node
.$ );
178 insertBeforeMe : function( node
)
180 this.$.parentNode
.insertBefore( node
.$, this.$ );
185 * Retrieves a uniquely identifiable tree address for this node.
186 * The tree address returned is an array of integers, with each integer
187 * indicating a child index of a DOM node, starting from
188 * <code>document.documentElement</code>.
190 * For example, assuming <code><body></code> is the second child
191 * of <code><html></code> (<code><head></code> being the first),
192 * and we would like to address the third child under the
193 * fourth child of <code><body></code>, the tree address returned would be:
196 * The tree address cannot be used for finding back the DOM tree node once
197 * the DOM tree structure has been modified.
199 getAddress : function( normalized
)
202 var $documentElement
= this.getDocument().$.documentElement
;
205 while ( node
&& node
!= $documentElement
)
207 var parentNode
= node
.parentNode
;
211 // Get the node index. For performance, call getIndex
212 // directly, instead of creating a new node object.
213 address
.unshift( this.getIndex
.call( { $ : node
}, normalized
) );
223 * Gets the document containing this element.
224 * @returns {CKEDITOR.dom.document} The document.
226 * var element = CKEDITOR.document.getById( 'example' );
227 * alert( <strong>element.getDocument().equals( CKEDITOR.document )</strong> ); // "true"
229 getDocument : function()
231 return new CKEDITOR
.dom
.document( this.$.ownerDocument
|| this.$.parentNode
.ownerDocument
);
234 getIndex : function( normalized
)
236 // Attention: getAddress depends on this.$
238 var current
= this.$,
241 while ( ( current
= current
.previousSibling
) )
243 // When normalizing, do not count it if this is an
244 // empty text node or if it's a text node following another one.
245 if ( normalized
&& current
.nodeType
== 3 &&
246 ( !current
.nodeValue
.length
||
247 ( current
.previousSibling
&& current
.previousSibling
.nodeType
== 3 ) ) )
258 getNextSourceNode : function( startFromSibling
, nodeType
, guard
)
260 // If "guard" is a node, transform it in a function.
261 if ( guard
&& !guard
.call
)
263 var guardNode
= guard
;
264 guard = function( node
)
266 return !node
.equals( guardNode
);
270 var node
= ( !startFromSibling
&& this.getFirst
&& this.getFirst() ),
273 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
274 // send the 'moving out' signal even we don't actually dive into.
277 if ( this.type
== CKEDITOR
.NODE_ELEMENT
&& guard
&& guard( this, true ) === false )
279 node
= this.getNext();
282 while ( !node
&& ( parent
= ( parent
|| this ).getParent() ) )
284 // The guard check sends the "true" paramenter to indicate that
285 // we are moving "out" of the element.
286 if ( guard
&& guard( parent
, true ) === false )
289 node
= parent
.getNext();
295 if ( guard
&& guard( node
) === false )
298 if ( nodeType
&& nodeType
!= node
.type
)
299 return node
.getNextSourceNode( false, nodeType
, guard
);
304 getPreviousSourceNode : function( startFromSibling
, nodeType
, guard
)
306 if ( guard
&& !guard
.call
)
308 var guardNode
= guard
;
309 guard = function( node
)
311 return !node
.equals( guardNode
);
315 var node
= ( !startFromSibling
&& this.getLast
&& this.getLast() ),
318 // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
319 // send the 'moving out' signal even we don't actually dive into.
322 if ( this.type
== CKEDITOR
.NODE_ELEMENT
&& guard
&& guard( this, true ) === false )
324 node
= this.getPrevious();
327 while ( !node
&& ( parent
= ( parent
|| this ).getParent() ) )
329 // The guard check sends the "true" paramenter to indicate that
330 // we are moving "out" of the element.
331 if ( guard
&& guard( parent
, true ) === false )
334 node
= parent
.getPrevious();
340 if ( guard
&& guard( node
) === false )
343 if ( nodeType
&& node
.type
!= nodeType
)
344 return node
.getPreviousSourceNode( false, nodeType
, guard
);
349 getPrevious : function( evaluator
)
351 var previous
= this.$, retval
;
354 previous
= previous
.previousSibling
;
355 retval
= previous
&& new CKEDITOR
.dom
.node( previous
);
357 while ( retval
&& evaluator
&& !evaluator( retval
) )
362 * Gets the node that follows this element in its parent's child list.
363 * @param {Function} evaluator Filtering the result node.
364 * @returns {CKEDITOR.dom.node} The next node or null if not available.
366 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b> <i>next</i></div>' );
367 * var first = <strong>element.getFirst().getNext()</strong>;
368 * alert( first.getName() ); // "i"
370 getNext : function( evaluator
)
372 var next
= this.$, retval
;
375 next
= next
.nextSibling
;
376 retval
= next
&& new CKEDITOR
.dom
.node( next
);
378 while ( retval
&& evaluator
&& !evaluator( retval
) )
383 * Gets the parent element for this node.
384 * @returns {CKEDITOR.dom.element} The parent element.
386 * var node = editor.document.getBody().getFirst();
387 * var parent = node.<strong>getParent()</strong>;
388 * alert( node.getName() ); // "body"
390 getParent : function()
392 var parent
= this.$.parentNode
;
393 return ( parent
&& parent
.nodeType
== 1 ) ? new CKEDITOR
.dom
.node( parent
) : null;
396 getParents : function( closerFirst
)
403 parents
[ closerFirst
? 'push' : 'unshift' ]( node
);
405 while ( ( node
= node
.getParent() ) )
410 getCommonAncestor : function( node
)
412 if ( node
.equals( this ) )
415 if ( node
.contains
&& node
.contains( this ) )
418 var start
= this.contains
? this : this.getParent();
422 if ( start
.contains( node
) )
425 while ( ( start
= start
.getParent() ) );
430 getPosition : function( otherNode
)
433 var $other
= otherNode
.$;
435 if ( $.compareDocumentPosition
)
436 return $.compareDocumentPosition( $other
);
438 // IE and Safari have no support for compareDocumentPosition.
441 return CKEDITOR
.POSITION_IDENTICAL
;
443 // Only element nodes support contains and sourceIndex.
444 if ( this.type
== CKEDITOR
.NODE_ELEMENT
&& otherNode
.type
== CKEDITOR
.NODE_ELEMENT
)
448 if ( $.contains( $other
) )
449 return CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_PRECEDING
;
451 if ( $other
.contains( $ ) )
452 return CKEDITOR
.POSITION_IS_CONTAINED
+ CKEDITOR
.POSITION_FOLLOWING
;
455 if ( 'sourceIndex' in $ )
457 return ( $.sourceIndex
< 0 || $other
.sourceIndex
< 0 ) ? CKEDITOR
.POSITION_DISCONNECTED
:
458 ( $.sourceIndex
< $other
.sourceIndex
) ? CKEDITOR
.POSITION_PRECEDING
:
459 CKEDITOR
.POSITION_FOLLOWING
;
463 // For nodes that don't support compareDocumentPosition, contains
464 // or sourceIndex, their "address" is compared.
466 var addressOfThis
= this.getAddress(),
467 addressOfOther
= otherNode
.getAddress(),
468 minLevel
= Math
.min( addressOfThis
.length
, addressOfOther
.length
);
470 // Determinate preceed/follow relationship.
471 for ( var i
= 0 ; i
<= minLevel
- 1 ; i
++ )
473 if ( addressOfThis
[ i
] != addressOfOther
[ i
] )
477 return addressOfThis
[ i
] < addressOfOther
[ i
] ?
478 CKEDITOR
.POSITION_PRECEDING
: CKEDITOR
.POSITION_FOLLOWING
;
484 // Determinate contains/contained relationship.
485 return ( addressOfThis
.length
< addressOfOther
.length
) ?
486 CKEDITOR
.POSITION_CONTAINS
+ CKEDITOR
.POSITION_PRECEDING
:
487 CKEDITOR
.POSITION_IS_CONTAINED
+ CKEDITOR
.POSITION_FOLLOWING
;
491 * Gets the closest ancestor node of this node, specified by its name.
492 * @param {String} reference The name of the ancestor node to search or
493 * an object with the node names to search for.
494 * @param {Boolean} [includeSelf] Whether to include the current
495 * node in the search.
496 * @returns {CKEDITOR.dom.node} The located ancestor node or null if not found.
499 * // Suppose we have the following HTML structure:
500 * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div>
501 * // If node == <b>
502 * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner">
503 * ascendant = node.getAscendant( 'b' ); // ascendant == null
504 * ascendant = node.getAscendant( 'b', true ); // ascendant == <b>
505 * ascendant = node.getAscendant( { div: 1, p: 1} ); // Searches for the first 'div' or 'p': ascendant == <div id="inner">
507 getAscendant : function( reference
, includeSelf
)
517 if ( $.nodeName
&& ( name
= $.nodeName
.toLowerCase(), ( typeof reference
== 'string' ? name
== reference
: name
in reference
) ) )
518 return new CKEDITOR
.dom
.node( $ );
525 hasAscendant : function( name
, includeSelf
)
534 if ( $.nodeName
&& $.nodeName
.toLowerCase() == name
)
542 move : function( target
, toStart
)
544 target
.append( this.remove(), toStart
);
548 * Removes this node from the document DOM.
549 * @param {Boolean} [preserveChildren] Indicates that the children
550 * elements must remain in the document, removing only the outer
553 * var element = CKEDITOR.dom.element.getById( 'MyElement' );
554 * <strong>element.remove()</strong>;
556 remove : function( preserveChildren
)
559 var parent
= $.parentNode
;
563 if ( preserveChildren
)
565 // Move all children before the node.
566 for ( var child
; ( child
= $.firstChild
) ; )
568 parent
.insertBefore( $.removeChild( child
), $ );
572 parent
.removeChild( $ );
578 replace : function( nodeToReplace
)
580 this.insertBefore( nodeToReplace
);
581 nodeToReplace
.remove();
593 while ( this.getFirst
&& ( child
= this.getFirst() ) )
595 if ( child
.type
== CKEDITOR
.NODE_TEXT
)
597 var trimmed
= CKEDITOR
.tools
.ltrim( child
.getText() ),
598 originalLength
= child
.getLength();
605 else if ( trimmed
.length
< originalLength
)
607 child
.split( originalLength
- trimmed
.length
);
609 // IE BUG: child.remove() may raise JavaScript errors here. (#81)
610 this.$.removeChild( this.$.firstChild
);
620 while ( this.getLast
&& ( child
= this.getLast() ) )
622 if ( child
.type
== CKEDITOR
.NODE_TEXT
)
624 var trimmed
= CKEDITOR
.tools
.rtrim( child
.getText() ),
625 originalLength
= child
.getLength();
632 else if ( trimmed
.length
< originalLength
)
634 child
.split( trimmed
.length
);
636 // IE BUG: child.getNext().remove() may raise JavaScript errors here.
638 this.$.lastChild
.parentNode
.removeChild( this.$.lastChild
);
644 if ( !CKEDITOR
.env
.ie
&& !CKEDITOR
.env
.opera
)
646 child
= this.$.lastChild
;
648 if ( child
&& child
.type
== 1 && child
.nodeName
.toLowerCase() == 'br' )
650 // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
651 child
.parentNode
.removeChild( child
) ;
657 * Checks if this node is read-only (should not be changed). Additionally
658 * it returns the element that defines the read-only state of this node
659 * (if present). It may be the node itself or any of its parent
661 * @returns {CKEDITOR.dom.element|Boolean} An element containing
662 * read-only attributes or "false" if none is found.
665 * // For the following HTML:
666 * // <div contenteditable="false">Some <b>text</b></div>
668 * // If "ele" is the above <div>
669 * ele.isReadOnly(); // the <div> element
671 * // If "ele" is the above <b>
672 * ele.isReadOnly(); // the <div> element
674 isReadOnly : function()
679 if ( current
.type
== CKEDITOR
.NODE_ELEMENT
)
681 if ( current
.is( 'body' ) || !!current
.data( 'cke-editable' ) )
684 if ( current
.getAttribute( 'contentEditable' ) == 'false' )
686 else if ( current
.getAttribute( 'contentEditable' ) == 'true' )
689 current
= current
.getParent();