X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=_source%2Fplugins%2Fstyles%2Fplugin.js;h=37126f13c841384805a96f5ac43c0159bd931d30;hb=614511639979907ceb0da3614122a4d8eb963ad4;hp=1d468ad4a4c0ccc938cfcbbfdcf3aff33caa2079;hpb=ea7e3453c7b0f023b050aca6d9f83ab372860d91;p=ckeditor.git diff --git a/_source/plugins/styles/plugin.js b/_source/plugins/styles/plugin.js index 1d468ad..37126f1 100644 --- a/_source/plugins/styles/plugin.js +++ b/_source/plugins/styles/plugin.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. +Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.html or http://ckeditor.com/license */ @@ -12,8 +12,8 @@ CKEDITOR.plugins.add( 'styles', * Registers a function to be called whenever a style changes its state in the * editing area. The current state is passed to the function. The possible * states are {@link CKEDITOR.TRISTATE_ON} and {@link CKEDITOR.TRISTATE_OFF}. - * @param {CKEDITOR.style} The style to be watched. - * @param {Function} The function to be called when the style state changes. + * @param {CKEDITOR.style} style The style to be watched. + * @param {Function} callback The function to be called when the style state changes. * @example * // Create a style object for the <b> element. * var style = new CKEDITOR.style( { element : 'b' } ); @@ -60,13 +60,13 @@ CKEDITOR.editor.prototype.attachStyleStateChange = function( style, callback ) // Save the current state, so it can be compared next // time. - callback.state !== currentState; + callback.state = currentState; } } }); } - // Save the callback info, so it can be checked on the next occurence of + // Save the callback info, so it can be checked on the next occurrence of // selectionChange. styleStateChangeCallbacks.push( { style : style, fn : callback } ); }; @@ -78,7 +78,7 @@ CKEDITOR.STYLE_OBJECT = 3; (function() { var blockElements = { address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1 }; - var objectElements = { a:1,embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,ul:1 }; + var objectElements = { a:1,embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,th:1,ul:1,dl:1,dt:1,dd:1,form:1}; var semicolonFixRegex = /\s*(?:;\s*|$)/; @@ -127,6 +127,8 @@ CKEDITOR.STYLE_OBJECT = 3; applyInlineStyle : this.type == CKEDITOR.STYLE_BLOCK ? applyBlockStyle + : this.type == CKEDITOR.STYLE_OBJECT ? + applyObjectStyle : null ).call( this, range ); }, @@ -135,6 +137,8 @@ CKEDITOR.STYLE_OBJECT = 3; return ( this.removeFromRange = this.type == CKEDITOR.STYLE_INLINE ? removeInlineStyle + : this.type == CKEDITOR.STYLE_OBJECT ? + removeObjectStyle : null ).call( this, range ); }, @@ -154,17 +158,23 @@ CKEDITOR.STYLE_OBJECT = 3; case CKEDITOR.STYLE_BLOCK : return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true ); + case CKEDITOR.STYLE_OBJECT : case CKEDITOR.STYLE_INLINE : var elements = elementPath.elements; for ( var i = 0, element ; i < elements.length ; i++ ) { - element = elements[i]; + element = elements[ i ]; - if ( element == elementPath.block || element == elementPath.blockLimit ) + if ( this.type == CKEDITOR.STYLE_INLINE + && ( element == elementPath.block || element == elementPath.blockLimit ) ) continue; + if( this.type == CKEDITOR.STYLE_OBJECT + && !( element.getName() in objectElements ) ) + continue; + if ( this.checkElementRemovable( element, true ) ) return true; } @@ -172,6 +182,25 @@ CKEDITOR.STYLE_OBJECT = 3; return false; }, + /** + * Whether this style can be applied at the element path. + * @param elementPath + */ + checkApplicable : function( elementPath ) + { + switch ( this.type ) + { + case CKEDITOR.STYLE_INLINE : + case CKEDITOR.STYLE_BLOCK : + break; + + case CKEDITOR.STYLE_OBJECT : + return elementPath.lastElement.getAscendant( this.element, true ); + } + + return true; + }, + // Checks if an element, or any of its attributes, is removable by the // current style definition. checkElementRemovable : function( element, fullMatch ) @@ -198,10 +227,12 @@ CKEDITOR.STYLE_OBJECT = 3; if ( attName == '_length' ) continue; - var elementAttr = element.getAttribute( attName ); - if ( attribs[attName] == - ( attName == 'style' ? - normalizeCssText( elementAttr, false ) : elementAttr ) ) + var elementAttr = element.getAttribute( attName ) || ''; + + // Special treatment for 'style' attribute is required. + if ( attName == 'style' ? + compareCssText( attribs[ attName ], normalizeCssText( elementAttr, false ) ) + : attribs[ attName ] == elementAttr ) { if ( !fullMatch ) return true; @@ -209,7 +240,7 @@ CKEDITOR.STYLE_OBJECT = 3; else if ( fullMatch ) return false; } - if( fullMatch ) + if ( fullMatch ) return true; } else @@ -246,6 +277,39 @@ CKEDITOR.STYLE_OBJECT = 3; } } return false; + }, + + // Builds the preview HTML based on the styles definition. + buildPreview : function() + { + var styleDefinition = this._.definition, + html = [], + elementName = styleDefinition.element; + + // Avoid in the preview. + if ( elementName == 'bdo' ) + elementName = 'span'; + + html = [ '<', elementName ]; + + // Assign all defined attributes. + var attribs = styleDefinition.attributes; + if ( attribs ) + { + for ( var att in attribs ) + { + html.push( ' ', att, '="', attribs[ att ], '"' ); + } + } + + // Assign the style attribute. + var cssStyle = CKEDITOR.style.getStyleText( styleDefinition ); + if ( cssStyle ) + html.push( ' style="', cssStyle, '"' ); + + html.push( '>', styleDefinition.name, '' ); + + return html.join( '' ); } }; @@ -260,20 +324,31 @@ CKEDITOR.STYLE_OBJECT = 3; stylesDef = styleDefinition.styles; // Builds the StyleText. - - var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || ''; + var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || '', + specialStylesText = ''; if ( stylesText.length ) stylesText = stylesText.replace( semicolonFixRegex, ';' ); for ( var style in stylesDef ) - stylesText += style + ':' + stylesDef[ style ] + ';'; + { + var styleVal = stylesDef[ style ], + text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); + + // Some browsers don't support 'inherit' property value, leave them intact. (#5242) + if ( styleVal == 'inherit' ) + specialStylesText += text; + else + stylesText += text; + } // Browsers make some changes to the style when applying them. So, here // we normalize it to the browser format. if ( stylesText.length ) stylesText = normalizeCssText( stylesText ); + stylesText += specialStylesText; + // Return it, saving it to the next request. return ( styleDefinition._ST = stylesText ); }; @@ -303,57 +378,20 @@ CKEDITOR.STYLE_OBJECT = 3; // Get the DTD definition for the element. Defaults to "span". var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span ); - // Bookmark the range so we can re-select it after processing. - var bookmark = range.createBookmark(); - // Expand the range. range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); range.trim(); // Get the first node to be processed and the last, which concludes the // processing. - var boundaryNodes = range.getBoundaryNodes(); - var firstNode = boundaryNodes.startNode; - var lastNode = boundaryNodes.endNode.getNextSourceNode( true ); - - // Probably the document end is reached, we need a marker node. - if ( !lastNode ) - { - var marker; - lastNode = marker = document.createText( '' ); - lastNode.insertAfter( range.endContainer ); - } - // The detection algorithm below skips the contents inside bookmark nodes, so - // we'll need to make sure lastNode isn't the   inside a bookmark node. - var lastParent = lastNode.getParent(); - if ( lastParent && lastParent.getAttribute( '_fck_bookmark' ) ) - lastNode = lastParent; - - if ( lastNode.equals( firstNode ) ) - { - // If the last node is the same as the the first one, we must move - // it to the next one, otherwise the first one will not be - // processed. - lastNode = lastNode.getNextSourceNode( true ); - - // It may happen that there are no more nodes after it (the end of - // the document), so we must add something there to make our code - // simpler. - if ( !lastNode ) - { - lastNode = marker = document.createText( '' ); - lastNode.insertAfter( firstNode ); - } - } + var boundaryNodes = range.createBookmark(), + firstNode = boundaryNodes.startNode, + lastNode = boundaryNodes.endNode; var currentNode = firstNode; var styleRange; - // Indicates that that some useful inline content has been found, so - // the style should be applied. - var hasContents; - while ( currentNode ) { var applyStyle = false; @@ -368,20 +406,24 @@ CKEDITOR.STYLE_OBJECT = 3; var nodeType = currentNode.type; var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null; - if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) ) + if ( nodeName && currentNode.getAttribute( '_cke_bookmark' ) ) { currentNode = currentNode.getNextSourceNode( true ); continue; } // Check if the current node can be a child of the style element. - if ( !nodeName || ( dtd[ nodeName ] && ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) ) + if ( !nodeName || ( dtd[ nodeName ] + && ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) + && ( !def.childRule || def.childRule( currentNode ) ) ) ) { var currentParent = currentNode.getParent(); // Check if the style element can be a child of the current // node parent or if the element is not defined in the DTD. - if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) ) + if ( currentParent + && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) + && ( !def.parentRule || def.parentRule( currentParent ) ) ) { // This node will be part of our range, so if it has not // been started, place its start right before the node. @@ -406,7 +448,8 @@ CKEDITOR.STYLE_OBJECT = 3; // to the range. while ( !includedNode.$.nextSibling && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) - && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) + && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) + && ( !def.childRule || def.childRule( parentNode ) ) ) { includedNode = parentNode; } @@ -419,8 +462,6 @@ CKEDITOR.STYLE_OBJECT = 3; if ( !includedNode.$.nextSibling ) applyStyle = true; - if ( !hasContents ) - hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) ); } } else @@ -434,42 +475,67 @@ CKEDITOR.STYLE_OBJECT = 3; } // Apply the style if we have something to which apply it. - if ( applyStyle && hasContents && styleRange && !styleRange.collapsed ) + if ( applyStyle && styleRange && !styleRange.collapsed ) { // Build the style element, based on the style object definition. - var styleNode = getElement( this, document ); + var styleNode = getElement( this, document ), + styleHasAttrs = styleNode.hasAttributes(); // Get the element that holds the entire range. var parent = styleRange.getCommonAncestor(); + var removeList = { + styles : {}, + attrs : {}, + // Styles cannot be removed. + blockedStyles : {}, + // Attrs cannot be removed. + blockedAttrs : {} + }; + + var attName, styleName, value; + // Loop through the parents, removing the redundant attributes // from the element to be applied. while ( styleNode && parent ) { if ( parent.getName() == elementName ) { - for ( var attName in def.attributes ) + for ( attName in def.attributes ) { - if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) ) - styleNode.removeAttribute( attName ); - } + if ( removeList.blockedAttrs[ attName ] || !( value = parent.getAttribute( styleName ) ) ) + continue; - for ( var styleName in def.styles ) - { - if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) ) - styleNode.removeStyle( styleName ); + if ( styleNode.getAttribute( attName ) == value ) + removeList.attrs[ attName ] = 1; + else + removeList.blockedAttrs[ attName ] = 1; } - if ( !styleNode.hasAttributes() ) + for ( styleName in def.styles ) { - styleNode = null; - break; + if ( removeList.blockedStyles[ styleName ] || !( value = parent.getStyle( styleName ) ) ) + continue; + + if ( styleNode.getStyle( styleName ) == value ) + removeList.styles[ styleName ] = 1; + else + removeList.blockedStyles[ styleName ] = 1; } } parent = parent.getParent(); } + for ( attName in removeList.attrs ) + styleNode.removeAttribute( attName ); + + for ( styleName in removeList.styles ) + styleNode.removeStyle( styleName ); + + if ( styleHasAttrs && !styleNode.hasAttributes() ) + styleNode = null; + if ( styleNode ) { // Move the contents of the range to the style element. @@ -484,7 +550,7 @@ CKEDITOR.STYLE_OBJECT = 3; styleRange.insertNode( styleNode ); // Let's merge our new style with its neighbors, if possible. - mergeSiblings( styleNode ); + styleNode.mergeSiblings(); // As the style system breaks text nodes constantly, let's normalize // things for performance. @@ -495,6 +561,15 @@ CKEDITOR.STYLE_OBJECT = 3; if ( !CKEDITOR.env.ie ) styleNode.$.normalize(); } + // Style already inherit from parents, left just to clear up any internal overrides. (#5931) + else + { + styleNode = new CKEDITOR.dom.element( 'span' ); + styleRange.extractContents().appendTo( styleNode ); + styleRange.insertNode( styleNode ); + removeFromInsideElement( this, styleNode ); + styleNode.remove( true ); + } // Style applied, let's release the range, so it gets // re-initialization in the next loop. @@ -502,9 +577,11 @@ CKEDITOR.STYLE_OBJECT = 3; } } - // Remove the temporary marking node.(#4111) - marker && marker.remove(); - range.moveToBookmark( bookmark ); + // Remove the bookmark nodes. + range.moveToBookmark( boundaryNodes ); + + // Minimize the result range to exclude empty text nodes. (#5374) + range.shrink( CKEDITOR.SHRINK_TEXT ); } function removeInlineStyle( range ) @@ -541,12 +618,14 @@ CKEDITOR.STYLE_OBJECT = 3; if ( this.checkElementRemovable( element ) ) { - var endOfElement = range.checkBoundaryOfElement( element, CKEDITOR.END ), - startOfElement = !endOfElement && range.checkBoundaryOfElement( element, CKEDITOR.START ); - if ( startOfElement || endOfElement ) + var isStart; + + if ( range.collapsed && ( + range.checkBoundaryOfElement( element, CKEDITOR.END ) || + ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) { boundaryElement = element; - boundaryElement.match = startOfElement ? 'start' : 'end'; + boundaryElement.match = isStart ? 'start' : 'end'; } else { @@ -556,7 +635,7 @@ CKEDITOR.STYLE_OBJECT = 3; * no difference that they're separate entities in the DOM tree. So, merge * them before removal. */ - mergeSiblings( element ); + element.mergeSiblings(); removeFromElement( this, element ); } @@ -575,7 +654,7 @@ CKEDITOR.STYLE_OBJECT = 3; if ( newElement.equals( boundaryElement ) ) break; // Avoid copying any matched element. - else if( newElement.match ) + else if ( newElement.match ) continue; else newElement = newElement.clone(); @@ -645,7 +724,7 @@ CKEDITOR.STYLE_OBJECT = 3; if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) { // Remove style from element or overriding element. - if( currentNode.getName() == this.element ) + if ( currentNode.getName() == this.element ) removeFromElement( this, currentNode ); else removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] ); @@ -669,6 +748,48 @@ CKEDITOR.STYLE_OBJECT = 3; range.moveToBookmark( bookmark ); } + function applyObjectStyle( range ) + { + var root = range.getCommonAncestor( true, true ), + element = root.getAscendant( this.element, true ); + element && setupElement( element, this ); + } + + function removeObjectStyle( range ) + { + var root = range.getCommonAncestor( true, true ), + element = root.getAscendant( this.element, true ); + + if ( !element ) + return; + + var style = this; + var def = style._.definition; + var attributes = def.attributes; + var styles = CKEDITOR.style.getStyleText( def ); + + // Remove all defined attributes. + if ( attributes ) + { + for ( var att in attributes ) + { + element.removeAttribute( att, attributes[ att ] ); + } + } + + // Assign all defined styles. + if ( def.styles ) + { + for ( var i in def.styles ) + { + if ( !def.styles.hasOwnProperty( i ) ) + continue; + + element.removeStyle( i ); + } + } + } + function applyBlockStyle( range ) { // Serializible bookmarks is needed here since @@ -678,13 +799,17 @@ CKEDITOR.STYLE_OBJECT = 3; var iterator = range.createIterator(); iterator.enforceRealBlocks = true; + // make recognize
tag as a separator in ENTER_BR mode (#5121) + if ( this._.enterMode ) + iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR ); + var block; var doc = range.document; var previousPreBlock; - while( ( block = iterator.getNextParagraph() ) ) // Only one = + while ( ( block = iterator.getNextParagraph() ) ) // Only one = { - var newBlock = getElement( this, doc ); + var newBlock = getElement( this, doc, block ); replaceBlock( block, newBlock ); } @@ -719,13 +844,14 @@ CKEDITOR.STYLE_OBJECT = 3; } } + var nonWhitespaces = CKEDITOR.dom.walker.whitespaces( true ); /** * Merge a
 block with a previous sibling if available.
 	 */
 	function mergePre( preBlock )
 	{
 		var previousBlock;
-		if ( !( ( previousBlock = preBlock.getPreviousSourceNode( true, CKEDITOR.NODE_ELEMENT ) )
+		if ( !( ( previousBlock = preBlock.getPrevious( nonWhitespaces ) )
 				 && previousBlock.is
 				 && previousBlock.is( 'pre') ) )
 			return;
@@ -757,7 +883,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 	{
 		// Exclude the ones at header OR at tail,
 		// and ignore bookmark content between them.
-		var duoBrRegex = /(\S\s*)\n(?:\s|(]+_fck_bookmark.*?\/span>))*\n(?!$)/gi,
+		var duoBrRegex = /(\S\s*)\n(?:\s|(]+_cke_bookmark.*?\/span>))*\n(?!$)/gi,
 			blockName = preBlock.getName(),
 			splitedHtml = replace( preBlock.getOuterHtml(),
 				duoBrRegex,
@@ -767,7 +893,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 				} );
 
 		var pres = [];
-		splitedHtml.replace( /
([\s\S]*?)<\/pre>/gi, function( match, preContent ){
+		splitedHtml.replace( /([\s\S]*?)<\/pre>/gi, function( match, preContent ){
 			pres.push( preContent );
 		} );
 		return pres;
@@ -779,7 +905,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 		var headBookmark = '',
 			tailBookmark = '';
 
-		str = str.replace( /(^]+_fck_bookmark.*?\/span>)|(]+_fck_bookmark.*?\/span>$)/gi,
+		str = str.replace( /(^]+_cke_bookmark.*?\/span>)|(]+_cke_bookmark.*?\/span>$)/gi,
 			function( str, m1, m2 ){
 					m1 && ( headBookmark = m1 );
 					m2 && ( tailBookmark = m2 );
@@ -868,31 +994,34 @@ CKEDITOR.STYLE_OBJECT = 3;
 	function removeFromElement( style, element )
 	{
 		var def = style._.definition,
-			attributes = def.attributes,
+			attributes = CKEDITOR.tools.extend( {}, def.attributes, getOverrides( style )[ element.getName() ] ),
 			styles = def.styles,
-			overrides = getOverrides( style );
+			// If the style is only about the element itself, we have to remove the element.
+			removeEmpty = CKEDITOR.tools.isEmpty( attributes ) && CKEDITOR.tools.isEmpty( styles );
 
-		function removeAttrs()
+		// Remove definition attributes/style from the elemnt.
+		for ( var attName in attributes )
 		{
-			for ( var attName in attributes )
-			{
-				// The 'class' element value must match (#1318).
-				if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] )
-					continue;
-				element.removeAttribute( attName );
-			}
+			// The 'class' element value must match (#1318).
+			if ( ( attName == 'class' || style._.definition.fullMatch )
+				&& element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) )
+				continue;
+			removeEmpty = element.hasAttribute( attName );
+			element.removeAttribute( attName );
 		}
 
-		// Remove definition attributes/style from the elemnt.
-		removeAttrs();
 		for ( var styleName in styles )
+		{
+			// Full match style insist on having fully equivalence. (#5018)
+			if ( style._.definition.fullMatch
+				&& element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) )
+				continue;
+
+			removeEmpty = removeEmpty || !!element.getStyle( styleName );
 			element.removeStyle( styleName );
+		}
 
