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