X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fcore%2Fdom%2Felement.js;h=bb315a4b07ee18e397eb5f17a0b12196358fee19;hb=a272c66d841421f8bf933c16535bdcde1c4649fc;hp=c4e8876f43bf28a28cf777765bbe679455b8d107;hpb=ea7e3453c7b0f023b050aca6d9f83ab372860d91;p=ckeditor.git diff --git a/_source/core/dom/element.js b/_source/core/dom/element.js index c4e8876..bb315a4 100644 --- a/_source/core/dom/element.js +++ b/_source/core/dom/element.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ @@ -88,7 +88,7 @@ CKEDITOR.dom.element.setMarker = function( database, element, name, value ) CKEDITOR.dom.element.clearAllMarkers = function( database ) { for ( var i in database ) - CKEDITOR.dom.element.clearMarkers( database, database[i], true ); + CKEDITOR.dom.element.clearMarkers( database, database[i], 1 ); }; CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatabase ) @@ -105,6 +105,9 @@ CKEDITOR.dom.element.clearMarkers = function( database, element, removeFromDatab } }; +( function() +{ + CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, /** @lends CKEDITOR.dom.element.prototype */ { @@ -243,10 +246,13 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, lastChild = lastChild.getPrevious(); if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) { - this.append( - CKEDITOR.env.opera ? + var bogus = CKEDITOR.env.opera ? this.getDocument().createText('') : - this.getDocument().createElement( 'br' ) ); + this.getDocument().createElement( 'br' ); + + CKEDITOR.env.gecko && bogus.setAttribute( 'type', '_moz' ); + + this.append( bogus ); } }, @@ -256,15 +262,15 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, * @param {CKEDITOR.dom.element} parent The anscestor element to get broken. * @example * // Before breaking: - * // This is some sample test text - * // If "element" is and "parent" is : - * // This is some sample test text + * // <b>This <i>is some<span /> sample</i> test text</b> + * // If "element" is <span /> and "parent" is <i>: + * // <b>This <i>is some</i><span /><i> sample</i> test text</b> * element.breakParent( parent ); * @example * // Before breaking: - * // This is some sample test text - * // If "element" is and "parent" is : - * // This is some sample test text + * // <b>This <i>is some<span /> sample</i> test text</b> + * // If "element" is <span /> and "parent" is <b>: + * // <b>This <i>is some</i></b><span /><b><i> sample</i> test text</b> * element.breakParent( parent ); */ breakParent : function( parent ) @@ -304,12 +310,17 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, /** * Moves the selection focus to this element. + * @function + * @param {Boolean} defer Whether to asynchronously defer the + * execution by 100 ms. * @example * var element = CKEDITOR.document.getById( 'myTextarea' ); * element.focus(); */ - focus : function() + focus : ( function() { + function exec() + { // IE throws error if the element is not visible. try { @@ -317,7 +328,16 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, } catch (e) {} - }, + } + + return function( defer ) + { + if ( defer ) + CKEDITOR.tools.setTimeout( exec, 100, this ); + else + exec.call( this ); + }; + })(), /** * Gets the inner HTML of this element. @@ -328,7 +348,9 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, */ getHtml : function() { - return this.$.innerHTML; + var retval = this.$.innerHTML; + // Strip tags in IE. (#3341). + return CKEDITOR.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval; }, getOuterHtml : function() @@ -410,6 +432,13 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, name = 'className'; break; + case 'http-equiv': + name = 'httpEquiv'; + break; + + case 'name': + return this.$.name; + case 'tabindex': var tabIndex = standard.call( this, name ); @@ -425,12 +454,26 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, break; case 'checked': - return this.$.checked; - break; + { + var attr = this.$.attributes.getNamedItem( name ), + attrValue = attr.specified ? attr.nodeValue // For value given by parser. + : this.$.checked; // For value created via DOM interface. + + return attrValue ? 'checked' : null; + } + + case 'hspace': + case 'value': + return this.$[ name ]; case 'style': // IE does not return inline styles via getAttribute(). See #2947. return this.$.style.cssText; + + case 'contenteditable': + case 'contentEditable': + return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? + this.$.getAttribute( 'contentEditable' ) : null; } return standard.call( this, name ); @@ -464,7 +507,10 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, : function( propertyName ) { - return this.getWindow().$.getComputedStyle( this.$, '' ).getPropertyValue( propertyName ); + var style = this.getWindow().$.getComputedStyle( this.$, null ); + + // Firefox may return null if we call the above on a hidden iframe. (#9117) + return style ? style.getPropertyValue( propertyName ) : ''; }, /** @@ -545,7 +591,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, * in the future. * @returns {String} The text value. * @example - * var element = CKEDITOR.dom.element.createFromHtml( '<div>Same <i>text</i>.</div>' ); + * var element = CKEDITOR.dom.element.createFromHtml( '<div>Sample <i>text</i>.</div>' ); * alert( element.getText() ); // "Sample text." */ getText : function() @@ -600,7 +646,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, // Cache the lowercased name inside a closure. var nodeName = this.$.nodeName.toLowerCase(); - if ( CKEDITOR.env.ie ) + if ( CKEDITOR.env.ie && ! ( document.documentMode > 8 ) ) { var scopeName = this.$.scopeName; if ( scopeName != 'HTML' ) @@ -608,7 +654,6 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, } return ( - /** @ignore */ this.getName = function() { return nodeName; @@ -627,6 +672,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, /** * Gets the first child node of this element. + * @param {Function} evaluator Filtering the result node. * @returns {CKEDITOR.dom.node} The first child node or null if not * available. * @example @@ -634,10 +680,14 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, * var first = element.getFirst(); * alert( first.getName() ); // "b" */ - getFirst : function() + getFirst : function( evaluator ) { - var $ = this.$.firstChild; - return $ ? new CKEDITOR.dom.node( $ ) : null; + var first = this.$.firstChild, + retval = first && new CKEDITOR.dom.node( first ); + if ( retval && evaluator && !evaluator( retval ) ) + retval = retval.getNext( evaluator ); + + return retval; }, /** @@ -680,17 +730,33 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, return false; }, - isEditable : function() + /** + * Decide whether one element is able to receive cursor. + * @param {Boolean} [textCursor=true] Only consider element that could receive text child. + */ + isEditable : function( textCursor ) { - // Get the element name. var name = this.getName(); - // Get the element DTD (defaults to span for unknown elements). - var dtd = !CKEDITOR.dtd.$nonEditable[ name ] - && ( CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span ); + if ( this.isReadOnly() + || this.getComputedStyle( 'display' ) == 'none' + || this.getComputedStyle( 'visibility' ) == 'hidden' + || this.is( 'a' ) && this.data( 'cke-saved-name' ) && !this.getChildCount() + || CKEDITOR.dtd.$nonEditable[ name ] + || CKEDITOR.dtd.$empty[ name ] ) + { + return false; + } - // In the DTD # == text node. - return ( dtd && dtd['#'] ); + if ( textCursor !== false ) + { + // Get the element DTD (defaults to span for unknown elements). + var dtd = CKEDITOR.dtd[ name ] || CKEDITOR.dtd.span; + // In the DTD # == text node. + return ( dtd && dtd[ '#'] ); + } + + return true; }, isIdentical : function( otherElement ) @@ -704,14 +770,14 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, var thisLength = thisAttribs.length, otherLength = otherAttribs.length; - if ( !CKEDITOR.env.ie && thisLength != otherLength ) - return false; - for ( var i = 0 ; i < thisLength ; i++ ) { var attribute = thisAttribs[ i ]; - if ( ( !CKEDITOR.env.ie || ( attribute.specified && attribute.nodeName != '_cke_expando' ) ) && attribute.nodeValue != otherElement.getAttribute( attribute.nodeName ) ) + if ( attribute.nodeName == '_moz_dirty' ) + continue; + + if ( ( !CKEDITOR.env.ie || ( attribute.specified && attribute.nodeName != 'data-cke-expando' ) ) && attribute.nodeValue != otherElement.getAttribute( attribute.nodeName ) ) return false; } @@ -722,8 +788,8 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, for ( i = 0 ; i < otherLength ; i++ ) { attribute = otherAttribs[ i ]; - - if ( ( !CKEDITOR.env.ie || ( attribute.specified && attribute.nodeName != '_cke_expando' ) ) && attribute.nodeValue != thisAttribs.getAttribute( attribute.nodeName ) ) + if ( attribute.specified && attribute.nodeName != 'data-cke-expando' + && attribute.nodeValue != this.getAttribute( attribute.nodeName ) ) return false; } } @@ -739,18 +805,61 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, */ isVisible : function() { - return this.$.offsetWidth && ( this.$.style.visibility != 'hidden' ); + var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden', + elementWindow, + elementWindowFrame; + + // Webkit and Opera report non-zero offsetHeight despite that + // element is inside an invisible iframe. (#4542) + if ( isVisible && ( CKEDITOR.env.webkit || CKEDITOR.env.opera ) ) + { + elementWindow = this.getWindow(); + + if ( !elementWindow.equals( CKEDITOR.document.getWindow() ) + && ( elementWindowFrame = elementWindow.$.frameElement ) ) + { + isVisible = new CKEDITOR.dom.element( elementWindowFrame ).isVisible(); + } + } + + return !!isVisible; }, /** - * Indicates that the element has defined attributes. + * Whether it's an empty inline elements which has no visual impact when removed. + */ + isEmptyInlineRemoveable : function() + { + if ( !CKEDITOR.dtd.$removeEmpty[ this.getName() ] ) + return false; + + var children = this.getChildren(); + for ( var i = 0, count = children.count(); i < count; i++ ) + { + var child = children.getItem( i ); + + if ( child.type == CKEDITOR.NODE_ELEMENT && child.data( 'cke-bookmark' ) ) + continue; + + if ( child.type == CKEDITOR.NODE_ELEMENT && !child.isEmptyInlineRemoveable() + || child.type == CKEDITOR.NODE_TEXT && CKEDITOR.tools.trim( child.getText() ) ) + { + return false; + } + } + return true; + }, + + /** + * Checks if the element has any defined attributes. + * @function * @returns {Boolean} True if the element has attributes. * @example - * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); - * alert( element.hasAttributes() ); "true" + * var element = CKEDITOR.dom.element.createFromHtml( '<div title="Test">Example</div>' ); + * alert( element.hasAttributes() ); // "true" * @example - * var element = CKEDITOR.dom.element.createFromHtml( '
Example
' ); - * alert( element.hasAttributes() ); "false" + * var element = CKEDITOR.dom.element.createFromHtml( '<div>Example</div>' ); + * alert( element.hasAttributes() ); // "false" */ hasAttributes : CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) ? @@ -775,7 +884,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, return true; // Attributes to be ignored. - case '_cke_expando' : + case 'data-cke-expando' : continue; /*jsl:fallthru*/ @@ -791,21 +900,46 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, : function() { - var attributes = this.$.attributes; - return ( attributes.length > 1 || ( attributes.length == 1 && attributes[0].nodeName != '_cke_expando' ) ); + var attrs = this.$.attributes, + attrsNum = attrs.length; + + // The _moz_dirty attribute might get into the element after pasting (#5455) + var execludeAttrs = { 'data-cke-expando' : 1, _moz_dirty : 1 }; + + return attrsNum > 0 && + ( attrsNum > 2 || + !execludeAttrs[ attrs[0].nodeName ] || + ( attrsNum == 2 && !execludeAttrs[ attrs[1].nodeName ] ) ); }, /** - * Indicates whether a specified attribute is defined for this element. + * Checks if the specified attribute is defined for this element. * @returns {Boolean} True if the specified attribute is defined. - * @param (String) name The attribute name. + * @param {String} name The attribute name. * @example */ - hasAttribute : function( name ) + hasAttribute : (function() { - var $attr = this.$.attributes.getNamedItem( name ); - return !!( $attr && $attr.specified ); - }, + function standard( name ) + { + var $attr = this.$.attributes.getNamedItem( name ); + return !!( $attr && $attr.specified ); + } + + return ( CKEDITOR.env.ie && CKEDITOR.env.version < 8 ) ? + function( name ) + { + // On IE < 8 the name attribute cannot be retrieved + // right after the element creation and setting the + // name with setAttribute. + if ( name == 'name' ) + return !!this.$.name; + + return standard.call( this, name ); + } + : + standard; + })(), /** * Hides this element (display:none). @@ -841,6 +975,67 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, }, /** + * Merges sibling elements that are identical to this one.
+ *
+ * Identical child elements are also merged. For example:
+ * <b><i></i></b><b><i></i></b> => <b><i></i></b> + * @function + * @param {Boolean} [inlineOnly] Allow only inline elements to be merged. Defaults to "true". + */ + mergeSiblings : ( function() + { + function mergeElements( element, sibling, isNext ) + { + if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) + { + // Jumping over bookmark nodes and empty inline elements, e.g. , + // queuing them to be moved later. (#5567) + var pendingNodes = []; + + while ( sibling.data( 'cke-bookmark' ) + || sibling.isEmptyInlineRemoveable() ) + { + pendingNodes.push( sibling ); + sibling = isNext ? sibling.getNext() : sibling.getPrevious(); + if ( !sibling || sibling.type != CKEDITOR.NODE_ELEMENT ) + return; + } + + if ( element.isIdentical( sibling ) ) + { + // Save the last child to be checked too, to merge things like + // => + var innerSibling = isNext ? element.getLast() : element.getFirst(); + + // Move pending nodes first into the target element. + while( pendingNodes.length ) + pendingNodes.shift().move( element, !isNext ); + + sibling.moveChildren( element, !isNext ); + sibling.remove(); + + // Now check the last inner child (see two comments above). + if ( innerSibling && innerSibling.type == CKEDITOR.NODE_ELEMENT ) + innerSibling.mergeSiblings(); + } + } + } + + return function( inlineOnly ) + { + if ( ! ( inlineOnly === false + || CKEDITOR.dtd.$removeEmpty[ this.getName() ] + || this.is( 'a' ) ) ) // Merge empty links and anchors also. (#5567) + { + return; + } + + mergeElements( this, this.getNext(), true ); + mergeElements( this, this.getPrevious() ); + }; + } )(), + + /** * Shows this element (display it). * @example * var element = CKEDITOR.dom.element.getById( 'myElement' ); @@ -886,6 +1081,20 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, this.$.tabIndex = value; else if ( name == 'checked' ) this.$.checked = value; + else if ( name == 'contenteditable' ) + standard.call( this, 'contentEditable', value ); + else + standard.apply( this, arguments ); + return this; + }; + } + else if ( CKEDITOR.env.ie8Compat && CKEDITOR.env.secure ) + { + return function( name, value ) + { + // IE8 throws error when setting src attribute to non-ssl value. (#7847) + if ( name == 'src' && value.match( /^http:\/\// ) ) + try { standard.apply( this, arguments ); } catch( e ){} else standard.apply( this, arguments ); return this; @@ -948,6 +1157,8 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, name = 'className'; else if ( name == 'tabindex' ) name = 'tabIndex'; + else if ( name == 'contenteditable' ) + name = 'contentEditable'; standard.call( this, name ); }; } @@ -957,8 +1168,16 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, removeAttributes : function ( attributes ) { - for ( var i = 0 ; i < attributes.length ; i++ ) - this.removeAttribute( attributes[ i ] ); + if ( CKEDITOR.tools.isArray( attributes ) ) + { + for ( var i = 0 ; i < attributes.length ; i++ ) + this.removeAttribute( attributes[ i ] ); + } + else + { + for ( var attr in attributes ) + attributes.hasOwnProperty( attr ) && this.removeAttribute( attr ); + } }, /** @@ -971,10 +1190,19 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, */ removeStyle : function( name ) { - if ( this.$.style.removeAttribute ) - this.$.style.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) ); - else - this.setStyle( name, '' ); + // Removes the specified property from the current style object. + var $ = this.$.style; + + // "removeProperty" need to be specific on the following styles. + if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) + { + var names = expandedRules( name ); + for ( var i = 0 ; i < names.length ; i++ ) + this.removeStyle( names[ i ] ); + return; + } + + $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( CKEDITOR.tools.cssStyleToDomStyle( name ) ); if ( !this.$.style.cssText ) this.removeAttribute( 'style' ); @@ -1025,7 +1253,7 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, */ setOpacity : function( opacity ) { - if ( CKEDITOR.env.ie ) + if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) { opacity = Math.round( opacity * 100 ); this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' ); @@ -1046,11 +1274,13 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, function() { this.$.style.MozUserSelect = 'none'; + this.on( 'dragstart', function( evt ) { evt.data.preventDefault(); } ); } : CKEDITOR.env.webkit ? function() { this.$.style.KhtmlUserSelect = 'none'; + this.on( 'dragstart', function( evt ) { evt.data.preventDefault(); } ); } : function() @@ -1058,12 +1288,13 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, if ( CKEDITOR.env.ie || CKEDITOR.env.opera ) { var element = this.$, + elements = element.getElementsByTagName("*"), e, i = 0; element.unselectable = 'on'; - while ( ( e = element.all[ i++ ] ) ) + while ( ( e = elements[ i++ ] ) ) { switch ( e.tagName.toLowerCase() ) { @@ -1096,10 +1327,9 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, getDocumentPosition : function( refDocument ) { var x = 0, y = 0, - body = this.getDocument().getBody(), - quirks = this.getDocument().$.compatMode == 'BackCompat'; - - var doc = this.getDocument(); + doc = this.getDocument(), + body = doc.getBody(), + quirks = doc.$.compatMode == 'BackCompat'; if ( document.documentElement[ "getBoundingClientRect" ] ) { @@ -1194,40 +1424,143 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, return { x : x, y : y }; }, - scrollIntoView : function( alignTop ) + /** + * Make any page element visible inside the browser viewport. + * @param {Boolean} [alignToTop] + */ + scrollIntoView : function( alignToTop ) { - // Get the element window. - var win = this.getWindow(), - winHeight = win.getViewPaneSize().height; - - // Starts from the offset that will be scrolled with the negative value of - // the visible window height. - var offset = winHeight * -1; - - // Append the view pane's height if align to top. - // Append element height if we are aligning to the bottom. - if ( alignTop ) - offset += winHeight; - else + var parent = this.getParent(); + if ( !parent ) return; + + // Scroll the element into parent container from the inner out. + do + { + // Check ancestors that overflows. + var overflowed = + parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth + || parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight; + + if ( overflowed ) + this.scrollIntoParent( parent, alignToTop, 1 ); + + // Walk across the frame. + if ( parent.is( 'html' ) ) + { + var win = parent.getWindow(); + + // Avoid security error. + try + { + var iframe = win.$.frameElement; + iframe && ( parent = new CKEDITOR.dom.element( iframe ) ); + } + catch(er){} + } + } + while ( ( parent = parent.getParent() ) ); + }, + + /** + * Make any page element visible inside one of the ancestors by scrolling the parent. + * @param {CKEDITOR.dom.element|CKEDITOR.dom.window} parent The container to scroll into. + * @param {Boolean} [alignToTop] Align the element's top side with the container's + * when true is specified; align the bottom with viewport bottom when + * false is specified. Otherwise scroll on either side with the minimum + * amount to show the element. + * @param {Boolean} [hscroll] Whether horizontal overflow should be considered. + */ + scrollIntoParent : function( parent, alignToTop, hscroll ) + { + !parent && ( parent = this.getWindow() ); + + var doc = parent.getDocument(); + var isQuirks = doc.$.compatMode == 'BackCompat'; + + // On window is scrolled while quirks scrolls . + if ( parent instanceof CKEDITOR.dom.window ) + parent = isQuirks ? doc.getBody() : doc.getDocumentElement(); + + // Scroll the parent by the specified amount. + function scrollBy( x, y ) + { + // Webkit doesn't support "scrollTop/scrollLeft" + // on documentElement/body element. + if ( /body|html/.test( parent.getName() ) ) + parent.getWindow().$.scrollBy( x, y ); + else + { + parent.$[ 'scrollLeft' ] += x; + parent.$[ 'scrollTop' ] += y; + } + } + + // Figure out the element position relative to the specified window. + function screenPos( element, refWin ) { - offset += this.$.offsetHeight || 0; + var pos = { x: 0, y: 0 }; - // Consider the margin in the scroll, which is ok for our current needs, but - // needs investigation if we will be using this function in other places. - offset += parseInt( this.getComputedStyle( 'marginBottom' ) || 0, 10 ) || 0; + if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) + { + var box = element.$.getBoundingClientRect(); + pos.x = box.left, pos.y = box.top; + } + + var win = element.getWindow(); + if ( !win.equals( refWin ) ) + { + var outerPos = screenPos( CKEDITOR.dom.element.get( win.$.frameElement ), refWin ); + pos.x += outerPos.x, pos.y += outerPos.y; + } + + return pos; + } + + // calculated margin size. + function margin( element, side ) + { + return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0; } - // Append the offsets for the entire element hierarchy. - var elementPosition = this.getDocumentPosition(); - offset += elementPosition.y; + var win = parent.getWindow(); + + var thisPos = screenPos( this, win ), + parentPos = screenPos( parent, win ), + eh = this.$.offsetHeight, + ew = this.$.offsetWidth, + ch = parent.$.clientHeight, + cw = parent.$.clientWidth, + lt, + br; - // offset value might be out of range(nagative), fix it(#3692). - offset = offset < 0 ? 0 : offset; + // Left-top margins. + lt = + { + x : thisPos.x - margin( this, 'left' ) - parentPos.x || 0, + y : thisPos.y - margin( this, 'top' ) - parentPos.y|| 0 + }; - // Scroll the window to the desired position, if not already visible(#3795). - var currentScroll = win.getScrollPosition().y; - if ( offset > currentScroll || offset < currentScroll - winHeight ) - win.$.scrollTo( 0, offset ); + // Bottom-right margins. + br = + { + x : thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0, + y : thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0 + }; + + // 1. Do the specified alignment as much as possible; + // 2. Otherwise be smart to scroll only the minimum amount; + // 3. Never cut at the top; + // 4. DO NOT scroll when already visible. + if ( lt.y < 0 || br.y > 0 ) + { + scrollBy( 0, + alignToTop === true ? lt.y : + alignToTop === false ? br.y : + lt.y < 0 ? lt.y : br.y ); + } + + if ( hscroll && ( lt.x < 0 || br.x > 0 ) ) + scrollBy( lt.x < 0 ? lt.x : br.x, 0 ); }, setState : function( state ) @@ -1306,16 +1639,22 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, { var attribute = attributes[n]; + // Lowercase attribute name hard rule is broken for + // some attribute on IE, e.g. CHECKED. + var attrName = attribute.nodeName.toLowerCase(), + attrValue; + + // We can set the type only once, so do it with the proper value, not copying it. + if ( attrName in skipAttributes ) + continue; + + if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) ) + dest.setAttribute( attrName, attrValue ); // IE BUG: value attribute is never specified even if it exists. - if ( attribute.specified || - ( CKEDITOR.env.ie && attribute.nodeValue && attribute.nodeName.toLowerCase() == 'value' ) ) + else if ( attribute.specified || + ( CKEDITOR.env.ie && attribute.nodeValue && attrName == 'value' ) ) { - var attrName = attribute.nodeName; - // We can set the type only once, so do it with the proper value, not copying it. - if ( attrName in skipAttributes ) - continue; - - var attrValue = this.getAttribute( attrName ); + attrValue = this.getAttribute( attrName ); if ( attrValue === null ) attrValue = attribute.nodeValue; @@ -1350,8 +1689,8 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, this.moveChildren( newNode ); // Replace the node. - this.$.parentNode.replaceChild( newNode.$, this.$ ); - newNode.$._cke_expando = this.$._cke_expando; + this.getParent() && this.$.parentNode.replaceChild( newNode.$, this.$ ); + newNode.$[ 'data-cke-expando' ] = this.$[ 'data-cke-expando' ]; this.$ = newNode.$; }, @@ -1380,5 +1719,126 @@ CKEDITOR.tools.extend( CKEDITOR.dom.element.prototype, getChildCount : function() { return this.$.childNodes.length; - } + }, + + disableContextMenu : function() + { + this.on( 'contextmenu', function( event ) + { + // Cancel the browser context menu. + if ( !event.data.getTarget().hasClass( 'cke_enable_context_menu' ) ) + event.data.preventDefault(); + } ); + }, + + /** + * Gets element's direction. Supports both CSS 'direction' prop and 'dir' attr. + */ + getDirection : function( useComputed ) + { + return useComputed ? + this.getComputedStyle( 'direction' ) + // Webkit: offline element returns empty direction (#8053). + || this.getDirection() + || this.getDocument().$.dir + || this.getDocument().getBody().getDirection( 1 ) + : this.getStyle( 'direction' ) || this.getAttribute( 'dir' ); + }, + + /** + * Gets, sets and removes custom data to be stored as HTML5 data-* attributes. + * @param {String} name The name of the attribute, excluding the 'data-' part. + * @param {String} [value] The value to set. If set to false, the attribute will be removed. + * @example + * element.data( 'extra-info', 'test' ); // appended the attribute data-extra-info="test" to the element + * alert( element.data( 'extra-info' ) ); // "test" + * element.data( 'extra-info', false ); // remove the data-extra-info attribute from the element + */ + data : function ( name, value ) + { + name = 'data-' + name; + if ( value === undefined ) + return this.getAttribute( name ); + else if ( value === false ) + this.removeAttribute( name ); + else + this.setAttribute( name, value ); + + return null; + } }); + + var sides = { + width : [ "border-left-width", "border-right-width","padding-left", "padding-right" ], + height : [ "border-top-width", "border-bottom-width", "padding-top", "padding-bottom" ] + }; + + // Generate list of specific style rules, applicable to margin/padding/border. + function expandedRules( style ) + { + var sides = [ 'top', 'left', 'right', 'bottom' ], components; + + if ( style == 'border' ) + components = [ 'color', 'style', 'width' ]; + + var styles = []; + for ( var i = 0 ; i < sides.length ; i++ ) + { + + if ( components ) + { + for ( var j = 0 ; j < components.length ; j++ ) + styles.push( [ style, sides[ i ], components[j] ].join( '-' ) ); + } + else + styles.push( [ style, sides[ i ] ].join( '-' ) ); + } + + return styles; + } + + function marginAndPaddingSize( type ) + { + var adjustment = 0; + for ( var i = 0, len = sides[ type ].length; i < len; i++ ) + adjustment += parseInt( this.getComputedStyle( sides [ type ][ i ] ) || 0, 10 ) || 0; + return adjustment; + } + + /** + * Sets the element size considering the box model. + * @name CKEDITOR.dom.element.prototype.setSize + * @function + * @param {String} type The dimension to set. It accepts "width" and "height". + * @param {Number} size The length unit in px. + * @param {Boolean} isBorderBox Apply the size based on the border box model. + */ + CKEDITOR.dom.element.prototype.setSize = function( type, size, isBorderBox ) + { + if ( typeof size == 'number' ) + { + if ( isBorderBox && !( CKEDITOR.env.ie && CKEDITOR.env.quirks ) ) + size -= marginAndPaddingSize.call( this, type ); + + this.setStyle( type, size + 'px' ); + } + }; + + /** + * Gets the element size, possibly considering the box model. + * @name CKEDITOR.dom.element.prototype.getSize + * @function + * @param {String} type The dimension to get. It accepts "width" and "height". + * @param {Boolean} isBorderBox Get the size based on the border box model. + */ + CKEDITOR.dom.element.prototype.getSize = function( type, isBorderBox ) + { + var size = Math.max( this.$[ 'offset' + CKEDITOR.tools.capitalize( type ) ], + this.$[ 'client' + CKEDITOR.tools.capitalize( type ) ] ) || 0; + + if ( isBorderBox ) + size -= marginAndPaddingSize.call( this, type ); + + return size; + }; +})();