-		// Now remove override styles on the element.
-		attributes = overrides[ element.getName() ];
-		if( attributes )
-			removeAttrs();
-		removeNoAttribsElement( element );
+		removeEmpty && removeNoAttribsElement( element );
 	}
 
 	// Removes a style from inside an element.
@@ -979,52 +1108,16 @@ CKEDITOR.STYLE_OBJECT = 3;
 			if ( firstChild )
 			{
 				// Check the cached nodes for merging.
-				mergeSiblings( firstChild );
-
-				if ( lastChild && !firstChild.equals( lastChild ) )
-					mergeSiblings( lastChild );
-			}
-		}
-	}
-
-	function mergeSiblings( element )
-	{
-		if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] )
-			return;
-
-		mergeElements( element, element.getNext(), true );
-		mergeElements( element, element.getPrevious() );
-	}
-
-	function mergeElements( element, sibling, isNext )
-	{
-		if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT )
-		{
-			var hasBookmark = sibling.getAttribute( '_fck_bookmark' );
-
-			if ( hasBookmark )
-				sibling = isNext ? sibling.getNext() : sibling.getPrevious();
-
-			if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && element.isIdentical( sibling ) )
-			{
-				// Save the last child to be checked too, to merge things like
-				//  => 
-				var innerSibling = isNext ? element.getLast() : element.getFirst();
-
-				if ( hasBookmark )
-					( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext );
+				firstChild.type == CKEDITOR.NODE_ELEMENT && firstChild.mergeSiblings();
 
-				sibling.moveChildren( element, !isNext );
-				sibling.remove();
-
-				// Now check the last inner child (see two comments above).
-				if ( innerSibling )
-					mergeSiblings( innerSibling );
+				if ( lastChild && !firstChild.equals( lastChild )
+					&& lastChild.type == CKEDITOR.NODE_ELEMENT  )
+					lastChild.mergeSiblings();
 			}
 		}
 	}
 
