2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview Defines the {@link CKEDITOR.dom.element} class, which
8 * represents a DOM element.
12 * Represents a DOM element.
14 * // Create a new <span> element.
15 * var element = new CKEDITOR.dom.element( 'span' );
17 * // Create an element based on a native DOM element.
18 * var element = new CKEDITOR.dom.element( document.getElementById( 'myId' ) );
21 * @extends CKEDITOR.dom.node
22 * @constructor Creates an element class instance.
23 * @param {Object/String} element A native DOM element or the element name for
25 * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
26 * the element in case of element creation.
28 CKEDITOR
.dom
.element = function( element
, ownerDocument
) {
29 if ( typeof element
== 'string' )
30 element
= ( ownerDocument
? ownerDocument
.$ : document
).createElement( element
);
32 // Call the base constructor (we must not call CKEDITOR.dom.node).
33 CKEDITOR
.dom
.domObject
.call( this, element
);
36 // PACKAGER_RENAME( CKEDITOR.dom.element )
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.
42 * var element = new CKEDITOR.dom.element( 'span' );
43 * alert( element == CKEDITOR.dom.element.get( element ) ); // true
45 * var element = document.getElementById( 'myElement' );
46 * alert( CKEDITOR.dom.element.get( element ).getName() ); // (e.g.) 'p'
49 * @param {String/Object} element Element's id or name or native DOM element.
50 * @returns {CKEDITOR.dom.element} The transformed element.
52 CKEDITOR
.dom
.element
.get = function( element
) {
53 var el
= typeof element
== 'string' ? document
.getElementById( element
) || document
.getElementsByName( element
)[ 0 ] : element
;
55 return el
&& ( el
.$ ? el : new CKEDITOR
.dom
.element( el
) );
58 CKEDITOR
.dom
.element
.prototype = new CKEDITOR
.dom
.node();
61 * Creates an instance of the {@link CKEDITOR.dom.element} class based on the
62 * HTML representation of an element.
64 * var element = CKEDITOR.dom.element.createFromHtml( '<strong class="anyclass">My element</strong>' );
65 * alert( element.getName() ); // 'strong'
68 * @param {String} html The element HTML. It should define only one element in
69 * the "root" level. The "root" element can have child nodes, but not siblings.
70 * @returns {CKEDITOR.dom.element} The element instance.
72 CKEDITOR
.dom
.element
.createFromHtml = function( html
, ownerDocument
) {
73 var temp
= new CKEDITOR
.dom
.element( 'div', ownerDocument
);
76 // When returning the node, remove it from its parent to detach it.
77 return temp
.getFirst().remove();
81 * Sets {@link CKEDITOR.dom.element#setCustomData custom data} on an element in a way that it is later
82 * possible to {@link #clearAllMarkers clear all data} set on all elements sharing the same database.
84 * This mechanism is very useful when processing some portion of DOM. All markers can later be removed
85 * by calling the {@link #clearAllMarkers} method, hence markers will not leak to second pass of this algorithm.
88 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
89 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
91 * element1.getCustomData( 'foo' ); // 'bar'
92 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
94 * CKEDITOR.dom.element.clearAllMarkers( database );
96 * element1.getCustomData( 'foo' ); // null
99 * @param {Object} database
100 * @param {CKEDITOR.dom.element} element
101 * @param {String} name
102 * @param {Object} value
103 * @returns {CKEDITOR.dom.element} The element.
105 CKEDITOR
.dom
.element
.setMarker = function( database
, element
, name
, value
) {
106 var id
= element
.getCustomData( 'list_marker_id' ) || ( element
.setCustomData( 'list_marker_id', CKEDITOR
.tools
.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
107 markerNames
= element
.getCustomData( 'list_marker_names' ) || ( element
.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
108 database
[ id
] = element
;
109 markerNames
[ name
] = 1;
111 return element
.setCustomData( name
, value
);
115 * Removes all markers added using this database. See the {@link #setMarker} method for more information.
117 * @param {Object} database
120 CKEDITOR
.dom
.element
.clearAllMarkers = function( database
) {
121 for ( var i
in database
)
122 CKEDITOR
.dom
.element
.clearMarkers( database
, database
[ i
], 1 );
126 * Removes all markers added to this element and removes it from the database if
127 * `removeFromDatabase` was passed. See the {@link #setMarker} method for more information.
130 * CKEDITOR.dom.element.setMarker( database, element1, 'foo', 'bar' );
131 * CKEDITOR.dom.element.setMarker( database, element2, 'oof', [ 1, 2, 3 ] );
133 * element1.getCustomData( 'foo' ); // 'bar'
134 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
136 * CKEDITOR.dom.element.clearMarkers( database, element1, true );
138 * element1.getCustomData( 'foo' ); // null
139 * element2.getCustomData( 'oof' ); // [ 1, 2, 3 ]
141 * @param {Object} database
144 CKEDITOR
.dom
.element
.clearMarkers = function( database
, element
, removeFromDatabase
) {
145 var names
= element
.getCustomData( 'list_marker_names' ),
146 id
= element
.getCustomData( 'list_marker_id' );
147 for ( var i
in names
)
148 element
.removeCustomData( i
);
149 element
.removeCustomData( 'list_marker_names' );
150 if ( removeFromDatabase
) {
151 element
.removeCustomData( 'list_marker_id' );
152 delete database
[ id
];
157 var elementsClassList
= document
.createElement( '_' ).classList
,
158 supportsClassLists
= typeof elementsClassList
!== 'undefined' && String( elementsClassList
.add
).match( /\[Native code
\]/gi
) !== null,
159 rclass
= /[\n\t\r]/g;
161 function hasClass( classNames
, className
) {
163 return ( ' ' + classNames
+ ' ' ).replace( rclass
, ' ' ).indexOf( ' ' + className
+ ' ' ) > -1;
166 CKEDITOR
.tools
.extend( CKEDITOR
.dom
.element
.prototype, {
168 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
171 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
173 type: CKEDITOR
.NODE_ELEMENT
,
176 * Adds a CSS class to the element. It appends the class to the
177 * already existing names.
179 * var element = new CKEDITOR.dom.element( 'div' );
180 * element.addClass( 'classA' ); // <div class="classA">
181 * element.addClass( 'classB' ); // <div class="classA classB">
182 * element.addClass( 'classA' ); // <div class="classA classB">
184 * **Note:** Since CKEditor 4.5 this method cannot be used with multiple classes (`'classA classB'`).
188 * @param {String} className The name of the class to be added.
190 addClass: supportsClassLists
?
191 function( className
) {
192 this.$.classList
.add( className
);
195 } : function( className
) {
196 var c
= this.$.className
;
198 if ( !hasClass( c
, className
) )
199 c
+= ' ' + className
;
201 this.$.className
= c
|| className
;
207 * Removes a CSS class name from the elements classes. Other classes
210 * var element = new CKEDITOR.dom.element( 'div' );
211 * element.addClass( 'classA' ); // <div class="classA">
212 * element.addClass( 'classB' ); // <div class="classA classB">
213 * element.removeClass( 'classA' ); // <div class="classB">
214 * element.removeClass( 'classB' ); // <div>
217 * @method removeClass
218 * @param {String} className The name of the class to remove.
220 removeClass: supportsClassLists
?
221 function( className
) {
223 $.classList
.remove( className
);
226 $.removeAttribute( 'class' );
229 } : function( className
) {
230 var c
= this.getAttribute( 'class' );
231 if ( c
&& hasClass( c
, className
) ) {
233 .replace( new RegExp( '(?:^|\\s+)' + className
+ '(?=\\s|$)' ), '' )
234 .replace( /^\s+/, '' );
237 this.setAttribute( 'class', c
);
239 this.removeAttribute( 'class' );
246 * Checks if element has class name.
248 * @param {String} className
251 hasClass: function( className
) {
252 return hasClass( this.$.className
, className
);
256 * Append a node as a child of this element.
258 * var p = new CKEDITOR.dom.element( 'p' );
260 * var strong = new CKEDITOR.dom.element( 'strong' );
261 * p.append( strong );
263 * var em = p.append( 'em' );
265 * // Result: '<p><strong></strong><em></em></p>'
267 * @param {CKEDITOR.dom.node/String} node The node or element name to be appended.
268 * @param {Boolean} [toStart=false] Indicates that the element is to be appended at the start.
269 * @returns {CKEDITOR.dom.node} The appended node.
271 append: function( node
, toStart
) {
272 if ( typeof node
== 'string' )
273 node
= this.getDocument().createElement( node
);
276 this.$.insertBefore( node
.$, this.$.firstChild
);
278 this.$.appendChild( node
.$ );
284 * Append HTML as a child(ren) of this element.
286 * @param {String} html
288 appendHtml: function( html
) {
289 if ( !this.$.childNodes
.length
)
290 this.setHtml( html
);
292 var temp
= new CKEDITOR
.dom
.element( 'div', this.getDocument() );
293 temp
.setHtml( html
);
294 temp
.moveChildren( this );
299 * Append text to this element.
301 * var p = new CKEDITOR.dom.element( 'p' );
302 * p.appendText( 'This is' );
303 * p.appendText( ' some text' );
305 * // Result: '<p>This is some text</p>'
307 * @param {String} text The text to be appended.
309 appendText: function( text
) {
310 // On IE8 it is impossible to append node to script tag, so we use its text.
311 // On the contrary, on Safari the text property is unpredictable in links. (http://dev.ckeditor.com/ticket/13232)
312 if ( this.$.text
!= null && CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 )
315 this.append( new CKEDITOR
.dom
.text( text
) );
319 * Appends a `<br>` filler element to this element if the filler is not present already.
320 * By default filler is appended only if {@link CKEDITOR.env#needsBrFiller} is `true`,
321 * however when `force` is set to `true` filler will be appended regardless of the environment.
323 * @param {Boolean} [force] Append filler regardless of the environment.
325 appendBogus: function( force
) {
326 if ( !force
&& !CKEDITOR
.env
.needsBrFiller
)
329 var lastChild
= this.getLast();
331 // Ignore empty/spaces text.
332 while ( lastChild
&& lastChild
.type
== CKEDITOR
.NODE_TEXT
&& !CKEDITOR
.tools
.rtrim( lastChild
.getText() ) )
333 lastChild
= lastChild
.getPrevious();
334 if ( !lastChild
|| !lastChild
.is
|| !lastChild
.is( 'br' ) ) {
335 var bogus
= this.getDocument().createElement( 'br' );
337 CKEDITOR
.env
.gecko
&& bogus
.setAttribute( 'type', '_moz' );
339 this.append( bogus
);
344 * Breaks one of the ancestor element in the element position, moving
345 * this element between the broken parts.
347 * // Before breaking:
348 * // <b>This <i>is some<span /> sample</i> test text</b>
349 * // If "element" is <span /> and "parent" is <i>:
350 * // <b>This <i>is some</i><span /><i> sample</i> test text</b>
351 * element.breakParent( parent );
353 * // Before breaking:
354 * // <b>This <i>is some<span /> sample</i> test text</b>
355 * // If "element" is <span /> and "parent" is <b>:
356 * // <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b>
357 * element.breakParent( parent );
359 * @param {CKEDITOR.dom.element} parent The anscestor element to get broken.
360 * @param {Boolean} [cloneId=false] Whether to preserve ancestor ID attributes while breaking.
362 breakParent: function( parent
, cloneId
) {
363 var range
= new CKEDITOR
.dom
.range( this.getDocument() );
365 // We'll be extracting part of this element, so let's use our
366 // range to get the correct piece.
367 range
.setStartAfter( this );
368 range
.setEndAfter( parent
);
371 var docFrag
= range
.extractContents( false, cloneId
|| false ),
375 // Move the element outside the broken element.
376 range
.insertNode( this.remove() );
378 // In case of Internet Explorer, we must check if there is no background-color
379 // added to the element. In such case, we have to overwrite it to prevent "switching it off"
380 // by a browser (http://dev.ckeditor.com/ticket/14667).
381 if ( CKEDITOR
.env
.ie
&& !CKEDITOR
.env
.edge
) {
382 tmpElement
= new CKEDITOR
.dom
.element( 'div' );
384 while ( current
= docFrag
.getFirst() ) {
385 if ( current
.$.style
.backgroundColor
) {
386 // This is a necessary hack to make sure that IE will track backgroundColor CSS property, see
387 // http://dev.ckeditor.com/ticket/14667#comment:8 for more details.
388 current
.$.style
.backgroundColor
= current
.$.style
.backgroundColor
;
391 tmpElement
.append( current
);
394 // Re-insert the extracted piece after the element.
395 tmpElement
.insertAfter( this );
396 tmpElement
.remove( true );
398 // Re-insert the extracted piece after the element.
399 docFrag
.insertAfterNode( this );
404 * Checks if this element contains given node.
407 * @param {CKEDITOR.dom.node} node
410 contains: !document
.compareDocumentPosition
?
414 return node
.type
!= CKEDITOR
.NODE_ELEMENT
? $.contains( node
.getParent().$ ) : $ != node
.$ && $.contains( node
.$ );
415 } : function( node
) {
416 return !!( this.$.compareDocumentPosition( node
.$ ) & 16 );
420 * Moves the selection focus to this element.
422 * var element = CKEDITOR.document.getById( 'myTextarea' );
426 * @param {Boolean} defer Whether to asynchronously defer the
427 * execution by 100 ms.
429 focus: ( function() {
431 // IE throws error if the element is not visible.
437 return function( defer
) {
439 CKEDITOR
.tools
.setTimeout( exec
, 100, this );
446 * Gets the inner HTML of this element.
448 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
449 * alert( element.getHtml() ); // '<b>Example</b>'
451 * @returns {String} The inner HTML of this element.
453 getHtml: function() {
454 var retval
= this.$.innerHTML
;
455 // Strip <?xml:namespace> tags in IE. (http://dev.ckeditor.com/ticket/3341).
456 return CKEDITOR
.env
.ie
? retval
.replace( /<\?[^>]*>/g, '' ) : retval
;
460 * Gets the outer (inner plus tags) HTML of this element.
462 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="bold"><b>Example</b></div>' );
463 * alert( element.getOuterHtml() ); // '<div class="bold"><b>Example</b></div>'
465 * @returns {String} The outer HTML of this element.
467 getOuterHtml: function() {
468 if ( this.$.outerHTML
) {
469 // IE includes the <?xml:namespace> tag in the outerHTML of
470 // namespaced element. So, we must strip it here. (http://dev.ckeditor.com/ticket/3341)
471 return this.$.outerHTML
.replace( /<\?[^>]*>/, '' );
474 var tmpDiv
= this.$.ownerDocument
.createElement( 'div' );
475 tmpDiv
.appendChild( this.$.cloneNode( true ) );
476 return tmpDiv
.innerHTML
;
480 * Retrieve the bounding rectangle of the current element, in pixels,
481 * relative to the upper-left corner of the browser's client area.
483 * @returns {Object} The dimensions of the DOM element including
484 * `left`, `top`, `right`, `bottom`, `width` and `height`.
486 getClientRect: function() {
487 // http://help.dottoro.com/ljvmcrrn.php
488 var rect
= CKEDITOR
.tools
.extend( {}, this.$.getBoundingClientRect() );
490 !rect
.width
&& ( rect
.width
= rect
.right
- rect
.left
);
491 !rect
.height
&& ( rect
.height
= rect
.bottom
- rect
.top
);
497 * Sets the inner HTML of this element.
499 * var p = new CKEDITOR.dom.element( 'p' );
500 * p.setHtml( '<b>Inner</b> HTML' );
502 * // Result: '<p><b>Inner</b> HTML</p>'
505 * @param {String} html The HTML to be set for this element.
506 * @returns {String} The inserted HTML.
508 setHtml: ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 ) ?
509 // old IEs throws error on HTML manipulation (through the "innerHTML" property)
510 // on the element which resides in an DTD invalid position, e.g. <span><div></div></span>
511 // fortunately it can be worked around with DOM manipulation.
516 // Fix the case when setHtml is called on detached element.
517 // HTML5 shiv used for document in which this element was created
518 // won't affect that detached element. So get document fragment with
519 // all HTML5 elements enabled and set innerHTML while this element is appended to it.
520 if ( this.getParent() )
521 return ( $.innerHTML
= html
);
523 var $frag
= this.getDocument()._getHtml5ShivFrag();
524 $frag
.appendChild( $ );
526 $frag
.removeChild( $ );
532 this.$.innerHTML
= '';
534 var temp
= new CKEDITOR
.dom
.element( 'body', this.getDocument() );
535 temp
.$.innerHTML
= html
;
537 var children
= temp
.getChildren();
538 while ( children
.count() )
539 this.append( children
.getItem( 0 ) );
543 } : function( html
) {
544 return ( this.$.innerHTML
= html
);
548 * Sets the element contents as plain text.
550 * var element = new CKEDITOR.dom.element( 'div' );
551 * element.setText( 'A > B & C < D' );
552 * alert( element.innerHTML ); // 'A > B & C < D'
554 * @param {String} text The text to be set.
555 * @returns {String} The inserted text.
557 setText: ( function() {
558 var supportsTextContent
= document
.createElement( 'p' );
559 supportsTextContent
.innerHTML
= 'x';
560 supportsTextContent
= supportsTextContent
.textContent
;
562 return function( text
) {
563 this.$[ supportsTextContent
? 'textContent' : 'innerText' ] = text
;
568 * Gets the value of an element attribute.
570 * var element = CKEDITOR.dom.element.createFromHtml( '<input type="text" />' );
571 * alert( element.getAttribute( 'type' ) ); // 'text'
574 * @param {String} name The attribute name.
575 * @returns {String} The attribute value or null if not defined.
577 getAttribute: ( function() {
578 var standard = function( name
) {
579 return this.$.getAttribute( name
, 2 );
582 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.quirks
) ) {
583 return function( name
) {
597 var tabIndex
= standard
.call( this, name
);
599 // IE returns tabIndex=0 by default for all
600 // elements. For those elements,
601 // getAtrribute( 'tabindex', 2 ) returns 32768
602 // instead. So, we must make this check to give a
603 // uniform result among all browsers.
604 if ( tabIndex
!== 0 && this.$.tabIndex
=== 0 )
610 var attr
= this.$.attributes
.getNamedItem( name
),
611 attrValue
= attr
.specified
? attr
.nodeValue
// For value given by parser.
612 : this.$.checked
; // For value created via DOM interface.
614 return attrValue
? 'checked' : null;
618 return this.$[ name
];
621 // IE does not return inline styles via getAttribute(). See http://dev.ckeditor.com/ticket/2947.
622 return this.$.style
.cssText
;
624 case 'contenteditable':
625 case 'contentEditable':
626 return this.$.attributes
.getNamedItem( 'contentEditable' ).specified
? this.$.getAttribute( 'contentEditable' ) : null;
629 return standard
.call( this, name
);
637 * Gets the values of all element attributes.
639 * @param {Array} exclude The names of attributes to be excluded from the returned object.
640 * @return {Object} An object containing all element attributes with their values.
642 getAttributes: function( exclude
) {
644 attrDefs
= this.$.attributes
,
647 exclude
= CKEDITOR
.tools
.isArray( exclude
) ? exclude : [];
649 for ( i
= 0; i
< attrDefs
.length
; i
++ ) {
650 if ( CKEDITOR
.tools
.indexOf( exclude
, attrDefs
[ i
].name
) === -1 ) {
651 attributes
[ attrDefs
[ i
].name
] = attrDefs
[ i
].value
;
659 * Gets the nodes list containing all children of this element.
661 * @returns {CKEDITOR.dom.nodeList}
663 getChildren: function() {
664 return new CKEDITOR
.dom
.nodeList( this.$.childNodes
);
668 * Gets the current computed value of one of the element CSS style
671 * var element = new CKEDITOR.dom.element( 'span' );
672 * alert( element.getComputedStyle( 'display' ) ); // 'inline'
675 * @param {String} propertyName The style property name.
676 * @returns {String} The property value.
678 getComputedStyle: ( document
.defaultView
&& document
.defaultView
.getComputedStyle
) ?
679 function( propertyName
) {
680 var style
= this.getWindow().$.getComputedStyle( this.$, null );
682 // Firefox may return null if we call the above on a hidden iframe. (http://dev.ckeditor.com/ticket/9117)
683 return style
? style
.getPropertyValue( propertyName
) : '';
684 } : function( propertyName
) {
685 return this.$.currentStyle
[ CKEDITOR
.tools
.cssStyleToDomStyle( propertyName
) ];
689 * Gets the DTD entries for this element.
691 * @returns {Object} An object containing the list of elements accepted
695 var dtd
= CKEDITOR
.dtd
[ this.getName() ];
697 this.getDtd = function() {
705 * Gets all this element's descendants having given tag name.
708 * @param {String} tagName
710 getElementsByTag: CKEDITOR
.dom
.document
.prototype.getElementsByTag
,
713 * Gets the computed tabindex for this element.
715 * var element = CKEDITOR.document.getById( 'myDiv' );
716 * alert( element.getTabIndex() ); // (e.g.) '-1'
719 * @returns {Number} The tabindex value.
721 getTabIndex: function() {
722 var tabIndex
= this.$.tabIndex
;
724 // IE returns tabIndex=0 by default for all elements. In
725 // those cases we must check that the element really has
726 // the tabindex attribute set to zero, or it is one of
727 // those element that should have zero by default.
728 if ( tabIndex
=== 0 && !CKEDITOR
.dtd
.$tabIndex
[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
735 * Gets the text value of this element.
737 * Only in IE (which uses innerText), `<br>` will cause linebreaks,
738 * and sucessive whitespaces (including line breaks) will be reduced to
739 * a single space. This behavior is ok for us, for now. It may change
742 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' );
743 * alert( <b>element.getText()</b> ); // 'Sample text.'
745 * @returns {String} The text value.
747 getText: function() {
748 return this.$.textContent
|| this.$.innerText
|| '';
752 * Gets the window object that contains this element.
754 * @returns {CKEDITOR.dom.window} The window object.
756 getWindow: function() {
757 return this.getDocument().getWindow();
761 * Gets the value of the `id` attribute of this element.
763 * var element = CKEDITOR.dom.element.createFromHtml( '<p id="myId"></p>' );
764 * alert( element.getId() ); // 'myId'
766 * @returns {String} The element id, or null if not available.
769 return this.$.id
|| null;
773 * Gets the value of the `name` attribute of this element.
775 * var element = CKEDITOR.dom.element.createFromHtml( '<input name="myName"></input>' );
776 * alert( <b>element.getNameAtt()</b> ); // 'myName'
778 * @returns {String} The element name, or null if not available.
780 getNameAtt: function() {
781 return this.$.name
|| null;
785 * Gets the element name (tag name). The returned name is guaranteed to
786 * be always full lowercased.
788 * var element = new CKEDITOR.dom.element( 'span' );
789 * alert( element.getName() ); // 'span'
791 * @returns {String} The element name.
793 getName: function() {
794 // Cache the lowercased name inside a closure.
795 var nodeName
= this.$.nodeName
.toLowerCase();
797 if ( CKEDITOR
.env
.ie
&& ( document
.documentMode
<= 8 ) ) {
798 var scopeName
= this.$.scopeName
;
799 if ( scopeName
!= 'HTML' )
800 nodeName
= scopeName
.toLowerCase() + ':' + nodeName
;
803 this.getName = function() {
807 return this.getName();
811 * Gets the value set to this element. This value is usually available
812 * for form field elements.
814 * @returns {String} The element value.
816 getValue: function() {
821 * Gets the first child node of this element.
823 * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b></div>' );
824 * var first = element.getFirst();
825 * alert( first.getName() ); // 'b'
827 * @param {Function} evaluator Filtering the result node.
828 * @returns {CKEDITOR.dom.node} The first child node or null if not available.
830 getFirst: function( evaluator
) {
831 var first
= this.$.firstChild
,
832 retval
= first
&& new CKEDITOR
.dom
.node( first
);
833 if ( retval
&& evaluator
&& !evaluator( retval
) )
834 retval
= retval
.getNext( evaluator
);
840 * See {@link #getFirst}.
842 * @param {Function} evaluator Filtering the result node.
843 * @returns {CKEDITOR.dom.node}
845 getLast: function( evaluator
) {
846 var last
= this.$.lastChild
,
847 retval
= last
&& new CKEDITOR
.dom
.node( last
);
848 if ( retval
&& evaluator
&& !evaluator( retval
) )
849 retval
= retval
.getPrevious( evaluator
);
855 * Gets CSS style value.
857 * @param {String} name The CSS property name.
858 * @returns {String} Style value.
860 getStyle: function( name
) {
861 return this.$.style
[ CKEDITOR
.tools
.cssStyleToDomStyle( name
) ];
865 * Checks if the element name matches the specified criteria.
867 * var element = new CKEDITOR.element( 'span' );
868 * alert( element.is( 'span' ) ); // true
869 * alert( element.is( 'p', 'span' ) ); // true
870 * alert( element.is( 'p' ) ); // false
871 * alert( element.is( 'p', 'div' ) ); // false
872 * alert( element.is( { p:1,span:1 } ) ); // true
874 * @param {String.../Object} name One or more names to be checked, or a {@link CKEDITOR.dtd} object.
875 * @returns {Boolean} `true` if the element name matches any of the names.
878 var name
= this.getName();
880 // Check against the specified DTD liternal.
881 if ( typeof arguments
[ 0 ] == 'object' )
882 return !!arguments
[ 0 ][ name
];
884 // Check for tag names
885 for ( var i
= 0; i
< arguments
.length
; i
++ ) {
886 if ( arguments
[ i
] == name
)
893 * Decide whether one element is able to receive cursor.
895 * @param {Boolean} [textCursor=true] Only consider element that could receive text child.
897 isEditable: function( textCursor
) {
898 var name
= this.getName();
900 if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
901 this.getComputedStyle( 'visibility' ) == 'hidden' ||
902 CKEDITOR
.dtd
.$nonEditable
[ name
] ||
903 CKEDITOR
.dtd
.$empty
[ name
] ||
905 ( this.data( 'cke-saved-name' ) || this.hasAttribute( 'name' ) ) &&
906 !this.getChildCount()
911 if ( textCursor
!== false ) {
912 // Get the element DTD (defaults to span for unknown elements).
913 var dtd
= CKEDITOR
.dtd
[ name
] || CKEDITOR
.dtd
.span
;
914 // In the DTD # == text node.
915 return !!( dtd
&& dtd
[ '#' ] );
922 * Compare this element's inner html, tag name, attributes, etc. with other one.
924 * See [W3C's DOM Level 3 spec - node#isEqualNode](http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode)
927 * @param {CKEDITOR.dom.element} otherElement Element to compare.
930 isIdentical: function( otherElement
) {
931 // do shallow clones, but with IDs
932 var thisEl
= this.clone( 0, 1 ),
933 otherEl
= otherElement
.clone( 0, 1 );
935 // Remove distractions.
936 thisEl
.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
937 otherEl
.removeAttributes( [ '_moz_dirty', 'data-cke-expando', 'data-cke-saved-href', 'data-cke-saved-name' ] );
939 // Native comparison available.
940 if ( thisEl
.$.isEqualNode
) {
941 // Styles order matters.
942 thisEl
.$.style
.cssText
= CKEDITOR
.tools
.normalizeCssText( thisEl
.$.style
.cssText
);
943 otherEl
.$.style
.cssText
= CKEDITOR
.tools
.normalizeCssText( otherEl
.$.style
.cssText
);
944 return thisEl
.$.isEqualNode( otherEl
.$ );
946 thisEl
= thisEl
.getOuterHtml();
947 otherEl
= otherEl
.getOuterHtml();
949 // Fix tiny difference between link href in older IEs.
950 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 && this.is( 'a' ) ) {
951 var parent
= this.getParent();
952 if ( parent
.type
== CKEDITOR
.NODE_ELEMENT
) {
953 var el
= parent
.clone();
954 el
.setHtml( thisEl
), thisEl
= el
.getHtml();
955 el
.setHtml( otherEl
), otherEl
= el
.getHtml();
959 return thisEl
== otherEl
;
964 * Checks if this element is visible. May not work if the element is
965 * child of an element with visibility set to `hidden`, but works well
966 * on the great majority of cases.
968 * @returns {Boolean} True if the element is visible.
970 isVisible: function() {
971 var isVisible
= ( this.$.offsetHeight
|| this.$.offsetWidth
) && this.getComputedStyle( 'visibility' ) != 'hidden',
972 elementWindow
, elementWindowFrame
;
974 // Webkit and Opera report non-zero offsetHeight despite that
975 // element is inside an invisible iframe. (http://dev.ckeditor.com/ticket/4542)
976 if ( isVisible
&& CKEDITOR
.env
.webkit
) {
977 elementWindow
= this.getWindow();
979 if ( !elementWindow
.equals( CKEDITOR
.document
.getWindow() ) && ( elementWindowFrame
= elementWindow
.$.frameElement
) )
980 isVisible
= new CKEDITOR
.dom
.element( elementWindowFrame
).isVisible();
988 * Whether it's an empty inline elements which has no visual impact when removed.
992 isEmptyInlineRemoveable: function() {
993 if ( !CKEDITOR
.dtd
.$removeEmpty
[ this.getName() ] )
996 var children
= this.getChildren();
997 for ( var i
= 0, count
= children
.count(); i
< count
; i
++ ) {
998 var child
= children
.getItem( i
);
1000 if ( child
.type
== CKEDITOR
.NODE_ELEMENT
&& child
.data( 'cke-bookmark' ) )
1003 if ( child
.type
== CKEDITOR
.NODE_ELEMENT
&& !child
.isEmptyInlineRemoveable() || child
.type
== CKEDITOR
.NODE_TEXT
&& CKEDITOR
.tools
.trim( child
.getText() ) )
1011 * Checks if the element has any defined attributes.
1013 * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' );
1014 * alert( element.hasAttributes() ); // true
1016 * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' );
1017 * alert( element.hasAttributes() ); // false
1020 * @returns {Boolean} True if the element has attributes.
1022 hasAttributes: CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.quirks
) ?
1024 var attributes
= this.$.attributes
;
1026 for ( var i
= 0; i
< attributes
.length
; i
++ ) {
1027 var attribute
= attributes
[ i
];
1029 switch ( attribute
.nodeName
) {
1031 // IE has a strange bug. If calling removeAttribute('className'),
1032 // the attributes collection will still contain the "class"
1033 // attribute, which will be marked as "specified", even if the
1034 // outerHTML of the element is not displaying the class attribute.
1035 // Note : I was not able to reproduce it outside the editor,
1036 // but I've faced it while working on the TC of http://dev.ckeditor.com/ticket/1391.
1037 if ( this.getAttribute( 'class' ) ) {
1041 // Attributes to be ignored.
1043 case 'data-cke-expando':
1049 if ( attribute
.specified
) {
1057 var attrs
= this.$.attributes
,
1058 attrsNum
= attrs
.length
;
1060 // The _moz_dirty attribute might get into the element after pasting (http://dev.ckeditor.com/ticket/5455)
1061 var execludeAttrs
= { 'data-cke-expando': 1, _moz_dirty: 1 };
1063 return attrsNum
> 0 && ( attrsNum
> 2 || !execludeAttrs
[ attrs
[ 0 ].nodeName
] || ( attrsNum
== 2 && !execludeAttrs
[ attrs
[ 1 ].nodeName
] ) );
1067 * Checks if the specified attribute is defined for this element.
1070 * @param {String} name The attribute name.
1071 * @returns {Boolean} `true` if the specified attribute is defined.
1073 hasAttribute: ( function() {
1074 function ieHasAttribute( name
) {
1075 var $attr
= this.$.attributes
.getNamedItem( name
);
1077 if ( this.getName() == 'input' ) {
1080 return this.$.className
.length
> 0;
1082 return !!this.$.checked
;
1084 var type
= this.getAttribute( 'type' );
1085 return type
== 'checkbox' || type
== 'radio' ? this.$.value
!= 'on' : !!this.$.value
;
1092 return $attr
.specified
;
1095 if ( CKEDITOR
.env
.ie
) {
1096 if ( CKEDITOR
.env
.version
< 8 ) {
1097 return function( name
) {
1098 // On IE < 8 the name attribute cannot be retrieved
1099 // right after the element creation and setting the
1100 // name with setAttribute.
1101 if ( name
== 'name' )
1102 return !!this.$.name
;
1104 return ieHasAttribute
.call( this, name
);
1107 return ieHasAttribute
;
1110 return function( name
) {
1111 // On other browsers specified property is deprecated and return always true,
1112 // but fortunately $.attributes contains only specified attributes.
1113 return !!this.$.attributes
.getNamedItem( name
);
1119 * Hides this element (sets `display: none`).
1121 * var element = CKEDITOR.document.getById( 'myElement' );
1125 this.setStyle( 'display', 'none' );
1129 * Moves this element's children to the target element.
1131 * @param {CKEDITOR.dom.element} target
1132 * @param {Boolean} [toStart=false] Insert moved children at the
1133 * beginning of the target element.
1135 moveChildren: function( target
, toStart
) {
1145 while ( ( child
= $.lastChild
) )
1146 target
.insertBefore( $.removeChild( child
), target
.firstChild
);
1148 while ( ( child
= $.firstChild
) )
1149 target
.appendChild( $.removeChild( child
) );
1154 * Merges sibling elements that are identical to this one.
1156 * Identical child elements are also merged. For example:
1158 * <b><i></i></b><b><i></i></b> => <b><i></i></b>
1161 * @param {Boolean} [inlineOnly=true] Allow only inline elements to be merged.
1163 mergeSiblings: ( function() {
1164 function mergeElements( element
, sibling
, isNext
) {
1165 if ( sibling
&& sibling
.type
== CKEDITOR
.NODE_ELEMENT
) {
1166 // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
1167 // queuing them to be moved later. (http://dev.ckeditor.com/ticket/5567)
1168 var pendingNodes
= [];
1170 while ( sibling
.data( 'cke-bookmark' ) || sibling
.isEmptyInlineRemoveable() ) {
1171 pendingNodes
.push( sibling
);
1172 sibling
= isNext
? sibling
.getNext() : sibling
.getPrevious();
1173 if ( !sibling
|| sibling
.type
!= CKEDITOR
.NODE_ELEMENT
)
1177 if ( element
.isIdentical( sibling
) ) {
1178 // Save the last child to be checked too, to merge things like
1179 // <b><i></i></b><b><i></i></b> => <b><i></i></b>
1180 var innerSibling
= isNext
? element
.getLast() : element
.getFirst();
1182 // Move pending nodes first into the target element.
1183 while ( pendingNodes
.length
)
1184 pendingNodes
.shift().move( element
, !isNext
);
1186 sibling
.moveChildren( element
, !isNext
);
1189 // Now check the last inner child (see two comments above).
1190 if ( innerSibling
&& innerSibling
.type
== CKEDITOR
.NODE_ELEMENT
)
1191 innerSibling
.mergeSiblings();
1196 return function( inlineOnly
) {
1197 // Merge empty links and anchors also. (http://dev.ckeditor.com/ticket/5567)
1198 if ( !( inlineOnly
=== false || CKEDITOR
.dtd
.$removeEmpty
[ this.getName() ] || this.is( 'a' ) ) ) {
1202 mergeElements( this, this.getNext(), true );
1203 mergeElements( this, this.getPrevious() );
1208 * Shows this element (displays it).
1210 * var element = CKEDITOR.document.getById( 'myElement' );
1221 * Sets the value of an element attribute.
1223 * var element = CKEDITOR.document.getById( 'myElement' );
1224 * element.setAttribute( 'class', 'myClass' );
1225 * element.setAttribute( 'title', 'This is an example' );
1228 * @param {String} name The name of the attribute.
1229 * @param {String} value The value to be set to the attribute.
1230 * @returns {CKEDITOR.dom.element} This element instance.
1232 setAttribute: ( function() {
1233 var standard = function( name
, value
) {
1234 this.$.setAttribute( name
, value
);
1238 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.quirks
) ) {
1239 return function( name
, value
) {
1240 if ( name
== 'class' )
1241 this.$.className
= value
;
1242 else if ( name
== 'style' )
1243 this.$.style
.cssText
= value
;
1244 else if ( name
== 'tabindex' ) // Case sensitive.
1245 this.$.tabIndex
= value
;
1246 else if ( name
== 'checked' )
1247 this.$.checked
= value
;
1248 else if ( name
== 'contenteditable' )
1249 standard
.call( this, 'contentEditable', value
);
1251 standard
.apply( this, arguments
);
1254 } else if ( CKEDITOR
.env
.ie8Compat
&& CKEDITOR
.env
.secure
) {
1255 return function( name
, value
) {
1256 // IE8 throws error when setting src attribute to non-ssl value. (http://dev.ckeditor.com/ticket/7847)
1257 if ( name
== 'src' && value
.match( /^http:\/\// ) ) {
1259 standard
.apply( this, arguments
);
1262 standard
.apply( this, arguments
);
1272 * Sets the value of several element attributes.
1274 * var element = CKEDITOR.document.getById( 'myElement' );
1275 * element.setAttributes( {
1276 * 'class': 'myClass',
1277 * title: 'This is an example'
1281 * @param {Object} attributesPairs An object containing the names and
1282 * values of the attributes.
1283 * @returns {CKEDITOR.dom.element} This element instance.
1285 setAttributes: function( attributesPairs
) {
1286 for ( var name
in attributesPairs
)
1287 this.setAttribute( name
, attributesPairs
[ name
] );
1292 * Sets the element value. This function is usually used with form
1296 * @param {String} value The element value.
1297 * @returns {CKEDITOR.dom.element} This element instance.
1299 setValue: function( value
) {
1300 this.$.value
= value
;
1305 * Removes an attribute from the element.
1307 * var element = CKEDITOR.dom.element.createFromHtml( '<div class="classA"></div>' );
1308 * element.removeAttribute( 'class' );
1311 * @param {String} name The attribute name.
1313 removeAttribute: ( function() {
1314 var standard = function( name
) {
1315 this.$.removeAttribute( name
);
1318 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.quirks
) ) {
1319 return function( name
) {
1320 if ( name
== 'class' )
1322 else if ( name
== 'tabindex' )
1324 else if ( name
== 'contenteditable' )
1325 name
= 'contentEditable';
1326 standard
.call( this, name
);
1334 * Removes all element's attributes or just given ones.
1336 * @param {Array} [attributes] The array with attributes names.
1338 removeAttributes: function( attributes
) {
1339 if ( CKEDITOR
.tools
.isArray( attributes
) ) {
1340 for ( var i
= 0; i
< attributes
.length
; i
++ ) {
1341 this.removeAttribute( attributes
[ i
] );
1344 attributes
= attributes
|| this.getAttributes();
1346 for ( var attr
in attributes
) {
1347 attributes
.hasOwnProperty( attr
) && this.removeAttribute( attr
);
1353 * Removes a style from the element.
1355 * var element = CKEDITOR.dom.element.createFromHtml( '<div style="display:none"></div>' );
1356 * element.removeStyle( 'display' );
1359 * @param {String} name The style name.
1361 removeStyle: function( name
) {
1362 // Removes the specified property from the current style object.
1363 var $ = this.$.style
;
1365 // "removeProperty" need to be specific on the following styles.
1366 if ( !$.removeProperty
&& ( name
== 'border' || name
== 'margin' || name
== 'padding' ) ) {
1367 var names
= expandedRules( name
);
1368 for ( var i
= 0 ; i
< names
.length
; i
++ )
1369 this.removeStyle( names
[ i
] );
1373 $.removeProperty
? $.removeProperty( name
) : $.removeAttribute( CKEDITOR
.tools
.cssStyleToDomStyle( name
) );
1375 // Eventually remove empty style attribute.
1376 if ( !this.$.style
.cssText
)
1377 this.removeAttribute( 'style' );
1381 * Sets the value of an element style.
1383 * var element = CKEDITOR.document.getById( 'myElement' );
1384 * element.setStyle( 'background-color', '#ff0000' );
1385 * element.setStyle( 'margin-top', '10px' );
1386 * element.setStyle( 'float', 'right' );
1388 * @param {String} name The name of the style. The CSS naming notation
1389 * must be used (e.g. `background-color`).
1390 * @param {String} value The value to be set to the style.
1391 * @returns {CKEDITOR.dom.element} This element instance.
1393 setStyle: function( name
, value
) {
1394 this.$.style
[ CKEDITOR
.tools
.cssStyleToDomStyle( name
) ] = value
;
1399 * Sets the value of several element styles.
1401 * var element = CKEDITOR.document.getById( 'myElement' );
1402 * element.setStyles( {
1403 * position: 'absolute',
1407 * @param {Object} stylesPairs An object containing the names and
1408 * values of the styles.
1409 * @returns {CKEDITOR.dom.element} This element instance.
1411 setStyles: function( stylesPairs
) {
1412 for ( var name
in stylesPairs
)
1413 this.setStyle( name
, stylesPairs
[ name
] );
1418 * Sets the opacity of an element.
1420 * var element = CKEDITOR.document.getById( 'myElement' );
1421 * element.setOpacity( 0.75 );
1423 * @param {Number} opacity A number within the range `[0.0, 1.0]`.
1425 setOpacity: function( opacity
) {
1426 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9 ) {
1427 opacity
= Math
.round( opacity
* 100 );
1428 this.setStyle( 'filter', opacity
>= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity
+ ')' );
1430 this.setStyle( 'opacity', opacity
);
1435 * Makes the element and its children unselectable.
1437 * var element = CKEDITOR.document.getById( 'myElement' );
1438 * element.unselectable();
1442 unselectable: function() {
1443 // CSS unselectable.
1444 this.setStyles( CKEDITOR
.tools
.cssVendorPrefix( 'user-select', 'none' ) );
1446 // For IE/Opera which doesn't support for the above CSS style,
1447 // the unselectable="on" attribute only specifies the selection
1448 // process cannot start in the element itself, and it doesn't inherit.
1449 if ( CKEDITOR
.env
.ie
) {
1450 this.setAttribute( 'unselectable', 'on' );
1453 elements
= this.getElementsByTag( '*' );
1455 for ( var i
= 0, count
= elements
.count() ; i
< count
; i
++ ) {
1456 element
= elements
.getItem( i
);
1457 element
.setAttribute( 'unselectable', 'on' );
1463 * Gets closest positioned (`position != static`) ancestor.
1465 * @returns {CKEDITOR.dom.element} Positioned ancestor or `null`.
1467 getPositionedAncestor: function() {
1469 while ( current
.getName() != 'html' ) {
1470 if ( current
.getComputedStyle( 'position' ) != 'static' )
1473 current
= current
.getParent();
1479 * Gets this element's position in document.
1481 * @param {CKEDITOR.dom.document} [refDocument]
1482 * @returns {Object} Element's position.
1483 * @returns {Number} return.x
1484 * @returns {Number} return.y
1487 getDocumentPosition: function( refDocument
) {
1490 doc
= this.getDocument(),
1491 body
= doc
.getBody(),
1492 quirks
= doc
.$.compatMode
== 'BackCompat';
1494 if ( document
.documentElement
.getBoundingClientRect
&&
1495 ( CKEDITOR
.env
.ie
? CKEDITOR
.env
.version
!== 8 : true ) ) {
1496 var box
= this.$.getBoundingClientRect(),
1498 $docElem
= $doc
.documentElement
;
1500 var clientTop
= $docElem
.clientTop
|| body
.$.clientTop
|| 0,
1501 clientLeft
= $docElem
.clientLeft
|| body
.$.clientLeft
|| 0,
1502 needAdjustScrollAndBorders
= true;
1504 // http://dev.ckeditor.com/ticket/3804: getBoundingClientRect() works differently on IE and non-IE
1505 // browsers, regarding scroll positions.
1507 // On IE, the top position of the <html> element is always 0, no matter
1508 // how much you scrolled down.
1510 // On other browsers, the top position of the <html> element is negative
1512 if ( CKEDITOR
.env
.ie
) {
1513 var inDocElem
= doc
.getDocumentElement().contains( this ),
1514 inBody
= doc
.getBody().contains( this );
1516 needAdjustScrollAndBorders
= ( quirks
&& inBody
) || ( !quirks
&& inDocElem
);
1519 // http://dev.ckeditor.com/ticket/12747.
1520 if ( needAdjustScrollAndBorders
) {
1521 var scrollRelativeLeft
,
1524 // See http://dev.ckeditor.com/ticket/12758 to know more about document.(documentElement|body).scroll(Left|Top) in Webkit.
1525 if ( CKEDITOR
.env
.webkit
|| ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
>= 12 ) ) {
1526 scrollRelativeLeft
= body
.$.scrollLeft
|| $docElem
.scrollLeft
;
1527 scrollRelativeTop
= body
.$.scrollTop
|| $docElem
.scrollTop
;
1529 var scrollRelativeElement
= quirks
? body
.$ : $docElem
;
1531 scrollRelativeLeft
= scrollRelativeElement
.scrollLeft
;
1532 scrollRelativeTop
= scrollRelativeElement
.scrollTop
;
1535 x
= box
.left
+ scrollRelativeLeft
- clientLeft
;
1536 y
= box
.top
+ scrollRelativeTop
- clientTop
;
1542 while ( current
&& !( current
.getName() == 'body' || current
.getName() == 'html' ) ) {
1543 x
+= current
.$.offsetLeft
- current
.$.scrollLeft
;
1544 y
+= current
.$.offsetTop
- current
.$.scrollTop
;
1546 // Opera includes clientTop|Left into offsetTop|Left.
1547 if ( !current
.equals( this ) ) {
1548 x
+= ( current
.$.clientLeft
|| 0 );
1549 y
+= ( current
.$.clientTop
|| 0 );
1552 var scrollElement
= previous
;
1553 while ( scrollElement
&& !scrollElement
.equals( current
) ) {
1554 x
-= scrollElement
.$.scrollLeft
;
1555 y
-= scrollElement
.$.scrollTop
;
1556 scrollElement
= scrollElement
.getParent();
1560 current
= ( offsetParent
= current
.$.offsetParent
) ? new CKEDITOR
.dom
.element( offsetParent
) : null;
1564 if ( refDocument
) {
1565 var currentWindow
= this.getWindow(),
1566 refWindow
= refDocument
.getWindow();
1568 if ( !currentWindow
.equals( refWindow
) && currentWindow
.$.frameElement
) {
1569 var iframePosition
= ( new CKEDITOR
.dom
.element( currentWindow
.$.frameElement
) ).getDocumentPosition( refDocument
);
1571 x
+= iframePosition
.x
;
1572 y
+= iframePosition
.y
;
1576 if ( !document
.documentElement
.getBoundingClientRect
) {
1577 // In Firefox, we'll endup one pixel before the element positions,
1578 // so we must add it here.
1579 if ( CKEDITOR
.env
.gecko
&& !quirks
) {
1580 x
+= this.$.clientLeft
? 1 : 0;
1581 y
+= this.$.clientTop
? 1 : 0;
1585 return { x: x
, y: y
};
1589 * Make any page element visible inside the browser viewport.
1591 * @param {Boolean} [alignToTop=false]
1593 scrollIntoView: function( alignToTop
) {
1594 var parent
= this.getParent();
1598 // Scroll the element into parent container from the inner out.
1600 // Check ancestors that overflows.
1602 parent
.$.clientWidth
&& parent
.$.clientWidth
< parent
.$.scrollWidth
||
1603 parent
.$.clientHeight
&& parent
.$.clientHeight
< parent
.$.scrollHeight
;
1605 // Skip body element, which will report wrong clientHeight when containing
1606 // floated content. (http://dev.ckeditor.com/ticket/9523)
1607 if ( overflowed
&& !parent
.is( 'body' ) )
1608 this.scrollIntoParent( parent
, alignToTop
, 1 );
1610 // Walk across the frame.
1611 if ( parent
.is( 'html' ) ) {
1612 var win
= parent
.getWindow();
1614 // Avoid security error.
1616 var iframe
= win
.$.frameElement
;
1617 iframe
&& ( parent
= new CKEDITOR
.dom
.element( iframe
) );
1621 while ( ( parent
= parent
.getParent() ) );
1625 * Make any page element visible inside one of the ancestors by scrolling the parent.
1627 * @param {CKEDITOR.dom.element/CKEDITOR.dom.window} parent The container to scroll into.
1628 * @param {Boolean} [alignToTop] Align the element's top side with the container's
1629 * when `true` is specified; align the bottom with viewport bottom when
1630 * `false` is specified. Otherwise scroll on either side with the minimum
1631 * amount to show the element.
1632 * @param {Boolean} [hscroll] Whether horizontal overflow should be considered.
1634 scrollIntoParent: function( parent
, alignToTop
, hscroll
) {
1635 !parent
&& ( parent
= this.getWindow() );
1637 var doc
= parent
.getDocument();
1638 var isQuirks
= doc
.$.compatMode
== 'BackCompat';
1640 // On window <html> is scrolled while quirks scrolls <body>.
1641 if ( parent
instanceof CKEDITOR
.dom
.window
)
1642 parent
= isQuirks
? doc
.getBody() : doc
.getDocumentElement();
1644 // Scroll the parent by the specified amount.
1645 function scrollBy( x
, y
) {
1646 // Webkit doesn't support "scrollTop/scrollLeft"
1647 // on documentElement/body element.
1648 if ( /body|html/.test( parent
.getName() ) )
1649 parent
.getWindow().$.scrollBy( x
, y
);
1651 parent
.$.scrollLeft
+= x
;
1652 parent
.$.scrollTop
+= y
;
1656 // Figure out the element position relative to the specified window.
1657 function screenPos( element
, refWin
) {
1658 var pos
= { x: 0, y: 0 };
1660 if ( !( element
.is( isQuirks
? 'body' : 'html' ) ) ) {
1661 var box
= element
.$.getBoundingClientRect();
1662 pos
.x
= box
.left
, pos
.y
= box
.top
;
1665 var win
= element
.getWindow();
1666 if ( !win
.equals( refWin
) ) {
1667 var outerPos
= screenPos( CKEDITOR
.dom
.element
.get( win
.$.frameElement
), refWin
);
1668 pos
.x
+= outerPos
.x
, pos
.y
+= outerPos
.y
;
1674 // calculated margin size.
1675 function margin( element
, side
) {
1676 return parseInt( element
.getComputedStyle( 'margin-' + side
) || 0, 10 ) || 0;
1679 // [WebKit] Reset stored scrollTop value to not break scrollIntoView() method flow.
1680 // Scrolling breaks when range.select() is used right after element.scrollIntoView(). (http://dev.ckeditor.com/ticket/14659)
1681 if ( CKEDITOR
.env
.webkit
) {
1682 var editor
= this.getEditor( false );
1685 editor
._
.previousScrollTop
= null;
1689 var win
= parent
.getWindow();
1691 var thisPos
= screenPos( this, win
),
1692 parentPos
= screenPos( parent
, win
),
1693 eh
= this.$.offsetHeight
,
1694 ew
= this.$.offsetWidth
,
1695 ch
= parent
.$.clientHeight
,
1696 cw
= parent
.$.clientWidth
,
1699 // Left-top margins.
1701 x: thisPos
.x
- margin( this, 'left' ) - parentPos
.x
|| 0,
1702 y: thisPos
.y
- margin( this, 'top' ) - parentPos
.y
|| 0
1705 // Bottom-right margins.
1707 x: thisPos
.x
+ ew
+ margin( this, 'right' ) - ( ( parentPos
.x
) + cw
) || 0,
1708 y: thisPos
.y
+ eh
+ margin( this, 'bottom' ) - ( ( parentPos
.y
) + ch
) || 0
1711 // 1. Do the specified alignment as much as possible;
1712 // 2. Otherwise be smart to scroll only the minimum amount;
1713 // 3. Never cut at the top;
1714 // 4. DO NOT scroll when already visible.
1715 if ( lt
.y
< 0 || br
.y
> 0 )
1716 scrollBy( 0, alignToTop
=== true ? lt
.y : alignToTop
=== false ? br
.y : lt
.y
< 0 ? lt
.y : br
.y
);
1718 if ( hscroll
&& ( lt
.x
< 0 || br
.x
> 0 ) )
1719 scrollBy( lt
.x
< 0 ? lt
.x : br
.x
, 0 );
1723 * Switch the `class` attribute to reflect one of the triple states of an
1724 * element in one of {@link CKEDITOR#TRISTATE_ON}, {@link CKEDITOR#TRISTATE_OFF}
1725 * or {@link CKEDITOR#TRISTATE_DISABLED}.
1727 * link.setState( CKEDITOR.TRISTATE_ON );
1728 * // <a class="cke_on" aria-pressed="true">...</a>
1729 * link.setState( CKEDITOR.TRISTATE_OFF );
1730 * // <a class="cke_off">...</a>
1731 * link.setState( CKEDITOR.TRISTATE_DISABLED );
1732 * // <a class="cke_disabled" aria-disabled="true">...</a>
1734 * span.setState( CKEDITOR.TRISTATE_ON, 'cke_button' );
1735 * // <span class="cke_button_on">...</span>
1737 * @param {Number} state Indicate the element state. One of {@link CKEDITOR#TRISTATE_ON},
1738 * {@link CKEDITOR#TRISTATE_OFF}, {@link CKEDITOR#TRISTATE_DISABLED}.
1739 * @param [base='cke'] The prefix apply to each of the state class name.
1740 * @param [useAria=true] Whether toggle the ARIA state attributes besides of class name change.
1742 setState: function( state
, base
, useAria
) {
1743 base
= base
|| 'cke';
1746 case CKEDITOR
.TRISTATE_ON:
1747 this.addClass( base
+ '_on' );
1748 this.removeClass( base
+ '_off' );
1749 this.removeClass( base
+ '_disabled' );
1750 useAria
&& this.setAttribute( 'aria-pressed', true );
1751 useAria
&& this.removeAttribute( 'aria-disabled' );
1754 case CKEDITOR
.TRISTATE_DISABLED:
1755 this.addClass( base
+ '_disabled' );
1756 this.removeClass( base
+ '_off' );
1757 this.removeClass( base
+ '_on' );
1758 useAria
&& this.setAttribute( 'aria-disabled', true );
1759 useAria
&& this.removeAttribute( 'aria-pressed' );
1763 this.addClass( base
+ '_off' );
1764 this.removeClass( base
+ '_on' );
1765 this.removeClass( base
+ '_disabled' );
1766 useAria
&& this.removeAttribute( 'aria-pressed' );
1767 useAria
&& this.removeAttribute( 'aria-disabled' );
1773 * Returns the inner document of this `<iframe>` element.
1775 * @returns {CKEDITOR.dom.document} The inner document.
1777 getFrameDocument: function() {
1781 // In IE, with custom document.domain, it may happen that
1782 // the iframe is not yet available, resulting in "Access
1783 // Denied" for the following property access.
1784 $.contentWindow
.document
;
1786 // Trick to solve this issue, forcing the iframe to get ready
1787 // by simply setting its "src" property.
1791 return $ && new CKEDITOR
.dom
.document( $.contentWindow
.document
);
1795 * Copy all the attributes from one node to the other, kinda like a clone
1796 * skipAttributes is an object with the attributes that must **not** be copied.
1798 * @param {CKEDITOR.dom.element} dest The destination element.
1799 * @param {Object} skipAttributes A dictionary of attributes to skip.
1801 copyAttributes: function( dest
, skipAttributes
) {
1802 var attributes
= this.$.attributes
;
1803 skipAttributes
= skipAttributes
|| {};
1805 for ( var n
= 0; n
< attributes
.length
; n
++ ) {
1806 var attribute
= attributes
[ n
];
1808 // Lowercase attribute name hard rule is broken for
1809 // some attribute on IE, e.g. CHECKED.
1810 var attrName
= attribute
.nodeName
.toLowerCase(),
1813 // We can set the type only once, so do it with the proper value, not copying it.
1814 if ( attrName
in skipAttributes
)
1817 if ( attrName
== 'checked' && ( attrValue
= this.getAttribute( attrName
) ) )
1818 dest
.setAttribute( attrName
, attrValue
);
1819 // IE contains not specified attributes in $.attributes so we need to check
1820 // if elements attribute is specified using hasAttribute.
1821 else if ( !CKEDITOR
.env
.ie
|| this.hasAttribute( attrName
) ) {
1822 attrValue
= this.getAttribute( attrName
);
1823 if ( attrValue
=== null )
1824 attrValue
= attribute
.nodeValue
;
1826 dest
.setAttribute( attrName
, attrValue
);
1831 if ( this.$.style
.cssText
!== '' )
1832 dest
.$.style
.cssText
= this.$.style
.cssText
;
1836 * Changes the tag name of the current element.
1838 * @param {String} newTag The new tag for the element.
1840 renameNode: function( newTag
) {
1841 // If it's already correct exit here.
1842 if ( this.getName() == newTag
)
1845 var doc
= this.getDocument();
1847 // Create the new node.
1848 var newNode
= new CKEDITOR
.dom
.element( newTag
, doc
);
1850 // Copy all attributes.
1851 this.copyAttributes( newNode
);
1853 // Move children to the new node.
1854 this.moveChildren( newNode
);
1856 // Replace the node.
1857 this.getParent( true ) && this.$.parentNode
.replaceChild( newNode
.$, this.$ );
1858 newNode
.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ];
1860 // Bust getName's cache. (http://dev.ckeditor.com/ticket/8663)
1861 delete this.getName
;
1865 * Gets a DOM tree descendant under the current node.
1867 * var strong = p.getChild( 0 );
1870 * @param {Array/Number} indices The child index or array of child indices under the node.
1871 * @returns {CKEDITOR.dom.node} The specified DOM child under the current node. Null if child does not exist.
1873 getChild: ( function() {
1874 function getChild( rawNode
, index
) {
1875 var childNodes
= rawNode
.childNodes
;
1877 if ( index
>= 0 && index
< childNodes
.length
)
1878 return childNodes
[ index
];
1881 return function( indices
) {
1882 var rawNode
= this.$;
1884 if ( !indices
.slice
)
1885 rawNode
= getChild( rawNode
, indices
);
1887 indices
= indices
.slice();
1888 while ( indices
.length
> 0 && rawNode
)
1889 rawNode
= getChild( rawNode
, indices
.shift() );
1892 return rawNode
? new CKEDITOR
.dom
.node( rawNode
) : null;
1897 * Gets number of element's children.
1901 getChildCount: function() {
1902 return this.$.childNodes
.length
;
1906 * Disables browser's context menu in this element.
1908 disableContextMenu: function() {
1909 this.on( 'contextmenu', function( evt
) {
1910 // Cancel the browser context menu.
1911 if ( !evt
.data
.getTarget().getAscendant( enablesContextMenu
, true ) )
1912 evt
.data
.preventDefault();
1915 function enablesContextMenu( node
) {
1916 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.hasClass( 'cke_enable_context_menu' );
1921 * Gets element's direction. Supports both CSS `direction` prop and `dir` attr.
1923 getDirection: function( useComputed
) {
1924 if ( useComputed
) {
1925 return this.getComputedStyle( 'direction' ) ||
1926 this.getDirection() ||
1927 this.getParent() && this.getParent().getDirection( 1 ) ||
1928 this.getDocument().$.dir
||
1932 return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
1937 * Gets, sets and removes custom data to be stored as HTML5 data-* attributes.
1939 * element.data( 'extra-info', 'test' ); // Appended the attribute data-extra-info="test" to the element.
1940 * alert( element.data( 'extra-info' ) ); // 'test'
1941 * element.data( 'extra-info', false ); // Remove the data-extra-info attribute from the element.
1943 * @param {String} name The name of the attribute, excluding the `data-` part.
1944 * @param {String} [value] The value to set. If set to false, the attribute will be removed.
1946 data: function( name
, value
) {
1947 name
= 'data-' + name
;
1948 if ( value
=== undefined )
1949 return this.getAttribute( name
);
1950 else if ( value
=== false )
1951 this.removeAttribute( name
);
1953 this.setAttribute( name
, value
);
1959 * Retrieves an editor instance which is based on this element (if any).
1960 * It basically loops over {@link CKEDITOR#instances} in search for an instance
1961 * that uses the element.
1963 * var element = new CKEDITOR.dom.element( 'div' );
1964 * element.appendTo( CKEDITOR.document.getBody() );
1965 * CKEDITOR.replace( element );
1966 * alert( element.getEditor().name ); // 'editor1'
1968 * By default this method considers only original DOM elements upon which the editor
1969 * was created. Setting `optimized` parameter to `false` will consider editor editable
1972 * @param {Boolean} [optimized=true] If set to `false` it will scan every editor editable.
1973 * @returns {CKEDITOR.editor} An editor instance or null if nothing has been found.
1975 getEditor: function( optimized
) {
1976 var instances
= CKEDITOR
.instances
,
1977 name
, instance
, editable
;
1979 optimized
= optimized
|| optimized
=== undefined;
1981 for ( name
in instances
) {
1982 instance
= instances
[ name
];
1984 if ( instance
.element
.equals( this ) && instance
.elementMode
!= CKEDITOR
.ELEMENT_MODE_APPENDTO
)
1988 editable
= instance
.editable();
1990 if ( editable
&& ( editable
.equals( this ) || editable
.contains( this ) ) ) {
2000 * Returns list of elements within this element that match specified `selector`.
2004 * * Not available in IE7.
2005 * * Returned list is not a live collection (like a result of native `querySelectorAll`).
2006 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2008 * HTML: '<body><div><i>foo</i></div></body>'
2009 * Native: div.querySelectorAll( 'body i' ) // -> [ <i>foo</i> ]
2010 * Method: div.find( 'body i' ) // -> []
2011 * div.find( 'i' ) // -> [ <i>foo</i> ]
2014 * @param {String} selector
2015 * @returns {CKEDITOR.dom.nodeList}
2017 find: function( selector
) {
2018 var removeTmpId
= createTmpId( this ),
2019 list
= new CKEDITOR
.dom
.nodeList(
2020 this.$.querySelectorAll( getContextualizedSelector( this, selector
) )
2029 * Returns first element within this element that matches specified `selector`.
2033 * * Not available in IE7.
2034 * * Unlike native `querySelectorAll` this method ensures selector contextualization. This is:
2036 * HTML: '<body><div><i>foo</i></div></body>'
2037 * Native: div.querySelector( 'body i' ) // -> <i>foo</i>
2038 * Method: div.findOne( 'body i' ) // -> null
2039 * div.findOne( 'i' ) // -> <i>foo</i>
2042 * @param {String} selector
2043 * @returns {CKEDITOR.dom.element}
2045 findOne: function( selector
) {
2046 var removeTmpId
= createTmpId( this ),
2047 found
= this.$.querySelector( getContextualizedSelector( this, selector
) );
2051 return found
? new CKEDITOR
.dom
.element( found
) : null;
2055 * Traverse the DOM of this element (inclusive), executing a callback for
2058 * var element = CKEDITOR.dom.element.createFromHtml( '<div><p>foo<b>bar</b>bom</p></div>' );
2059 * element.forEach( function( node ) {
2060 * console.log( node );
2063 * // 1. <div> element,
2064 * // 2. <p> element,
2065 * // 3. "foo" text node,
2066 * // 4. <b> element,
2067 * // 5. "bar" text node,
2068 * // 6. "bom" text node.
2071 * @param {Function} callback Function to be executed on every node.
2072 * If `callback` returns `false` descendants of the node will be ignored.
2073 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
2074 * @param {Number} [type] If specified `callback` will be executed only on
2075 * nodes of this type.
2076 * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
2078 forEach: function( callback
, type
, skipRoot
) {
2079 if ( !skipRoot
&& ( !type
|| this.type
== type
) )
2080 var ret
= callback( this );
2082 // Do not filter children if callback returned false.
2083 if ( ret
=== false )
2086 var children
= this.getChildren(),
2090 // We do not cache the size, because the live list of nodes may be changed by the callback.
2091 for ( ; i
< children
.count(); i
++ ) {
2092 node
= children
.getItem( i
);
2093 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
2094 node
.forEach( callback
, type
);
2095 else if ( !type
|| node
.type
== type
)
2101 function createTmpId( element
) {
2104 if ( !element
.$.id
) {
2105 element
.$.id
= 'cke_tmp_' + CKEDITOR
.tools
.getNextNumber();
2111 element
.removeAttribute( 'id' );
2115 function getContextualizedSelector( element
, selector
) {
2116 var id
= CKEDITOR
.tools
.escapeCss( element
.$.id
);
2117 return '#' + id
+ ' ' + selector
.split( /,\s*/ ).join( ', #' + id
+ ' ' );
2121 width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
2122 height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
2125 // Generate list of specific style rules, applicable to margin/padding/border.
2126 function expandedRules( style
) {
2127 var sides
= [ 'top', 'left', 'right', 'bottom' ], components
;
2129 if ( style
== 'border' )
2130 components
= [ 'color', 'style', 'width' ];
2133 for ( var i
= 0 ; i
< sides
.length
; i
++ ) {
2136 for ( var j
= 0 ; j
< components
.length
; j
++ )
2137 styles
.push( [ style
, sides
[ i
], components
[ j
] ].join( '-' ) );
2139 styles
.push( [ style
, sides
[ i
] ].join( '-' ) );
2146 function marginAndPaddingSize( type
) {
2148 for ( var i
= 0, len
= sides
[ type
].length
; i
< len
; i
++ )
2149 adjustment
+= parseFloat( this.getComputedStyle( sides
[ type
][ i
] ) || 0, 10 ) || 0;
2154 * Sets the element size considering the box model.
2156 * @param {'width'/'height'} type The dimension to set.
2157 * @param {Number} size The length unit in px.
2158 * @param {Boolean} isBorderBox Apply the size based on the border box model.
2160 CKEDITOR
.dom
.element
.prototype.setSize = function( type
, size
, isBorderBox
) {
2161 if ( typeof size
== 'number' ) {
2162 if ( isBorderBox
&& !( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.quirks
) )
2163 size
-= marginAndPaddingSize
.call( this, type
);
2165 this.setStyle( type
, size
+ 'px' );
2170 * Gets the element size, possibly considering the box model.
2172 * @param {'width'/'height'} type The dimension to get.
2173 * @param {Boolean} isBorderBox Get the size based on the border box model.
2175 CKEDITOR
.dom
.element
.prototype.getSize = function( type
, isBorderBox
) {
2176 var size
= Math
.max( this.$[ 'offset' + CKEDITOR
.tools
.capitalize( type
) ], this.$[ 'client' + CKEDITOR
.tools
.capitalize( type
) ] ) || 0;
2179 size
-= marginAndPaddingSize
.call( this, type
);