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.element} class, which
  8  *		represents a DOM element.
  9  */
 10
 11 /**
 12  * Represents a DOM element.
 13  * @constructor
 14  * @augments CKEDITOR.dom.node
 15  * @param {Object|String} element A native DOM element or the element name for
 16  *		new elements.
 17  * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
 18  *		the element in case of element creation.
 19  * @example
 20  * // Create a new <span> element.
 21  * var element = new CKEDITOR.dom.element( 'span' );
 22  * @example
 23  * // Create an element based on a native DOM element.
 24  * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) );
 25  */
 26 CKEDITOR.dom.element = function( element, ownerDocument )
 27 {
 28 	if ( typeof element == 'string' )
 29 		element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element );
 30
 31 	// Call the base constructor (we must not call CKEDITOR.dom.node).
 32 	CKEDITOR.dom.domObject.call( this, element );
 33 };
 34
 35 // PACKAGER_RENAME( CKEDITOR.dom.element )
 36
 37 /**
 38  * The the {@link CKEDITOR.dom.element} representing and element. If the
 39  * element is a native DOM element, it will be transformed into a valid
 40  * CKEDITOR.dom.element object.
 41  * @returns {CKEDITOR.dom.element} The transformed element.
 42  * @example
 43  * var element = new CKEDITOR.dom.element( 'span' );
 44  * alert( element == <b>CKEDITOR.dom.element.get( element )</b> );  "true"
 45  * @example
 46  * var element = document.getElementById( 'myElement' );
 47  * alert( <b>CKEDITOR.dom.element.get( element )</b>.getName() );  e.g. "p"
 48  */
 49 CKEDITOR.dom.element.get = function( element )
 50 {
 51 	return element && ( element.$ ? element : new CKEDITOR.dom.element( element ) );
 52 };
 53
 54 CKEDITOR.dom.element.prototype = new CKEDITOR.dom.node();
 55
 56 /**
 57  * Creates an instance of the {@link CKEDITOR.dom.element} class based on the
 58  * HTML representation of an element.
 59  * @param {String} html The element HTML. It should define only one element in
 60  *		the "root" level. The "root" element can have child nodes, but not
 61  *		siblings.
 62  * @returns {CKEDITOR.dom.element} The element instance.
 63  * @example
 64  * var element = <b>CKEDITOR.dom.element.createFromHtml( '<strong class="anyclass">My element</strong>' )</b>;
 65  * alert( element.getName() );  // "strong"
 66  */
 67 CKEDITOR.dom.element.createFromHtml = function( html, ownerDocument )
 68 {
 69 	var temp = new CKEDITOR.dom.element( 'div', ownerDocument );
 70 	temp.setHtml( html );
 71
 72 	// When returning the node, remove it from its parent to detach it.
 73 	return temp.getFirst().remove();
 74 };
 75
 76 CKEDITOR.dom.element.setMarker = function( database, element, name, value )
 77 {
 78 	var id = element.getCustomData( 'list_marker_id' ) ||
 79 			( element.setCustomData( 'list_marker_id', CKEDITOR.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
 80 		markerNames = element.getCustomData( 'list_marker_names' ) ||
 81 			( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
 82 	database[id] = element;
 83 	markerNames[name] = 1;
 84
 85 	return element.setCustomData( name, value );
 86 };
 87
 88 CKEDITOR.dom.element.clearAllMarkers = function( database )
 89 {
 90 	for ( var i in database )
 91 		CKEDITOR.dom.element.clearMarkers( database, database[i], true );
 92 };
 93
 94 CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase )
 95 {
 96 	var names = element.getCustomData( 'list_marker_names' ),
 97 		id = element.getCustomData( 'list_marker_id' );
 98 	for ( var i in names )
 99 		element.removeCustomData( i );
100 	element.removeCustomData( 'list_marker_names' );
101 	if ( removeFromDatabase )
102 	{
103 		element.removeCustomData( 'list_marker_id' );
104 		delete database[id];
105 	}
106 };
107
108 CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype,
109 	/** @lends CKEDITOR.dom.element.prototype */
110 	{
111 		/**
112 		 * The node type. This is a constant value set to
113 		 * {@link CKEDITOR.NODE_ELEMENT}.
114 		 * @type Number
115 		 * @example
116 		 */
117 		type : CKEDITOR.NODE_ELEMENT,
118
119 		/**
120 		 * Adds a CSS class to the element. It appends the class to the
121 		 * already existing names.
122 		 * @param {String} className The name of the class to be added.
123 		 * @example
124 		 * var element = new CKEDITOR.dom.element( 'div' );
125 		 * element.addClass( 'classA' );  // <div class="classA">
126 		 * element.addClass( 'classB' );  // <div class="classA classB">
127 		 * element.addClass( 'classA' );  // <div class="classA classB">
128 		 */
129 		addClass : function( className )
130 		{
131 			var c = this.$.className;
132 			if ( c )
133 			{
134 				var regex = new RegExp( '(?:^|\\s)' + className + '(?:\\s|$)', '' );
135 				if ( !regex.test( c ) )
136 					c += ' ' + className;
137 			}
138 			this.$.className = c || className;
139 		},
140
141 		/**
142 		 * Removes a CSS class name from the elements classes. Other classes
143 		 * remain untouched.
144 		 * @param {String} className The name of the class to remove.
145 		 * @example
146 		 * var element = new CKEDITOR.dom.element( 'div' );
147 		 * element.addClass( 'classA' );  // <div class="classA">
148 		 * element.addClass( 'classB' );  // <div class="classA classB">
149 		 * element.removeClass( 'classA' );  // <div class="classB">
150 		 * element.removeClass( 'classB' );  // <div>
151 		 */
152 		removeClass : function( className )
153 		{
154 			var c = this.getAttribute( 'class' );
155 			if ( c )
156 			{
157 				var regex = new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)', 'i' );
158 				if ( regex.test( c ) )
159 				{
160 					c = c.replace( regex, '' ).replace( /^\s+/, '' );
161
162 					if ( c )
163 						this.setAttribute( 'class', c );
164 					else
165 						this.removeAttribute( 'class' );
166 				}
167 			}
168 		},
169
170 		hasClass : function( className )
171 		{
172 			var regex = new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)', '' );
173 			return regex.test( this.getAttribute('class') );
174 		},
175
176 		/**
177 		 * Append a node as a child of this element.
178 		 * @param {CKEDITOR.dom.node|String} node The node or element name to be
179 		 *		appended.
180 		 * @param {Boolean} [toStart] Indicates that the element is to be
181 		 *		appended at the start.
182 		 * @returns {CKEDITOR.dom.node} The appended node.
183 		 * @example
184 		 * var p = new CKEDITOR.dom.element( 'p' );
185 		 *
186 		 * var strong = new CKEDITOR.dom.element( 'strong' );
187 		 * <b>p.append( strong );</b>
188 		 *
189 		 * var em = <b>p.append( 'em' );</b>
190 		 *
191 		 * // result: "<p><strong></strong><em></em></p>"
192 		 */
193 		append : function( node, toStart )
194 		{
195 			if ( typeof node == 'string' )
196 				node = this.getDocument().createElement( node );
197
198 			if ( toStart )
199 				this.$.insertBefore( node.$, this.$.firstChild );
200 			else
201 				this.$.appendChild( node.$ );
202
203 			return node;
204 		},
205
206 		appendHtml : function( html )
207 		{
208 			if ( !this.$.childNodes.length )
209 				this.setHtml( html );
210 			else
211 			{
212 				var temp = new CKEDITOR.dom.element( 'div', this.getDocument() );
213 				temp.setHtml( html );
214 				temp.moveChildren( this );
215 			}
216 		},
217
218 		/**
219 		 * Append text to this element.
220 		 * @param {String} text The text to be appended.
221 		 * @returns {CKEDITOR.dom.node} The appended node.
222 		 * @example
223 		 * var p = new CKEDITOR.dom.element( 'p' );
224 		 * p.appendText( 'This is' );
225 		 * p.appendText( ' some text' );
226 		 *
227 		 * // result: "<p>This is some text</p>"
228 		 */
229 		appendText : function( text )
230 		{
231 			if ( this.$.text != undefined )
232 				this.$.text += text;
233 			else
234 				this.append( new CKEDITOR.dom.text( text ) );
235 		},
236
237 		appendBogus : function()
238 		{
239 			var lastChild = this.getLast() ;
240
241 			// Ignore empty/spaces text.
242 			while ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.rtrim( lastChild.getText() ) )
243 				lastChild = lastChild.getPrevious();
244 			if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) )
245 			{
246 				this.append(
247 					CKEDITOR.env.opera ?
248 						this.getDocument().createText('') :
249 						this.getDocument().createElement( 'br' ) );
250 			}
251 		},
252
253 		/**
254 		 * Breaks one of the ancestor element in the element position, moving
255 		 * this element between the broken parts.
256 		 * @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
257 		 * @example
258 		 * // Before breaking:
259 		 * //     <b>This <i>is some<span /> sample</i> test text</b>
260 		 * // If "element" is <span /> and "parent" is <i>:
261 		 * //     <b>This <i>is some</i><span /><i> sample</i> test text</b>
262 		 * element.breakParent( parent );
263 		 * @example
264 		 * // Before breaking:
265 		 * //     <b>This <i>is some<span /> sample</i> test text</b>
266 		 * // If "element" is <span /> and "parent" is <b>:
267 		 * //     <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b>
268 		 * element.breakParent( parent );
269 		 */
270 		breakParent : function( parent )
271 		{
272 			var range = new CKEDITOR.dom.range( this.getDocument() );
273
274 			// We'll be extracting part of this element, so let's use our
275 			// range to get the correct piece.
276 			range.setStartAfter( this );
277 			range.setEndAfter( parent );
278
279 			// Extract it.
280 			var docFrag = range.extractContents();
281
282 			// Move the element outside the broken element.
283 			range.insertNode( this.remove() );
284
285 			// Re-insert the extracted piece after the element.
286 			docFrag.insertAfterNode( this );
287 		},
288
289 		contains :
290 			CKEDITOR.env.ie || CKEDITOR.env.webkit ?
291 				function( node )
292 				{
293 					var $ = this.$;
294
295 					return node.type != CKEDITOR.NODE_ELEMENT ?
296 						$.contains( node.getParent().$ ) :
297 						$ != node.$ && $.contains( node.$ );
298 				}
299 			:
300 				function( node )
301 				{
302 					return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
303 				},
304
305 		/**
306 		 * Moves the selection focus to this element.
307 		 * @example
308 		 * var element = CKEDITOR.document.getById( 'myTextarea' );
309 		 * <b>element.focus()</b>;
310 		 */
311 		focus : function()
312 		{
313 			// IE throws error if the element is not visible.
314 			try
315 			{
316 				this.$.focus();
317 			}
318 			catch (e)
319 			{}
320 		},
321
322 		/**
323 		 * Gets the inner HTML of this element.
324 		 * @returns {String} The inner HTML of this element.
325 		 * @example
326 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
327 		 * alert( <b>p.getHtml()</b> );  // "<b>Example</b>"
328 		 */
329 		getHtml : function()
330 		{
331 			return this.$.innerHTML;
332 		},
333
334 		getOuterHtml : function()
335 		{
336 			if ( this.$.outerHTML )
337 			{
338 				// IE includes the <?xml:namespace> tag in the outerHTML of
339 				// namespaced element. So, we must strip it here. (#3341)
340 				return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
341 			}
342
343 			var tmpDiv = this.$.ownerDocument.createElement( 'div' );
344 			tmpDiv.appendChild( this.$.cloneNode( true ) );
345 			return tmpDiv.innerHTML;
346 		},
347
348 		/**
349 		 * Sets the inner HTML of this element.
350 		 * @param {String} html The HTML to be set for this element.
351 		 * @returns {String} The inserted HTML.
352 		 * @example
353 		 * var p = new CKEDITOR.dom.element( 'p' );
354 		 * <b>p.setHtml( '<b>Inner</b> HTML' );</b>
355 		 *
356 		 * // result: "<p><b>Inner</b> HTML</p>"
357 		 */
358 		setHtml : function( html )
359 		{
360 			return ( this.$.innerHTML = html );
361 		},
362
363 		/**
364 		 * Sets the element contents as plain text.
365 		 * @param {String} text The text to be set.
366 		 * @returns {String} The inserted text.
367 		 * @example
368 		 * var element = new CKEDITOR.dom.element( 'div' );
369 		 * element.setText( 'A > B & C < D' );
370 		 * alert( element.innerHTML );  // "A &gt; B &amp; C &lt; D"
371 		 */
372 		setText : function( text )
373 		{
374 			CKEDITOR.dom.element.prototype.setText = ( this.$.innerText != undefined ) ?
375 				function ( text )
376 				{
377 					return this.$.innerText = text;
378 				} :
379 				function ( text )
380 				{
381 					return this.$.textContent = text;
382 				};
383
384 			return this.setText( text );
385 		},
386
387 		/**
388 		 * Gets the value of an element attribute.
389 		 * @function
390 		 * @param {String} name The attribute name.
391 		 * @returns {String} The attribute value or null if not defined.
392 		 * @example
393 		 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
394 		 * alert( <b>element.getAttribute( 'type' )</b> );  // "text"
395 		 */
396 		getAttribute : (function()
397 		{
398 			var standard = function( name )
399 			{
400 				return this.$.getAttribute( name, 2 );
401 			};
402
403 			if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
404 			{
405 				return function( name )
406 				{
407 					switch ( name )
408 					{
409 						case 'class':
410 							name = 'className';
411 							break;
412
413 						case 'tabindex':
414 							var tabIndex = standard.call( this, name );
415
416 							// IE returns tabIndex=0 by default for all
417 							// elements. For those elements,
418 							// getAtrribute( 'tabindex', 2 ) returns 32768
419 							// instead. So, we must make this check to give a
420 							// uniform result among all browsers.
421 							if ( tabIndex !== 0 && this.$.tabIndex === 0 )
422 								tabIndex = null;
423
424 							return tabIndex;
425 							break;
426
427 						case 'checked':
428 							return this.$.checked;
429 							break;
430
431 						case 'style':
432 							// IE does not return inline styles via getAttribute(). See #2947.
433 							var styleText = this.$.style.cssText;
434 							return styleText.toLowerCase().replace(
435 								/\s*(?:;\s*|$)/, ';').replace(
436 									/([^;])$/, '$1;');
437 					}
438
439 					return standard.call( this, name );
440 				};
441 			}
442 			else
443 				return standard;
444 		})(),
445
446 		getChildren : function()
447 		{
448 			return new CKEDITOR.dom.nodeList( this.$.childNodes );
449 		},
450
451 		/**
452 		 * Gets the current computed value of one of the element CSS style
453 		 * properties.
454 		 * @function
455 		 * @param {String} propertyName The style property name.
456 		 * @returns {String} The property value.
457 		 * @example
458 		 * var element = new CKEDITOR.dom.element( 'span' );
459 		 * alert( <b>element.getComputedStyle( 'display' )</b> );  // "inline"
460 		 */
461 		getComputedStyle :
462 			CKEDITOR.env.ie ?
463 				function( propertyName )
464 				{
465 					return this.$.currentStyle[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
466 				}
467 			:
468 				function( propertyName )
469 				{
470 					return this.getWindow().$.getComputedStyle( this.$, '' ).getPropertyValue( propertyName );
471 				},
472
473 		/**
474 		 * Gets the DTD entries for this element.
475 		 * @returns {Object} An object containing the list of elements accepted
476 		 *		by this element.
477 		 */
478 		getDtd : function()
479 		{
480 			var dtd = CKEDITOR.dtd[ this.getName() ];
481
482 			this.getDtd = function()
483 			{
484 				return dtd;
485 			};
486
487 			return dtd;
488 		},
489
490 		getElementsByTag : CKEDITOR.dom.document.prototype.getElementsByTag,
491
492 		/**
493 		 * Gets the computed tabindex for this element.
494 		 * @function
495 		 * @returns {Number} The tabindex value.
496 		 * @example
497 		 * var element = CKEDITOR.document.getById( 'myDiv' );
498 		 * alert( <b>element.getTabIndex()</b> );  // e.g. "-1"
499 		 */
500 		getTabIndex :
501 			CKEDITOR.env.ie ?
502 				function()
503 				{
504 					var tabIndex = this.$.tabIndex;
505
506 					// IE returns tabIndex=0 by default for all elements. In
507 					// those cases we must check that the element really has
508 					// the tabindex attribute set to zero, or it is one of
509 					// those element that should have zero by default.
510 					if ( tabIndex === 0 && !CKEDITOR.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
511 						tabIndex = -1;
512
513 						return tabIndex;
514 				}
515 			: CKEDITOR.env.webkit ?
516 				function()
517 				{
518 					var tabIndex = this.$.tabIndex;
519
520 					// Safari returns "undefined" for elements that should not
521 					// have tabindex (like a div). So, we must try to get it
522 					// from the attribute.
523 					// https://bugs.webkit.org/show_bug.cgi?id=20596
524 					if ( tabIndex == undefined )
525 					{
526 						tabIndex = parseInt( this.getAttribute( 'tabindex' ), 10 );
527
528 						// If the element don't have the tabindex attribute,
529 						// then we should return -1.
530 						if ( isNaN( tabIndex ) )
531 							tabIndex = -1;
532 					}
533
534 					return tabIndex;
535 				}
536 			:
537 				function()
538 				{
539 					return this.$.tabIndex;
540 				},
541
542 		/**
543 		 * Gets the text value of this element.
544 		 *
545 		 * Only in IE (which uses innerText), <br> will cause linebreaks,
546 		 * and sucessive whitespaces (including line breaks) will be reduced to
547 		 * a single space. This behavior is ok for us, for now. It may change
548 		 * in the future.
549 		 * @returns {String} The text value.
550 		 * @example
551 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Same <i>text</i>.</div>' );
552 		 * alert( <b>element.getText()</b> );  // "Sample text."
553 		 */
554 		getText : function()
555 		{
556 			return this.$.textContent || this.$.innerText || '';
557 		},
558
559 		/**
560 		 * Gets the window object that contains this element.
561 		 * @returns {CKEDITOR.dom.window} The window object.
562 		 * @example
563 		 */
564 		getWindow : function()
565 		{
566 			return this.getDocument().getWindow();
567 		},
568
569 		/**
570 		 * Gets the value of the "id" attribute of this element.
571 		 * @returns {String} The element id, or null if not available.
572 		 * @example
573 		 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
574 		 * alert( <b>element.getId()</b> );  // "myId"
575 		 */
576 		getId : function()
577 		{
578 			return this.$.id || null;
579 		},
580
581 		/**
582 		 * Gets the value of the "name" attribute of this element.
583 		 * @returns {String} The element name, or null if not available.
584 		 * @example
585 		 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
586 		 * alert( <b>element.getNameAtt()</b> );  // "myName"
587 		 */
588 		getNameAtt : function()
589 		{
590 			return this.$.name || null;
591 		},
592
593 		/**
594 		 * Gets the element name (tag name). The returned name is guaranteed to
595 		 * be always full lowercased.
596 		 * @returns {String} The element name.
597 		 * @example
598 		 * var element = new CKEDITOR.dom.element( 'span' );
599 		 * alert( <b>element.getName()</b> );  // "span"
600 		 */
601 		getName : function()
602 		{
603 			// Cache the lowercased name inside a closure.
604 			var nodeName = this.$.nodeName.toLowerCase();
605
606 			if ( CKEDITOR.env.ie )
607 			{
608 				var scopeName = this.$.scopeName;
609 				if ( scopeName != 'HTML' )
610 					nodeName = scopeName.toLowerCase() + ':' + nodeName;
611 			}
612
613 			return (
614 			/** @ignore */
615 			this.getName = function()
616 				{
617 					return nodeName;
618 				})();
619 		},
620
621 		/**
622 		 * Gets the value set to this element. This value is usually available
623 		 * for form field elements.
624 		 * @returns {String} The element value.
625 		 */
626 		getValue : function()
627 		{
628 			return this.$.value;
629 		},
630
631 		/**
632 		 * Gets the first child node of this element.
633 		 * @returns {CKEDITOR.dom.node} The first child node or null if not
634 		 *		available.
635 		 * @example
636 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
637 		 * var first = <b>element.getFirst()</b>;
638 		 * alert( first.getName() );  // "b"
639 		 */
640 		getFirst : function()
641 		{
642 			var $ = this.$.firstChild;
643 			return $ ? new CKEDITOR.dom.node( $ ) : null;
644 		},
645
646 		/**
647 		 * @param ignoreEmpty Skip empty text nodes.
648 		 */
649 		getLast : function( ignoreEmpty )
650 		{
651 			var $ = this.$.lastChild;
652 			if ( ignoreEmpty && $ && ( $.nodeType == CKEDITOR.NODE_TEXT )
653 					&& !CKEDITOR.tools.trim( $.nodeValue ) )
654 				return new CKEDITOR.dom.node( $ ).getPrevious( true );
655 			else
656 				return $ ? new CKEDITOR.dom.node( $ ) : null;
657 		},
658
659 		getStyle : function( name )
660 		{
661 			return this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ];
662 		},
663
664 		/**
665 		 * Checks if the element name matches one or more names.
666 		 * @param {String} name[,name[,...]] One or more names to be checked.
667 		 * @returns {Boolean} true if the element name matches any of the names.
668 		 * @example
669 		 * var element = new CKEDITOR.element( 'span' );
670 		 * alert( <b>element.is( 'span' )</b> );  "true"
671 		 * alert( <b>element.is( 'p', 'span' )</b> );  "true"
672 		 * alert( <b>element.is( 'p' )</b> );  "false"
673 		 * alert( <b>element.is( 'p', 'div' )</b> );  "false"
674 		 */
675 		is : function()
676 		{
677 			var name = this.getName();
678 			for ( var i = 0 ; i < arguments.length ; i++ )
679 			{
680 				if ( arguments[ i ] == name )
681 					return true;
682 			}
683 			return false;
684 		},
685
686 		isEditable : function()
687 		{
688 			// Get the element name.
689 			var name = this.getName();
690
691 			// Get the element DTD (defaults to span for unknown elements).
692 			var dtd = !CKEDITOR.dtd.$nonEditable[ name ]
693 						&& ( CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span );
694
695 			// In the DTD # == text node.
696 			return ( dtd && dtd['#'] );
697 		},
698
699 		isIdentical : function( otherElement )
700 		{
701 			if ( this.getName() != otherElement.getName() )
702 				return false;
703
704 			var thisAttribs = this.$.attributes,
705 				otherAttribs = otherElement.$.attributes;
706
707 			var thisLength = thisAttribs.length,
708 				otherLength = otherAttribs.length;
709
710 			if ( !CKEDITOR.env.ie && thisLength != otherLength )
711 				return false;
712
713 			for ( var i = 0 ; i < thisLength ; i++ )
714 			{
715 				var attribute = thisAttribs[ i ];
716
717 				if ( ( !CKEDITOR.env.ie || ( attribute.specified && attribute.nodeName != '_cke_expando' ) ) && attribute.nodeValue != otherElement.getAttribute( attribute.nodeName ) )
718 					return false;
719 			}
720
721 			// For IE, we have to for both elements, because it's difficult to
722 			// know how the atttibutes collection is organized in its DOM.
723 			if ( CKEDITOR.env.ie )
724 			{
725 				for ( i = 0 ; i < otherLength ; i++ )
726 				{
727 					attribute = otherAttribs[ i ];
728
729 					if ( ( !CKEDITOR.env.ie || ( attribute.specified && attribute.nodeName != '_cke_expando' ) ) && attribute.nodeValue != thisAttribs.getAttribute( attribute.nodeName ) )
730 						return false;
731 				}
732 			}
733
734 			return true;
735 		},
736
737 		/**
738 		 * Checks if this element is visible. May not work if the element is
739 		 * child of an element with visibility set to "hidden", but works well
740 		 * on the great majority of cases.
741 		 * @return {Boolean} True if the element is visible.
742 		 */
743 		isVisible : function()
744 		{
745 			return this.$.offsetWidth && ( this.$.style.visibility != 'hidden' );
746 		},
747
748 		/**
749 		 * Indicates that the element has defined attributes.
750 		 * @returns {Boolean} True if the element has attributes.
751 		 * @example
752 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
753 		 * alert( <b>element.hasAttributes()</b> );  "true"
754 		 * @example
755 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
756 		 * alert( <b>element.hasAttributes()</b> );  "false"
757 		 */
758 		hasAttributes :
759 			CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) ?
760 				function()
761 				{
762 					var attributes = this.$.attributes;
763
764 					for ( var i = 0 ; i < attributes.length ; i++ )
765 					{
766 						var attribute = attributes[i];
767
768 						switch ( attribute.nodeName )
769 						{
770 							case 'class' :
771 								// IE has a strange bug. If calling removeAttribute('className'),
772 								// the attributes collection will still contain the "class"
773 								// attribute, which will be marked as "specified", even if the
774 								// outerHTML of the element is not displaying the class attribute.
775 								// Note : I was not able to reproduce it outside the editor,
776 								// but I've faced it while working on the TC of #1391.
777 								if ( this.getAttribute( 'class' ) > 0 )
778 									return true;
779
780 							// Attributes to be ignored.
781 							case '_cke_expando' :
782 								continue;
783
784 							/*jsl:fallthru*/
785
786 							default :
787 								if ( attribute.specified )
788 									return true;
789 						}
790 					}
791
792 					return false;
793 				}
794 			:
795 				function()
796 				{
797 					var attributes = this.$.attributes;
798 					return ( attributes.length > 1 || ( attributes.length == 1 && attributes[0].nodeName != '_cke_expando' ) );
799 				},
800
801 		/**
802 		 * Indicates whether a specified attribute is defined for this element.
803 		 * @returns {Boolean} True if the specified attribute is defined.
804 		 * @param (String) name The attribute name.
805 		 * @example
806 		 */
807 		hasAttribute : function( name )
808 		{
809 			var $attr = this.$.attributes.getNamedItem( name );
810 			return !!( $attr && $attr.specified );
811 		},
812
813 		/**
814 		 * Hides this element (display:none).
815 		 * @example
816 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
817 		 * <b>element.hide()</b>;
818 		 */
819 		hide : function()
820 		{
821 			this.setStyle( 'display', 'none' );
822 		},
823
824 		moveChildren : function( target, toStart )
825 		{
826 			var $ = this.$;
827 			target = target.$;
828
829 			if ( $ == target )
830 				return;
831
832 			var child;
833
834 			if ( toStart )
835 			{
836 				while ( ( child = $.lastChild ) )
837 					target.insertBefore( $.removeChild( child ), target.firstChild );
838 			}
839 			else
840 			{
841 				while ( ( child = $.firstChild ) )
842 					target.appendChild( $.removeChild( child ) );
843 			}
844 		},
845
846 		/**
847 		 * Shows this element (display it).
848 		 * @example
849 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
850 		 * <b>element.show()</b>;
851 		 */
852 		show : function()
853 		{
854 			this.setStyles(
855 				{
856 					display : '',
857 					visibility : ''
858 				});
859 		},
860
861 		/**
862 		 * Sets the value of an element attribute.
863 		 * @param {String} name The name of the attribute.
864 		 * @param {String} value The value to be set to the attribute.
865 		 * @function
866 		 * @returns {CKEDITOR.dom.element} This element instance.
867 		 * @example
868 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
869 		 * <b>element.setAttribute( 'class', 'myClass' )</b>;
870 		 * <b>element.setAttribute( 'title', 'This is an example' )</b>;
871 		 */
872 		setAttribute : (function()
873 		{
874 			var standard = function( name, value )
875 			{
876 				this.$.setAttribute( name, value );
877 				return this;
878 			};
879
880 			if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
881 			{
882 				return function( name, value )
883 				{
884 					if ( name == 'class' )
885 						this.$.className = value;
886 					else if ( name == 'style' )
887 						this.$.style.cssText = value;
888 					else if ( name == 'tabindex' )	// Case sensitive.
889 						this.$.tabIndex = value;
890 					else if ( name == 'checked' )
891 						this.$.checked = value;
892 					else
893 						standard.apply( this, arguments );
894 					return this;
895 				};
896 			}
897 			else
898 				return standard;
899 		})(),
900
901 		/**
902 		 * Sets the value of several element attributes.
903 		 * @param {Object} attributesPairs An object containing the names and
904 		 *		values of the attributes.
905 		 * @returns {CKEDITOR.dom.element} This element instance.
906 		 * @example
907 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
908 		 * <b>element.setAttributes({
909 		 *     'class' : 'myClass',
910 		 *     'title' : 'This is an example' })</b>;
911 		 */
912 		setAttributes : function( attributesPairs )
913 		{
914 			for ( var name in attributesPairs )
915 				this.setAttribute( name, attributesPairs[ name ] );
916 			return this;
917 		},
918
919 		/**
920 		 * Sets the element value. This function is usually used with form
921 		 * field element.
922 		 * @param {String} value The element value.
923 		 * @returns {CKEDITOR.dom.element} This element instance.
924 		 */
925 		setValue : function( value )
926 		{
927 			this.$.value = value;
928 			return this;
929 		},
930
931 		/**
932 		 * Removes an attribute from the element.
933 		 * @param {String} name The attribute name.
934 		 * @function
935 		 * @example
936 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
937 		 * element.removeAttribute( 'class' );
938 		 */
939 		removeAttribute : (function()
940 		{
941 			var standard = function( name )
942 			{
943 				this.$.removeAttribute( name );
944 			};
945
946 			if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
947 			{
948 				return function( name )
949 				{
950 					if ( name == 'class' )
951 						name = 'className';
952 					else if ( name == 'tabindex' )
953 						name = 'tabIndex';
954 					standard.call( this, name );
955 				};
956 			}
957 			else
958 				return standard;
959 		})(),
960
961 		removeAttributes : function ( attributes )
962 		{
963 			for ( var i = 0 ; i < attributes.length ; i++ )
964 				this.removeAttribute( attributes[ i ] );
965 		},
966
967 		/**
968 		 * Removes a style from the element.
969 		 * @param {String} name The style name.
970 		 * @function
971 		 * @example
972 		 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
973 		 * element.removeStyle( 'display' );
974 		 */
975 		removeStyle : function( name )
976 		{
977 			if ( this.$.style.removeAttribute )
978 				this.$.style.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) );
979 			else
980 				this.setStyle( name, '' );
981
982 			if ( !this.$.style.cssText )
983 				this.removeAttribute( 'style' );
984 		},
985
986 		/**
987 		 * Sets the value of an element style.
988 		 * @param {String} name The name of the style. The CSS naming notation
989 		 *		must be used (e.g. "background-color").
990 		 * @param {String} value The value to be set to the style.
991 		 * @returns {CKEDITOR.dom.element} This element instance.
992 		 * @example
993 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
994 		 * <b>element.setStyle( 'background-color', '#ff0000' )</b>;
995 		 * <b>element.setStyle( 'margin-top', '10px' )</b>;
996 		 * <b>element.setStyle( 'float', 'right' )</b>;
997 		 */
998 		setStyle : function( name, value )
999 		{
1000 			this.$.style[ CKEDITOR.tools.cssStyleToDomStyle( name ) ] = value;
1001 			return this;
1002 		},
1003
1004 		/**
1005 		 * Sets the value of several element styles.
1006 		 * @param {Object} stylesPairs An object containing the names and
1007 		 *		values of the styles.
1008 		 * @returns {CKEDITOR.dom.element} This element instance.
1009 		 * @example
1010 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
1011 		 * <b>element.setStyles({
1012 		 *     'position' : 'absolute',
1013 		 *     'float' : 'right' })</b>;
1014 		 */
1015 		setStyles : function( stylesPairs )
1016 		{
1017 			for ( var name in stylesPairs )
1018 				this.setStyle( name, stylesPairs[ name ] );
1019 			return this;
1020 		},
1021
1022 		/**
1023 		 * Sets the opacity of an element.
1024 		 * @param {Number} opacity A number within the range [0.0, 1.0].
1025 		 * @example
1026 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
1027 		 * <b>element.setOpacity( 0.75 )</b>;
1028 		 */
1029 		setOpacity : function( opacity )
1030 		{
1031 			if ( CKEDITOR.env.ie )
1032 			{
1033 				opacity = Math.round( opacity * 100 );
1034 				this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
1035 			}
1036 			else
1037 				this.setStyle( 'opacity', opacity );
1038 		},
1039
1040 		/**
1041 		 * Makes the element and its children unselectable.
1042 		 * @function
1043 		 * @example
1044 		 * var element = CKEDITOR.dom.element.getById( 'myElement' );
1045 		 * element.unselectable();
1046 		 */
1047 		unselectable :
1048 			CKEDITOR.env.gecko ?
1049 				function()
1050 				{
1051 					this.$.style.MozUserSelect = 'none';
1052 				}
1053 			: CKEDITOR.env.webkit ?
1054 				function()
1055 				{
1056 					this.$.style.KhtmlUserSelect = 'none';
1057 				}
1058 			:
1059 				function()
1060 				{
1061 					if ( CKEDITOR.env.ie || CKEDITOR.env.opera )
1062 					{
1063 						var element = this.$,
1064 							e,
1065 							i = 0;
1066
1067 						element.unselectable = 'on';
1068
1069 						while ( ( e = element.all[ i++ ] ) )
1070 						{
1071 							switch ( e.tagName.toLowerCase() )
1072 							{
1073 								case 'iframe' :
1074 								case 'textarea' :
1075 								case 'input' :
1076 								case 'select' :
1077 									/* Ignore the above tags */
1078 									break;
1079 								default :
1080 									e.unselectable = 'on';
1081 							}
1082 						}
1083 					}
1084 				},
1085
1086 		getPositionedAncestor : function()
1087 		{
1088 			var current = this;
1089 			while ( current.getName() != 'html' )
1090 			{
1091 				if ( current.getComputedStyle( 'position' ) != 'static' )
1092 					return current;
1093
1094 				current = current.getParent();
1095 			}
1096 			return null;
1097 		},
1098
1099 		getDocumentPosition : function( refDocument )
1100 		{
1101 			var x = 0, y = 0,
1102 				body = this.getDocument().getBody(),
1103 				quirks = this.getDocument().$.compatMode == 'BackCompat';
1104
1105 			var doc = this.getDocument();
1106
1107 			if ( document.documentElement[ "getBoundingClientRect" ] )
1108 			{
1109 				var box  = this.$.getBoundingClientRect(),
1110 					$doc = doc.$,
1111 					$docElem = $doc.documentElement;
1112
1113 				var clientTop = $docElem.clientTop || body.$.clientTop || 0,
1114 					clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
1115 					needAdjustScrollAndBorders = true;
1116
1117 				/*
1118 				 * #3804: getBoundingClientRect() works differently on IE and non-IE
1119 				 * browsers, regarding scroll positions.
1120 				 *
1121 				 * On IE, the top position of the <html> element is always 0, no matter
1122 				 * how much you scrolled down.
1123 				 *
1124 				 * On other browsers, the top position of the <html> element is negative
1125 				 * scrollTop.
1126 				 */
1127 				if ( CKEDITOR.env.ie )
1128 				{
1129 					var inDocElem = doc.getDocumentElement().contains( this ),
1130 						inBody = doc.getBody().contains( this );
1131
1132 					needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
1133 				}
1134
1135 				if ( needAdjustScrollAndBorders )
1136 				{
1137 					x = box.left + ( !quirks && $docElem.scrollLeft || body.$.scrollLeft );
1138 					x -= clientLeft;
1139 					y = box.top  + ( !quirks && $docElem.scrollTop || body.$.scrollTop );
1140 					y -= clientTop;
1141 				}
1142 			}
1143 			else
1144  			{
1145 				var current = this, previous = null, offsetParent;
1146 				while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) )
1147 				{
1148 					x += current.$.offsetLeft - current.$.scrollLeft;
1149 					y += current.$.offsetTop - current.$.scrollTop;
1150
1151 					// Opera includes clientTop|Left into offsetTop|Left.
1152 					if ( !current.equals( this ) )
1153 					{
1154 						x += ( current.$.clientLeft || 0 );
1155 						y += ( current.$.clientTop || 0 );
1156 					}
1157
1158 					var scrollElement = previous;
1159 					while ( scrollElement && !scrollElement.equals( current ) )
1160 					{
1161 						x -= scrollElement.$.scrollLeft;
1162 						y -= scrollElement.$.scrollTop;
1163 						scrollElement = scrollElement.getParent();
1164 					}
1165
1166 					previous = current;
1167 					current = ( offsetParent = current.$.offsetParent ) ?
1168 					          new CKEDITOR.dom.element( offsetParent ) : null;
1169 				}
1170 			}
1171
1172 			if ( refDocument )
1173 			{
1174 				var currentWindow = this.getWindow(),
1175 					refWindow = refDocument.getWindow();
1176
1177 				if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement )
1178 				{
1179 					var iframePosition = ( new CKEDITOR.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );
1180
1181 					x += iframePosition.x;
1182 					y += iframePosition.y;
1183 				}
1184 			}
1185
1186 			if ( !document.documentElement[ "getBoundingClientRect" ] )
1187 			{
1188 				// In Firefox, we'll endup one pixel before the element positions,
1189 				// so we must add it here.
1190 				if ( CKEDITOR.env.gecko && !quirks )
1191 				{
1192 					x += this.$.clientLeft ? 1 : 0;
1193 					y += this.$.clientTop ? 1 : 0;
1194 				}
1195 			}
1196
1197 			return { x : x, y : y };
1198 		},
1199
1200 		scrollIntoView : function( alignTop )
1201 		{
1202 			// Get the element window.
1203 			var win = this.getWindow(),
1204 				winHeight = win.getViewPaneSize().height;
1205
1206 			// Starts from the offset that will be scrolled with the negative value of
1207 			// the visible window height.
1208 			var offset = winHeight * -1;
1209
1210 			// Append the view pane's height if align to top.
1211 			// Append element height if we are aligning to the bottom.
1212 			if ( alignTop )
1213 				offset += winHeight;
1214 			else
1215 			{
1216 				offset += this.$.offsetHeight || 0;
1217
1218 				// Consider the margin in the scroll, which is ok for our current needs, but
1219 				// needs investigation if we will be using this function in other places.
1220 				offset += parseInt( this.getComputedStyle( 'marginBottom' ) || 0, 10 ) || 0;
1221 			}
1222
1223 			// Append the offsets for the entire element hierarchy.
1224 			var elementPosition = this.getDocumentPosition();
1225 			offset += elementPosition.y;
1226 			// Scroll the window to the desired position, if not already visible.
1227 			var currentScroll = win.getScrollPosition().y;
1228
1229 			// Though the computed offset value maybe out of range ( e.g.
1230 			// a negative value ), browser will try to scroll as much as possible. (#3692)
1231 			win.$.scrollTo( 0, offset > 0 ? offset : 0 );
1232 		},
1233
1234 		setState : function( state )
1235 		{
1236 			switch ( state )
1237 			{
1238 				case CKEDITOR.TRISTATE_ON :
1239 					this.addClass( 'cke_on' );
1240 					this.removeClass( 'cke_off' );
1241 					this.removeClass( 'cke_disabled' );
1242 					break;
1243 				case CKEDITOR.TRISTATE_DISABLED :
1244 					this.addClass( 'cke_disabled' );
1245 					this.removeClass( 'cke_off' );
1246 					this.removeClass( 'cke_on' );
1247 					break;
1248 				default :
1249 					this.addClass( 'cke_off' );
1250 					this.removeClass( 'cke_on' );
1251 					this.removeClass( 'cke_disabled' );
1252 					break;
1253 			}
1254 		},
1255
1256 		/**
1257 		 * Returns the inner document of this IFRAME element.
1258 		 * @returns {CKEDITOR.dom.document} The inner document.
1259 		 */
1260 		getFrameDocument : function()
1261 		{
1262 			var $ = this.$;
1263
1264 			try
1265 			{
1266 				// In IE, with custom document.domain, it may happen that
1267 				// the iframe is not yet available, resulting in "Access
1268 				// Denied" for the following property access.
1269 				$.contentWindow.document;
1270 			}
1271 			catch ( e )
1272 			{
1273 				// Trick to solve this issue, forcing the iframe to get ready
1274 				// by simply setting its "src" property.
1275 				$.src = $.src;
1276
1277 				// In IE6 though, the above is not enough, so we must pause the
1278 				// execution for a while, giving it time to think.
1279 				if ( CKEDITOR.env.ie && CKEDITOR.env.version < 7 )
1280 				{
1281 					window.showModalDialog(
1282 						'javascript:document.write("' +
1283 							'<script>' +
1284 								'window.setTimeout(' +
1285 									'function(){window.close();}' +
1286 									',50);' +
1287 							'</script>")' );
1288 				}
1289 			}
1290
1291 			return $ && new CKEDITOR.dom.document( $.contentWindow.document );
1292 		},
1293
1294 		/**
1295 		 * Copy all the attributes from one node to the other, kinda like a clone
1296 		 * skipAttributes is an object with the attributes that must NOT be copied.
1297 		 * @param {CKEDITOR.dom.element} dest The destination element.
1298 		 * @param {Object} skipAttributes A dictionary of attributes to skip.
1299 		 * @example
1300 		 */
1301 		copyAttributes : function( dest, skipAttributes )
1302 		{
1303 			var attributes = this.$.attributes;
1304 			skipAttributes = skipAttributes || {};
1305
1306 			for ( var n = 0 ; n < attributes.length ; n++ )
1307 			{
1308 				var attribute = attributes[n];
1309
1310 				// IE BUG: value attribute is never specified even if it exists.
1311 				if ( attribute.specified ||
1312 				  ( CKEDITOR.env.ie && attribute.nodeValue && attribute.nodeName.toLowerCase() == 'value' ) )
1313 				{
1314 					var attrName = attribute.nodeName;
1315 					// We can set the type only once, so do it with the proper value, not copying it.
1316 					if ( attrName in skipAttributes )
1317 						continue;
1318
1319 					var attrValue = this.getAttribute( attrName );
1320 					if ( attrValue === null )
1321 						attrValue = attribute.nodeValue;
1322
1323 					dest.setAttribute( attrName, attrValue );
1324 				}
1325 			}
1326
1327 			// The style:
1328 			if ( this.$.style.cssText !== '' )
1329 				dest.$.style.cssText = this.$.style.cssText;
1330 		},
1331
1332 		/**
1333 		 * Changes the tag name of the current element.
1334 		 * @param {String} newTag The new tag for the element.
1335 		 */
1336 		renameNode : function( newTag )
1337 		{
1338 			// If it's already correct exit here.
1339 			if ( this.getName() == newTag )
1340 				return;
1341
1342 			var doc = this.getDocument();
1343
1344 			// Create the new node.
1345 			var newNode = new CKEDITOR.dom.element( newTag, doc );
1346
1347 			// Copy all attributes.
1348 			this.copyAttributes( newNode );
1349
1350 			// Move children to the new node.
1351 			this.moveChildren( newNode );
1352
1353 			// Replace the node.
1354 			this.$.parentNode.replaceChild( newNode.$, this.$ );
1355 			newNode.$._cke_expando = this.$._cke_expando;
1356 			this.$ = newNode.$;
1357 		},
1358
1359 		/**
1360 		 * Gets a DOM tree descendant under the current node.
1361 		 * @param {Array|Number} indices The child index or array of child indices under the node.
1362 		 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1363 		 * @example
1364 		 * var strong = p.getChild(0);
1365 		 */
1366 		getChild : function( indices )
1367 		{
1368 			var rawNode = this.$;
1369
1370 			if ( !indices.slice )
1371 				rawNode = rawNode.childNodes[ indices ];
1372 			else
1373 			{
1374 				while ( indices.length > 0 && rawNode )
1375 					rawNode = rawNode.childNodes[ indices.shift() ];
1376 			}
1377
1378 			return rawNode ? new CKEDITOR.dom.node( rawNode ) : null;
1379 		},
1380
1381 		getChildCount : function()
1382 		{
1383 			return this.$.childNodes.length;
1384  		}
1385 	});
1386