-	function getElement( style, targetDocument )
+	function getElement( style, targetDocument, element )
 	{
 		var el;
 
@@ -1039,6 +1132,10 @@ CKEDITOR.STYLE_OBJECT = 3;
 		// Create the element.
 		el = new CKEDITOR.dom.element( elementName, targetDocument );
 
+		// #6226: attributes should be copied before the new ones are applied
+		if ( element )
+			element.copyAttributes( el );
+
 		return setupElement( el, style );
 	}
 
@@ -1058,7 +1155,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 		}
 
 		// Assign all defined styles.
-		if ( styles )
+		if( styles )
 			el.setAttribute( 'style', styles );
 
 		return el;
@@ -1126,7 +1223,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 	 */
 	function getOverrides( style )
 	{
-		if( style._.overrides )
+		if ( style._.overrides )
 			return style._.overrides;
 
 		var overrides = ( style._.overrides = {} ),
@@ -1182,6 +1279,15 @@ CKEDITOR.STYLE_OBJECT = 3;
 		return overrides;
 	}
 
+	// Make the comparison of attribute value easier by standardizing it.
+	function normalizeProperty( name, value, isStyle )
+	{
+		var temp = new CKEDITOR.dom.element( 'span' );
+		temp [ isStyle ? 'setStyle' : 'setAttribute' ]( name, value );
+		return temp[ isStyle ? 'getStyle' : 'getAttribute' ]( name );
+	}
+
+	// Make the comparison of style text easier by standardizing it.
 	function normalizeCssText( unparsedCssText, nativeNormalize )
 	{
 		var styleText;
@@ -1191,7 +1297,7 @@ CKEDITOR.STYLE_OBJECT = 3;
 			// retrieving its final format.
 			var temp = new CKEDITOR.dom.element( 'span' );
 			temp.setAttribute( 'style', unparsedCssText );
-			styleText = temp.getAttribute( 'style' );
+			styleText = temp.getAttribute( 'style' ) || '';
 		}
 		else
 			styleText = unparsedCssText;
@@ -1200,22 +1306,70 @@ CKEDITOR.STYLE_OBJECT = 3;
 		// Compensate tail semi-colon.
 		return styleText.replace( /\s*([;:])\s*/, '$1' )
 							 .replace( /([^\s;])$/, '$1;')
+				 			// Trimming spaces after comma(#4107),
+				 			// remove quotations(#6403),
+				 			// mostly for differences on "font-family".
+							 .replace( /,\s+/g, ',' )
+							 .replace( /\"/g,'' )
 							 .toLowerCase();
 	}
 
-	function applyStyle( document, remove )
+	// Turn inline style text properties into one hash.
+	function parseStyleText( styleText )
 	{
-		// Get all ranges from the selection.
-		var selection = document.getSelection();
-		var ranges = selection.getRanges();
-		var func = remove ? this.removeFromRange : this.applyToRange;
+		var retval = {};
+		styleText
+		   .replace( /"/g, '"' )
+		   .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value )
+		{
+			retval[ name ] = value;
+		} );
+		return retval;
+	}
 
-		// Apply the style to the ranges.
-		for ( var i = 0 ; i < ranges.length ; i++ )
-			func.call( this, ranges[ i ] );
+	/**
+	 * Compare two bunch of styles, with the speciality that value 'inherit'
+	 * is treated as a wildcard which will match any value.
+	 * @param {Object|String} source
+	 * @param {Object|String} target
+	 */
+	function compareCssText( source, target )
+	{
+		typeof source == 'string' && ( source = parseStyleText( source ) );
+		typeof target == 'string' && ( target = parseStyleText( target ) );
+		for( var name in source )
+		{
+			if ( !( name in target &&
+					( target[ name ] == source[ name ]
+						|| source[ name ] == 'inherit'
+						|| target[ name ] == 'inherit' ) ) )
+			{
+				return false;
+			}
+		}
+		return true;
+	}
 
-		// Select the ranges again.
-		selection.selectRanges( ranges );
+	function applyStyle( document, remove )
+	{
+		var selection = document.getSelection(),
+			// Bookmark the range so we can re-select it after processing.
+			bookmarks = selection.createBookmarks( 1 ),
+			ranges = selection.getRanges( 1 ),
+			func = remove ? this.removeFromRange : this.applyToRange,
+			range;
+
+		var iterator = ranges.createIterator();
+		while ( ( range = iterator.getNextRange() ) )
+			func.call( this, range );
+
+		if ( bookmarks.length == 1 && bookmarks[0].collapsed )
+		{
+			selection.selectRanges( ranges );
+			document.getById( bookmarks[ 0 ].startNode ).remove();
+		}
+		else
+			selection.selectBookmarks( bookmarks );
 	}
 })();
 
@@ -1240,3 +1394,86 @@ CKEDITOR.styleCommand.prototype.exec = function( editor )
 
 	return !!doc;
 };
+
+CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' );
+
+// Backward compatibility (#5025).
+CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet );
+CKEDITOR.loadStylesSet = function( name, url, callback )
+	{
+		CKEDITOR.stylesSet.addExternal( name, url, '' );
+		CKEDITOR.stylesSet.load( name, callback );
+	};
+
+
+/**
+ * Gets the current styleSet for this instance
+ * @param {Function} callback The function to be called with the styles data.
+ * @example
+ * editor.getStylesSet( function( stylesDefinitions ) {} );
+ */
+CKEDITOR.editor.prototype.getStylesSet = function( callback )
+{
+	if ( !this._.stylesDefinitions )
+	{
+		var editor = this,
+			// Respect the backwards compatible definition entry
+			configStyleSet = editor.config.stylesCombo_stylesSet || editor.config.stylesSet || 'default';
+
+		// #5352 Allow to define the styles directly in the config object
+		if ( configStyleSet instanceof Array )
+		{
+			editor._.stylesDefinitions = configStyleSet;
+			callback( configStyleSet );
+			return;
+		}
+
+		var	partsStylesSet = configStyleSet.split( ':' ),
+			styleSetName = partsStylesSet[ 0 ],
+			externalPath = partsStylesSet[ 1 ],
+			pluginPath = CKEDITOR.plugins.registered.styles.path;
+
+		CKEDITOR.stylesSet.addExternal( styleSetName,
+				externalPath ?
+					partsStylesSet.slice( 1 ).join( ':' ) :
+					pluginPath + 'styles/' + styleSetName + '.js', '' );
+
+		CKEDITOR.stylesSet.load( styleSetName, function( stylesSet )
+			{
+				editor._.stylesDefinitions = stylesSet[ styleSetName ];
+				callback( editor._.stylesDefinitions );
+			} ) ;
+	}
+	else
+		callback( this._.stylesDefinitions );
+};
+
+/**
+ * The "styles definition set" to use in the editor. They will be used in the
+ * styles combo and the Style selector of the div container. 
+ * The styles may be defined in the page containing the editor, or can be + * loaded on demand from an external file. In the second case, if this setting + * contains only a name, the styles definition file will be loaded from the + * "styles" folder inside the styles plugin folder. + * Otherwise, this setting has the "name:url" syntax, making it + * possible to set the URL from which loading the styles file.
+ * Previously this setting was available as config.stylesCombo_stylesSet
+ * @name CKEDITOR.config.stylesSet + * @type String|Array + * @default 'default' + * @since 3.3 + * @example + * // Load from the styles' styles folder (mystyles.js file). + * config.stylesSet = 'mystyles'; + * @example + * // Load from a relative URL. + * config.stylesSet = 'mystyles:/editorstyles/styles.js'; + * @example + * // Load from a full URL. + * config.stylesSet = 'mystyles:http://www.example.com/editorstyles/styles.js'; + * @example + * // Load from a list of definitions. + * config.stylesSet = [ + * { name : 'Strong Emphasis', element : 'strong' }, + * { name : 'Emphasis', element : 'em' }, ... ]; + */