1 /* 2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 CKEDITOR.plugins.add( 'styles', 7 { 8 requires : [ 'selection' ], 9 init : function( editor ) 10 { 11 // This doesn't look like correct, but it's the safest way to proper 12 // pass the disableReadonlyStyling configuration to the style system 13 // without having to change any method signature in the API. (#6103) 14 editor.on( 'contentDom', function() 15 { 16 editor.document.setCustomData( 'cke_includeReadonly', !editor.config.disableReadonlyStyling ); 17 }); 18 } 19 }); 20 21 /** 22 * Registers a function to be called whenever the selection position changes in the 23 * editing area. The current state is passed to the function. The possible 24 * states are {@link CKEDITOR.TRISTATE_ON} and {@link CKEDITOR.TRISTATE_OFF}. 25 * @param {CKEDITOR.style} style The style to be watched. 26 * @param {Function} callback The function to be called. 27 * @example 28 * // Create a style object for the <b> element. 29 * var style = new CKEDITOR.style( { element : 'b' } ); 30 * var editor = CKEDITOR.instances.editor1; 31 * editor.attachStyleStateChange( style, function( state ) 32 * { 33 * if ( state == CKEDITOR.TRISTATE_ON ) 34 * alert( 'The current state for the B element is ON' ); 35 * else 36 * alert( 'The current state for the B element is OFF' ); 37 * }); 38 */ 39 CKEDITOR.editor.prototype.attachStyleStateChange = function( style, callback ) 40 { 41 // Try to get the list of attached callbacks. 42 var styleStateChangeCallbacks = this._.styleStateChangeCallbacks; 43 44 // If it doesn't exist, it means this is the first call. So, let's create 45 // all the structure to manage the style checks and the callback calls. 46 if ( !styleStateChangeCallbacks ) 47 { 48 // Create the callbacks array. 49 styleStateChangeCallbacks = this._.styleStateChangeCallbacks = []; 50 51 // Attach to the selectionChange event, so we can check the styles at 52 // that point. 53 this.on( 'selectionChange', function( ev ) 54 { 55 // Loop throw all registered callbacks. 56 for ( var i = 0 ; i < styleStateChangeCallbacks.length ; i++ ) 57 { 58 var callback = styleStateChangeCallbacks[ i ]; 59 60 // Check the current state for the style defined for that 61 // callback. 62 var currentState = callback.style.checkActive( ev.data.path ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF; 63 64 // Call the callback function, passing the current 65 // state to it. 66 callback.fn.call( this, currentState ); 67 } 68 }); 69 } 70 71 // Save the callback info, so it can be checked on the next occurrence of 72 // selectionChange. 73 styleStateChangeCallbacks.push( { style : style, fn : callback } ); 74 }; 75 76 CKEDITOR.STYLE_BLOCK = 1; 77 CKEDITOR.STYLE_INLINE = 2; 78 CKEDITOR.STYLE_OBJECT = 3; 79 80 (function() 81 { 82 var blockElements = { address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1,section:1,header:1,footer:1,nav:1,article:1,aside:1,figure:1,dialog:1,hgroup:1,time:1,meter:1,menu:1,command:1,keygen:1,output:1,progress:1,details:1,datagrid:1,datalist:1 }, 83 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,audio:1,video:1 }; 84 85 var semicolonFixRegex = /\s*(?:;\s*|$)/, 86 varRegex = /#\((.+?)\)/g; 87 88 var notBookmark = CKEDITOR.dom.walker.bookmark( 0, 1 ), 89 nonWhitespaces = CKEDITOR.dom.walker.whitespaces( 1 ); 90 91 CKEDITOR.style = function( styleDefinition, variablesValues ) 92 { 93 // Inline style text as attribute should be converted 94 // to styles object. 95 var attrs = styleDefinition.attributes; 96 if ( attrs && attrs.style ) 97 { 98 styleDefinition.styles = CKEDITOR.tools.extend( {}, 99 styleDefinition.styles, parseStyleText( attrs.style ) ); 100 delete attrs.style; 101 } 102 103 if ( variablesValues ) 104 { 105 styleDefinition = CKEDITOR.tools.clone( styleDefinition ); 106 107 replaceVariables( styleDefinition.attributes, variablesValues ); 108 replaceVariables( styleDefinition.styles, variablesValues ); 109 } 110 111 var element = this.element = styleDefinition.element ? 112 ( typeof styleDefinition.element == 'string' ? styleDefinition.element.toLowerCase() : styleDefinition.element ) 113 : '*'; 114 115 this.type = 116 blockElements[ element ] ? 117 CKEDITOR.STYLE_BLOCK 118 : objectElements[ element ] ? 119 CKEDITOR.STYLE_OBJECT 120 : 121 CKEDITOR.STYLE_INLINE; 122 123 // If the 'element' property is an object with a set of possible element, it will be applied like an object style: only to existing elements 124 if ( typeof this.element == 'object' ) 125 this.type = CKEDITOR.STYLE_OBJECT; 126 127 this._ = 128 { 129 definition : styleDefinition 130 }; 131 }; 132 133 CKEDITOR.style.prototype = 134 { 135 apply : function( document ) 136 { 137 applyStyle.call( this, document, false ); 138 }, 139 140 remove : function( document ) 141 { 142 applyStyle.call( this, document, true ); 143 }, 144 145 applyToRange : function( range ) 146 { 147 return ( this.applyToRange = 148 this.type == CKEDITOR.STYLE_INLINE ? 149 applyInlineStyle 150 : this.type == CKEDITOR.STYLE_BLOCK ? 151 applyBlockStyle 152 : this.type == CKEDITOR.STYLE_OBJECT ? 153 applyObjectStyle 154 : null ).call( this, range ); 155 }, 156 157 removeFromRange : function( range ) 158 { 159 return ( this.removeFromRange = 160 this.type == CKEDITOR.STYLE_INLINE ? 161 removeInlineStyle 162 : this.type == CKEDITOR.STYLE_BLOCK ? 163 removeBlockStyle 164 : this.type == CKEDITOR.STYLE_OBJECT ? 165 removeObjectStyle 166 : null ).call( this, range ); 167 }, 168 169 applyToObject : function( element ) 170 { 171 setupElement( element, this ); 172 }, 173 174 /** 175 * Get the style state inside an element path. Returns "true" if the 176 * element is active in the path. 177 */ 178 checkActive : function( elementPath ) 179 { 180 switch ( this.type ) 181 { 182 case CKEDITOR.STYLE_BLOCK : 183 return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true ); 184 185 case CKEDITOR.STYLE_OBJECT : 186 case CKEDITOR.STYLE_INLINE : 187 188 var elements = elementPath.elements; 189 190 for ( var i = 0, element ; i < elements.length ; i++ ) 191 { 192 element = elements[ i ]; 193 194 if ( this.type == CKEDITOR.STYLE_INLINE 195 && ( element == elementPath.block || element == elementPath.blockLimit ) ) 196 continue; 197 198 if( this.type == CKEDITOR.STYLE_OBJECT ) 199 { 200 var name = element.getName(); 201 if ( !( typeof this.element == 'string' ? name == this.element : name in this.element ) ) 202 continue; 203 } 204 205 if ( this.checkElementRemovable( element, true ) ) 206 return true; 207 } 208 } 209 return false; 210 }, 211 212 /** 213 * Whether this style can be applied at the element path. 214 * @param elementPath 215 */ 216 checkApplicable : function( elementPath ) 217 { 218 switch ( this.type ) 219 { 220 case CKEDITOR.STYLE_INLINE : 221 case CKEDITOR.STYLE_BLOCK : 222 break; 223 224 case CKEDITOR.STYLE_OBJECT : 225 return elementPath.lastElement.getAscendant( this.element, true ); 226 } 227 228 return true; 229 }, 230 231 // Check if the element matches the current style definition. 232 checkElementMatch : function( element, fullMatch ) 233 { 234 var def = this._.definition; 235 236 if ( !element || !def.ignoreReadonly && element.isReadOnly() ) 237 return false; 238 239 var attribs, 240 name = element.getName(); 241 242 // If the element name is the same as the style name. 243 if ( typeof this.element == 'string' ? name == this.element : name in this.element ) 244 { 245 // If no attributes are defined in the element. 246 if ( !fullMatch && !element.hasAttributes() ) 247 return true; 248 249 attribs = getAttributesForComparison( def ); 250 251 if ( attribs._length ) 252 { 253 for ( var attName in attribs ) 254 { 255 if ( attName == '_length' ) 256 continue; 257 258 var elementAttr = element.getAttribute( attName ) || ''; 259 260 // Special treatment for 'style' attribute is required. 261 if ( attName == 'style' ? 262 compareCssText( attribs[ attName ], normalizeCssText( elementAttr, false ) ) 263 : attribs[ attName ] == elementAttr ) 264 { 265 if ( !fullMatch ) 266 return true; 267 } 268 else if ( fullMatch ) 269 return false; 270 } 271 if ( fullMatch ) 272 return true; 273 } 274 else 275 return true; 276 } 277 278 return false; 279 }, 280 281 // Checks if an element, or any of its attributes, is removable by the 282 // current style definition. 283 checkElementRemovable : function( element, fullMatch ) 284 { 285 // Check element matches the style itself. 286 if ( this.checkElementMatch( element, fullMatch ) ) 287 return true; 288 289 // Check if the element matches the style overrides. 290 var override = getOverrides( this )[ element.getName() ] ; 291 if ( override ) 292 { 293 var attribs, attName; 294 295 // If no attributes have been defined, remove the element. 296 if ( !( attribs = override.attributes ) ) 297 return true; 298 299 for ( var i = 0 ; i < attribs.length ; i++ ) 300 { 301 attName = attribs[i][0]; 302 var actualAttrValue = element.getAttribute( attName ); 303 if ( actualAttrValue ) 304 { 305 var attValue = attribs[i][1]; 306 307 // Remove the attribute if: 308 // - The override definition value is null; 309 // - The override definition value is a string that 310 // matches the attribute value exactly. 311 // - The override definition value is a regex that 312 // has matches in the attribute value. 313 if ( attValue === null || 314 ( typeof attValue == 'string' && actualAttrValue == attValue ) || 315 attValue.test( actualAttrValue ) ) 316 return true; 317 } 318 } 319 } 320 return false; 321 }, 322 323 // Builds the preview HTML based on the styles definition. 324 buildPreview : function( label ) 325 { 326 var styleDefinition = this._.definition, 327 html = [], 328 elementName = styleDefinition.element; 329 330 // Avoid <bdo> in the preview. 331 if ( elementName == 'bdo' ) 332 elementName = 'span'; 333 334 html = [ '<', elementName ]; 335 336 // Assign all defined attributes. 337 var attribs = styleDefinition.attributes; 338 if ( attribs ) 339 { 340 for ( var att in attribs ) 341 { 342 html.push( ' ', att, '="', attribs[ att ], '"' ); 343 } 344 } 345 346 // Assign the style attribute. 347 var cssStyle = CKEDITOR.style.getStyleText( styleDefinition ); 348 if ( cssStyle ) 349 html.push( ' style="', cssStyle, '"' ); 350 351 html.push( '>', ( label || styleDefinition.name ), '</', elementName, '>' ); 352 353 return html.join( '' ); 354 } 355 }; 356 357 // Build the cssText based on the styles definition. 358 CKEDITOR.style.getStyleText = function( styleDefinition ) 359 { 360 // If we have already computed it, just return it. 361 var stylesDef = styleDefinition._ST; 362 if ( stylesDef ) 363 return stylesDef; 364 365 stylesDef = styleDefinition.styles; 366 367 // Builds the StyleText. 368 var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || '', 369 specialStylesText = ''; 370 371 if ( stylesText.length ) 372 stylesText = stylesText.replace( semicolonFixRegex, ';' ); 373 374 for ( var style in stylesDef ) 375 { 376 var styleVal = stylesDef[ style ], 377 text = ( style + ':' + styleVal ).replace( semicolonFixRegex, ';' ); 378 379 // Some browsers don't support 'inherit' property value, leave them intact. (#5242) 380 if ( styleVal == 'inherit' ) 381 specialStylesText += text; 382 else 383 stylesText += text; 384 } 385 386 // Browsers make some changes to the style when applying them. So, here 387 // we normalize it to the browser format. 388 if ( stylesText.length ) 389 stylesText = normalizeCssText( stylesText ); 390 391 stylesText += specialStylesText; 392 393 // Return it, saving it to the next request. 394 return ( styleDefinition._ST = stylesText ); 395 }; 396 397 // Gets the parent element which blocks the styling for an element. This 398 // can be done through read-only elements (contenteditable=false) or 399 // elements with the "data-nostyle" attribute. 400 function getUnstylableParent( element ) 401 { 402 var unstylable, 403 editable; 404 405 while ( ( element = element.getParent() ) ) 406 { 407 if ( element.getName() == 'body' ) 408 break; 409 410 if ( element.getAttribute( 'data-nostyle' ) ) 411 unstylable = element; 412 else if ( !editable ) 413 { 414 var contentEditable = element.getAttribute( 'contentEditable' ); 415 416 if ( contentEditable == 'false' ) 417 unstylable = element; 418 else if ( contentEditable == 'true' ) 419 editable = 1; 420 } 421 } 422 423 return unstylable; 424 } 425 426 function applyInlineStyle( range ) 427 { 428 var document = range.document; 429 430 if ( range.collapsed ) 431 { 432 // Create the element to be inserted in the DOM. 433 var collapsedElement = getElement( this, document ); 434 435 // Insert the empty element into the DOM at the range position. 436 range.insertNode( collapsedElement ); 437 438 // Place the selection right inside the empty element. 439 range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END ); 440 441 return; 442 } 443 444 var elementName = this.element; 445 var def = this._.definition; 446 var isUnknownElement; 447 448 // Indicates that fully selected read-only elements are to be included in the styling range. 449 var ignoreReadonly = def.ignoreReadonly, 450 includeReadonly = ignoreReadonly || def.includeReadonly; 451 452 // If the read-only inclusion is not available in the definition, try 453 // to get it from the document data. 454 if ( includeReadonly == undefined ) 455 includeReadonly = document.getCustomData( 'cke_includeReadonly' ); 456 457 // Get the DTD definition for the element. Defaults to "span". 458 var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span ); 459 460 // Expand the range. 461 range.enlarge( CKEDITOR.ENLARGE_ELEMENT, 1 ); 462 range.trim(); 463 464 // Get the first node to be processed and the last, which concludes the 465 // processing. 466 var boundaryNodes = range.createBookmark(), 467 firstNode = boundaryNodes.startNode, 468 lastNode = boundaryNodes.endNode; 469 470 var currentNode = firstNode; 471 472 var styleRange; 473 474 if ( !ignoreReadonly ) 475 { 476 // Check if the boundaries are inside non stylable elements. 477 var firstUnstylable = getUnstylableParent( firstNode ), 478 lastUnstylable = getUnstylableParent( lastNode ); 479 480 // If the first element can't be styled, we'll start processing right 481 // after its unstylable root. 482 if ( firstUnstylable ) 483 currentNode = firstUnstylable.getNextSourceNode( true ); 484 485 // If the last element can't be styled, we'll stop processing on its 486 // unstylable root. 487 if ( lastUnstylable ) 488 lastNode = lastUnstylable; 489 } 490 491 // Do nothing if the current node now follows the last node to be processed. 492 if ( currentNode.getPosition( lastNode ) == CKEDITOR.POSITION_FOLLOWING ) 493 currentNode = 0; 494 495 while ( currentNode ) 496 { 497 var applyStyle = false; 498 499 if ( currentNode.equals( lastNode ) ) 500 { 501 currentNode = null; 502 applyStyle = true; 503 } 504 else 505 { 506 var nodeType = currentNode.type; 507 var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null; 508 var nodeIsReadonly = nodeName && ( currentNode.getAttribute( 'contentEditable' ) == 'false' ); 509 var nodeIsNoStyle = nodeName && currentNode.getAttribute( 'data-nostyle' ); 510 511 if ( nodeName && currentNode.data( 'cke-bookmark' ) ) 512 { 513 currentNode = currentNode.getNextSourceNode( true ); 514 continue; 515 } 516 517 // Check if the current node can be a child of the style element. 518 if ( !nodeName || ( dtd[ nodeName ] 519 && !nodeIsNoStyle 520 && ( !nodeIsReadonly || includeReadonly ) 521 && ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) 522 && ( !def.childRule || def.childRule( currentNode ) ) ) ) 523 { 524 var currentParent = currentNode.getParent(); 525 526 // Check if the style element can be a child of the current 527 // node parent or if the element is not defined in the DTD. 528 if ( currentParent 529 && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) 530 && ( !def.parentRule || def.parentRule( currentParent ) ) ) 531 { 532 // This node will be part of our range, so if it has not 533 // been started, place its start right before the node. 534 // In the case of an element node, it will be included 535 // only if it is entirely inside the range. 536 if ( !styleRange && ( !nodeName || !CKEDITOR.dtd.$removeEmpty[ nodeName ] || ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) ) 537 { 538 styleRange = new CKEDITOR.dom.range( document ); 539 styleRange.setStartBefore( currentNode ); 540 } 541 542 // Non element nodes, readonly elements, or empty 543 // elements can be added completely to the range. 544 if ( nodeType == CKEDITOR.NODE_TEXT || nodeIsReadonly || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() ) ) 545 { 546 var includedNode = currentNode; 547 var parentNode; 548 549 // This node is about to be included completelly, but, 550 // if this is the last node in its parent, we must also 551 // check if the parent itself can be added completelly 552 // to the range, otherwise apply the style immediately. 553 while ( ( applyStyle = !includedNode.getNext( notBookmark ) ) 554 && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) 555 && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) 556 && ( !def.childRule || def.childRule( parentNode ) ) ) 557 { 558 includedNode = parentNode; 559 } 560 561 styleRange.setEndAfter( includedNode ); 562 563 } 564 } 565 else 566 applyStyle = true; 567 } 568 else 569 applyStyle = true; 570 571 // Get the next node to be processed. 572 currentNode = currentNode.getNextSourceNode( nodeIsNoStyle || nodeIsReadonly ); 573 } 574 575 // Apply the style if we have something to which apply it. 576 if ( applyStyle && styleRange && !styleRange.collapsed ) 577 { 578 // Build the style element, based on the style object definition. 579 var styleNode = getElement( this, document ), 580 styleHasAttrs = styleNode.hasAttributes(); 581 582 // Get the element that holds the entire range. 583 var parent = styleRange.getCommonAncestor(); 584 585 var removeList = { 586 styles : {}, 587 attrs : {}, 588 // Styles cannot be removed. 589 blockedStyles : {}, 590 // Attrs cannot be removed. 591 blockedAttrs : {} 592 }; 593 594 var attName, styleName, value; 595 596 // Loop through the parents, removing the redundant attributes 597 // from the element to be applied. 598 while ( styleNode && parent ) 599 { 600 if ( parent.getName() == elementName ) 601 { 602 for ( attName in def.attributes ) 603 { 604 if ( removeList.blockedAttrs[ attName ] || !( value = parent.getAttribute( styleName ) ) ) 605 continue; 606 607 if ( styleNode.getAttribute( attName ) == value ) 608 removeList.attrs[ attName ] = 1; 609 else 610 removeList.blockedAttrs[ attName ] = 1; 611 } 612 613 for ( styleName in def.styles ) 614 { 615 if ( removeList.blockedStyles[ styleName ] || !( value = parent.getStyle( styleName ) ) ) 616 continue; 617 618 if ( styleNode.getStyle( styleName ) == value ) 619 removeList.styles[ styleName ] = 1; 620 else 621 removeList.blockedStyles[ styleName ] = 1; 622 } 623 } 624 625 parent = parent.getParent(); 626 } 627 628 for ( attName in removeList.attrs ) 629 styleNode.removeAttribute( attName ); 630 631 for ( styleName in removeList.styles ) 632 styleNode.removeStyle( styleName ); 633 634 if ( styleHasAttrs && !styleNode.hasAttributes() ) 635 styleNode = null; 636 637 if ( styleNode ) 638 { 639 // Move the contents of the range to the style element. 640 styleRange.extractContents().appendTo( styleNode ); 641 642 // Here we do some cleanup, removing all duplicated 643 // elements from the style element. 644 removeFromInsideElement( this, styleNode ); 645 646 // Insert it into the range position (it is collapsed after 647 // extractContents. 648 styleRange.insertNode( styleNode ); 649 650 // Let's merge our new style with its neighbors, if possible. 651 styleNode.mergeSiblings(); 652 653 // As the style system breaks text nodes constantly, let's normalize 654 // things for performance. 655 // With IE, some paragraphs get broken when calling normalize() 656 // repeatedly. Also, for IE, we must normalize body, not documentElement. 657 // IE is also known for having a "crash effect" with normalize(). 658 // We should try to normalize with IE too in some way, somewhere. 659 if ( !CKEDITOR.env.ie ) 660 styleNode.$.normalize(); 661 } 662 // Style already inherit from parents, left just to clear up any internal overrides. (#5931) 663 else 664 { 665 styleNode = new CKEDITOR.dom.element( 'span' ); 666 styleRange.extractContents().appendTo( styleNode ); 667 styleRange.insertNode( styleNode ); 668 removeFromInsideElement( this, styleNode ); 669 styleNode.remove( true ); 670 } 671 672 // Style applied, let's release the range, so it gets 673 // re-initialization in the next loop. 674 styleRange = null; 675 } 676 } 677 678 // Remove the bookmark nodes. 679 range.moveToBookmark( boundaryNodes ); 680 681 // Minimize the result range to exclude empty text nodes. (#5374) 682 range.shrink( CKEDITOR.SHRINK_TEXT ); 683 } 684 685 function removeInlineStyle( range ) 686 { 687 /* 688 * Make sure our range has included all "collpased" parent inline nodes so 689 * that our operation logic can be simpler. 690 */ 691 range.enlarge( CKEDITOR.ENLARGE_ELEMENT, 1 ); 692 693 var bookmark = range.createBookmark(), 694 startNode = bookmark.startNode; 695 696 if ( range.collapsed ) 697 { 698 699 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), 700 // The topmost element in elementspatch which we should jump out of. 701 boundaryElement; 702 703 704 for ( var i = 0, element ; i < startPath.elements.length 705 && ( element = startPath.elements[i] ) ; i++ ) 706 { 707 /* 708 * 1. If it's collaped inside text nodes, try to remove the style from the whole element. 709 * 710 * 2. Otherwise if it's collapsed on element boundaries, moving the selection 711 * outside the styles instead of removing the whole tag, 712 * also make sure other inner styles were well preserverd.(#3309) 713 */ 714 if ( element == startPath.block || element == startPath.blockLimit ) 715 break; 716 717 if ( this.checkElementRemovable( element ) ) 718 { 719 var isStart; 720 721 if ( range.collapsed && ( 722 range.checkBoundaryOfElement( element, CKEDITOR.END ) || 723 ( isStart = range.checkBoundaryOfElement( element, CKEDITOR.START ) ) ) ) 724 { 725 boundaryElement = element; 726 boundaryElement.match = isStart ? 'start' : 'end'; 727 } 728 else 729 { 730 /* 731 * Before removing the style node, there may be a sibling to the style node 732 * that's exactly the same to the one to be removed. To the user, it makes 733 * no difference that they're separate entities in the DOM tree. So, merge 734 * them before removal. 735 */ 736 element.mergeSiblings(); 737 if ( element.getName() == this.element ) 738 removeFromElement( this, element ); 739 else 740 removeOverrides( element, getOverrides( this )[ element.getName() ] ); 741 } 742 } 743 } 744 745 // Re-create the style tree after/before the boundary element, 746 // the replication start from bookmark start node to define the 747 // new range. 748 if ( boundaryElement ) 749 { 750 var clonedElement = startNode; 751 for ( i = 0 ;; i++ ) 752 { 753 var newElement = startPath.elements[ i ]; 754 if ( newElement.equals( boundaryElement ) ) 755 break; 756 // Avoid copying any matched element. 757 else if ( newElement.match ) 758 continue; 759 else 760 newElement = newElement.clone(); 761 newElement.append( clonedElement ); 762 clonedElement = newElement; 763 } 764 clonedElement[ boundaryElement.match == 'start' ? 765 'insertBefore' : 'insertAfter' ]( boundaryElement ); 766 } 767 } 768 else 769 { 770 /* 771 * Now our range isn't collapsed. Lets walk from the start node to the end 772 * node via DFS and remove the styles one-by-one. 773 */ 774 var endNode = bookmark.endNode, 775 me = this; 776 777 /* 778 * Find out the style ancestor that needs to be broken down at startNode 779 * and endNode. 780 */ 781 function breakNodes() 782 { 783 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), 784 endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ), 785 breakStart = null, 786 breakEnd = null; 787 for ( var i = 0 ; i < startPath.elements.length ; i++ ) 788 { 789 var element = startPath.elements[ i ]; 790 791 if ( element == startPath.block || element == startPath.blockLimit ) 792 break; 793 794 if ( me.checkElementRemovable( element ) ) 795 breakStart = element; 796 } 797 for ( i = 0 ; i < endPath.elements.length ; i++ ) 798 { 799 element = endPath.elements[ i ]; 800 801 if ( element == endPath.block || element == endPath.blockLimit ) 802 break; 803 804 if ( me.checkElementRemovable( element ) ) 805 breakEnd = element; 806 } 807 808 if ( breakEnd ) 809 endNode.breakParent( breakEnd ); 810 if ( breakStart ) 811 startNode.breakParent( breakStart ); 812 } 813 breakNodes(); 814 815 // Now, do the DFS walk. 816 var currentNode = startNode; 817 while ( !currentNode.equals( endNode ) ) 818 { 819 /* 820 * Need to get the next node first because removeFromElement() can remove 821 * the current node from DOM tree. 822 */ 823 var nextNode = currentNode.getNextSourceNode(); 824 if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) 825 { 826 // Remove style from element or overriding element. 827 if ( currentNode.getName() == this.element ) 828 removeFromElement( this, currentNode ); 829 else 830 removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] ); 831 832 /* 833 * removeFromElement() may have merged the next node with something before 834 * the startNode via mergeSiblings(). In that case, the nextNode would 835 * contain startNode and we'll have to call breakNodes() again and also 836 * reassign the nextNode to something after startNode. 837 */ 838 if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) ) 839 { 840 breakNodes(); 841 nextNode = startNode.getNext(); 842 } 843 } 844 currentNode = nextNode; 845 } 846 } 847 848 range.moveToBookmark( bookmark ); 849 } 850 851 function applyObjectStyle( range ) 852 { 853 var root = range.getCommonAncestor( true, true ), 854 element = root.getAscendant( this.element, true ); 855 element && !element.isReadOnly() && setupElement( element, this ); 856 } 857 858 function removeObjectStyle( range ) 859 { 860 var root = range.getCommonAncestor( true, true ), 861 element = root.getAscendant( this.element, true ); 862 863 if ( !element ) 864 return; 865 866 var style = this, 867 def = style._.definition, 868 attributes = def.attributes; 869 870 // Remove all defined attributes. 871 if ( attributes ) 872 { 873 for ( var att in attributes ) 874 { 875 element.removeAttribute( att, attributes[ att ] ); 876 } 877 } 878 879 // Assign all defined styles. 880 if ( def.styles ) 881 { 882 for ( var i in def.styles ) 883 { 884 if ( !def.styles.hasOwnProperty( i ) ) 885 continue; 886 887 element.removeStyle( i ); 888 } 889 } 890 } 891 892 function applyBlockStyle( range ) 893 { 894 // Serializible bookmarks is needed here since 895 // elements may be merged. 896 var bookmark = range.createBookmark( true ); 897 898 var iterator = range.createIterator(); 899 iterator.enforceRealBlocks = true; 900 901 // make recognize <br /> tag as a separator in ENTER_BR mode (#5121) 902 if ( this._.enterMode ) 903 iterator.enlargeBr = ( this._.enterMode != CKEDITOR.ENTER_BR ); 904 905 var block; 906 var doc = range.document; 907 var previousPreBlock; 908 909 while ( ( block = iterator.getNextParagraph() ) ) // Only one = 910 { 911 if ( !block.isReadOnly() ) 912 { 913 var newBlock = getElement( this, doc, block ); 914 replaceBlock( block, newBlock ); 915 } 916 } 917 918 range.moveToBookmark( bookmark ); 919 } 920 921 function removeBlockStyle( range ) 922 { 923 // Serializible bookmarks is needed here since 924 // elements may be merged. 925 var bookmark = range.createBookmark( 1 ); 926 927 var iterator = range.createIterator(); 928 iterator.enforceRealBlocks = true; 929 iterator.enlargeBr = this._.enterMode != CKEDITOR.ENTER_BR; 930 931 var block; 932 while ( ( block = iterator.getNextParagraph() ) ) 933 { 934 if ( this.checkElementRemovable( block ) ) 935 { 936 // <pre> get special treatment. 937 if ( block.is( 'pre' ) ) 938 { 939 var newBlock = this._.enterMode == CKEDITOR.ENTER_BR ? 940 null : range.document.createElement( 941 this._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); 942 943 newBlock && block.copyAttributes( newBlock ); 944 replaceBlock( block, newBlock ); 945 } 946 else 947 removeFromElement( this, block, 1 ); 948 } 949 } 950 951 range.moveToBookmark( bookmark ); 952 } 953 954 // Replace the original block with new one, with special treatment 955 // for <pre> blocks to make sure content format is well preserved, and merging/splitting adjacent 956 // when necessary.(#3188) 957 function replaceBlock( block, newBlock ) 958 { 959 // Block is to be removed, create a temp element to 960 // save contents. 961 var removeBlock = !newBlock; 962 if ( removeBlock ) 963 { 964 newBlock = block.getDocument().createElement( 'div' ); 965 block.copyAttributes( newBlock ); 966 } 967 968 var newBlockIsPre = newBlock && newBlock.is( 'pre' ); 969 var blockIsPre = block.is( 'pre' ); 970 971 var isToPre = newBlockIsPre && !blockIsPre; 972 var isFromPre = !newBlockIsPre && blockIsPre; 973 974 if ( isToPre ) 975 newBlock = toPre( block, newBlock ); 976 else if ( isFromPre ) 977 // Split big <pre> into pieces before start to convert. 978 newBlock = fromPres( removeBlock ? 979 [ block.getHtml() ] : splitIntoPres( block ), newBlock ); 980 else 981 block.moveChildren( newBlock ); 982 983 newBlock.replace( block ); 984 985 if ( newBlockIsPre ) 986 { 987 // Merge previous <pre> blocks. 988 mergePre( newBlock ); 989 } 990 else if ( removeBlock ) 991 removeNoAttribsElement( newBlock ); 992 } 993 994 /** 995 * Merge a <pre> block with a previous sibling if available. 996 */ 997 function mergePre( preBlock ) 998 { 999 var previousBlock; 1000 if ( !( ( previousBlock = preBlock.getPrevious( nonWhitespaces ) ) 1001 && previousBlock.is 1002 && previousBlock.is( 'pre') ) ) 1003 return; 1004 1005 // Merge the previous <pre> block contents into the current <pre> 1006 // block. 1007 // 1008 // Another thing to be careful here is that currentBlock might contain 1009 // a '\n' at the beginning, and previousBlock might contain a '\n' 1010 // towards the end. These new lines are not normally displayed but they 1011 // become visible after merging. 1012 var mergedHtml = replace( previousBlock.getHtml(), /\n$/, '' ) + '\n\n' + 1013 replace( preBlock.getHtml(), /^\n/, '' ) ; 1014 1015 // Krugle: IE normalizes innerHTML from <pre>, breaking whitespaces. 1016 if ( CKEDITOR.env.ie ) 1017 preBlock.$.outerHTML = '<pre>' + mergedHtml + '</pre>'; 1018 else 1019 preBlock.setHtml( mergedHtml ); 1020 1021 previousBlock.remove(); 1022 } 1023 1024 /** 1025 * Split into multiple <pre> blocks separated by double line-break. 1026 * @param preBlock 1027 */ 1028 function splitIntoPres( preBlock ) 1029 { 1030 // Exclude the ones at header OR at tail, 1031 // and ignore bookmark content between them. 1032 var duoBrRegex = /(\S\s*)\n(?:\s|(<span[^>]+data-cke-bookmark.*?\/span>))*\n(?!$)/gi, 1033 blockName = preBlock.getName(), 1034 splitedHtml = replace( preBlock.getOuterHtml(), 1035 duoBrRegex, 1036 function( match, charBefore, bookmark ) 1037 { 1038 return charBefore + '</pre>' + bookmark + '<pre>'; 1039 } ); 1040 1041 var pres = []; 1042 splitedHtml.replace( /<pre\b.*?>([\s\S]*?)<\/pre>/gi, function( match, preContent ){ 1043 pres.push( preContent ); 1044 } ); 1045 return pres; 1046 } 1047 1048 // Wrapper function of String::replace without considering of head/tail bookmarks nodes. 1049 function replace( str, regexp, replacement ) 1050 { 1051 var headBookmark = '', 1052 tailBookmark = ''; 1053 1054 str = str.replace( /(^<span[^>]+data-cke-bookmark.*?\/span>)|(<span[^>]+data-cke-bookmark.*?\/span>$)/gi, 1055 function( str, m1, m2 ){ 1056 m1 && ( headBookmark = m1 ); 1057 m2 && ( tailBookmark = m2 ); 1058 return ''; 1059 } ); 1060 return headBookmark + str.replace( regexp, replacement ) + tailBookmark; 1061 } 1062 1063 /** 1064 * Converting a list of <pre> into blocks with format well preserved. 1065 */ 1066 function fromPres( preHtmls, newBlock ) 1067 { 1068 var docFrag; 1069 if ( preHtmls.length > 1 ) 1070 docFrag = new CKEDITOR.dom.documentFragment( newBlock.getDocument() ); 1071 1072 for ( var i = 0 ; i < preHtmls.length ; i++ ) 1073 { 1074 var blockHtml = preHtmls[ i ]; 1075 1076 // 1. Trim the first and last line-breaks immediately after and before <pre>, 1077 // they're not visible. 1078 blockHtml = blockHtml.replace( /(\r\n|\r)/g, '\n' ) ; 1079 blockHtml = replace( blockHtml, /^[ \t]*\n/, '' ) ; 1080 blockHtml = replace( blockHtml, /\n$/, '' ) ; 1081 // 2. Convert spaces or tabs at the beginning or at the end to 1082 blockHtml = replace( blockHtml, /^[ \t]+|[ \t]+$/g, function( match, offset, s ) 1083 { 1084 if ( match.length == 1 ) // one space, preserve it 1085 return ' ' ; 1086 else if ( !offset ) // beginning of block 1087 return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' '; 1088 else // end of block 1089 return ' ' + CKEDITOR.tools.repeat( ' ', match.length - 1 ); 1090 } ) ; 1091 1092 // 3. Convert \n to <BR>. 1093 // 4. Convert contiguous (i.e. non-singular) spaces or tabs to 1094 blockHtml = blockHtml.replace( /\n/g, '<br>' ) ; 1095 blockHtml = blockHtml.replace( /[ \t]{2,}/g, 1096 function ( match ) 1097 { 1098 return CKEDITOR.tools.repeat( ' ', match.length - 1 ) + ' ' ; 1099 } ) ; 1100 1101 if ( docFrag ) 1102 { 1103 var newBlockClone = newBlock.clone(); 1104 newBlockClone.setHtml( blockHtml ); 1105 docFrag.append( newBlockClone ); 1106 } 1107 else 1108 newBlock.setHtml( blockHtml ); 1109 } 1110 1111 return docFrag || newBlock; 1112 } 1113 1114 /** 1115 * Converting from a non-PRE block to a PRE block in formatting operations. 1116 */ 1117 function toPre( block, newBlock ) 1118 { 1119 var bogus = block.getBogus(); 1120 bogus && bogus.remove(); 1121 1122 // First trim the block content. 1123 var preHtml = block.getHtml(); 1124 1125 // 1. Trim head/tail spaces, they're not visible. 1126 preHtml = replace( preHtml, /(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, '' ); 1127 // 2. Delete ANSI whitespaces immediately before and after <BR> because 1128 // they are not visible. 1129 preHtml = preHtml.replace( /[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi, '$1' ); 1130 // 3. Compress other ANSI whitespaces since they're only visible as one 1131 // single space previously. 1132 // 4. Convert to spaces since is no longer needed in <PRE>. 1133 preHtml = preHtml.replace( /([ \t\n\r]+| )/g, ' ' ); 1134 // 5. Convert any <BR /> to \n. This must not be done earlier because 1135 // the \n would then get compressed. 1136 preHtml = preHtml.replace( /<br\b[^>]*>/gi, '\n' ); 1137 1138 // Krugle: IE normalizes innerHTML to <pre>, breaking whitespaces. 1139 if ( CKEDITOR.env.ie ) 1140 { 1141 var temp = block.getDocument().createElement( 'div' ); 1142 temp.append( newBlock ); 1143 newBlock.$.outerHTML = '<pre>' + preHtml + '</pre>'; 1144 newBlock.copyAttributes( temp.getFirst() ); 1145 newBlock = temp.getFirst().remove(); 1146 } 1147 else 1148 newBlock.setHtml( preHtml ); 1149 1150 return newBlock; 1151 } 1152 1153 // Removes a style from an element itself, don't care about its subtree. 1154 function removeFromElement( style, element ) 1155 { 1156 var def = style._.definition, 1157 attributes = def.attributes, 1158 styles = def.styles, 1159 overrides = getOverrides( style )[ element.getName() ], 1160 // If the style is only about the element itself, we have to remove the element. 1161 removeEmpty = CKEDITOR.tools.isEmpty( attributes ) && CKEDITOR.tools.isEmpty( styles ); 1162 1163 // Remove definition attributes/style from the elemnt. 1164 for ( var attName in attributes ) 1165 { 1166 // The 'class' element value must match (#1318). 1167 if ( ( attName == 'class' || style._.definition.fullMatch ) 1168 && element.getAttribute( attName ) != normalizeProperty( attName, attributes[ attName ] ) ) 1169 continue; 1170 removeEmpty = element.hasAttribute( attName ); 1171 element.removeAttribute( attName ); 1172 } 1173 1174 for ( var styleName in styles ) 1175 { 1176 // Full match style insist on having fully equivalence. (#5018) 1177 if ( style._.definition.fullMatch 1178 && element.getStyle( styleName ) != normalizeProperty( styleName, styles[ styleName ], true ) ) 1179 continue; 1180 1181 removeEmpty = removeEmpty || !!element.getStyle( styleName ); 1182 element.removeStyle( styleName ); 1183 } 1184 1185 // Remove overrides, but don't remove the element if it's a block element 1186 removeOverrides( element, overrides, blockElements[ element.getName() ] ) ; 1187 1188 if ( removeEmpty ) 1189 { 1190 !CKEDITOR.dtd.$block[ element.getName() ] || style._.enterMode == CKEDITOR.ENTER_BR && !element.hasAttributes() ? 1191 removeNoAttribsElement( element ) : 1192 element.renameNode( style._.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ); 1193 } 1194 } 1195 1196 // Removes a style from inside an element. 1197 function removeFromInsideElement( style, element ) 1198 { 1199 var def = style._.definition, 1200 attribs = def.attributes, 1201 styles = def.styles, 1202 overrides = getOverrides( style ), 1203 innerElements = element.getElementsByTag( style.element ); 1204 1205 for ( var i = innerElements.count(); --i >= 0 ; ) 1206 removeFromElement( style, innerElements.getItem( i ) ); 1207 1208 // Now remove any other element with different name that is 1209 // defined to be overriden. 1210 for ( var overrideElement in overrides ) 1211 { 1212 if ( overrideElement != style.element ) 1213 { 1214 innerElements = element.getElementsByTag( overrideElement ) ; 1215 for ( i = innerElements.count() - 1 ; i >= 0 ; i-- ) 1216 { 1217 var innerElement = innerElements.getItem( i ); 1218 removeOverrides( innerElement, overrides[ overrideElement ] ) ; 1219 } 1220 } 1221 } 1222 } 1223 1224 /** 1225 * Remove overriding styles/attributes from the specific element. 1226 * Note: Remove the element if no attributes remain. 1227 * @param {Object} element 1228 * @param {Object} overrides 1229 * @param {Boolean} Don't remove the element 1230 */ 1231 function removeOverrides( element, overrides, dontRemove ) 1232 { 1233 var attributes = overrides && overrides.attributes ; 1234 1235 if ( attributes ) 1236 { 1237 for ( var i = 0 ; i < attributes.length ; i++ ) 1238 { 1239 var attName = attributes[i][0], actualAttrValue ; 1240 1241 if ( ( actualAttrValue = element.getAttribute( attName ) ) ) 1242 { 1243 var attValue = attributes[i][1] ; 1244 1245 // Remove the attribute if: 1246 // - The override definition value is null ; 1247 // - The override definition valie is a string that 1248 // matches the attribute value exactly. 1249 // - The override definition value is a regex that 1250 // has matches in the attribute value. 1251 if ( attValue === null || 1252 ( attValue.test && attValue.test( actualAttrValue ) ) || 1253 ( typeof attValue == 'string' && actualAttrValue == attValue ) ) 1254 element.removeAttribute( attName ) ; 1255 } 1256 } 1257 } 1258 1259 if ( !dontRemove ) 1260 removeNoAttribsElement( element ); 1261 } 1262 1263 // If the element has no more attributes, remove it. 1264 function removeNoAttribsElement( element ) 1265 { 1266 // If no more attributes remained in the element, remove it, 1267 // leaving its children. 1268 if ( !element.hasAttributes() ) 1269 { 1270 if ( CKEDITOR.dtd.$block[ element.getName() ] ) 1271 { 1272 var previous = element.getPrevious( nonWhitespaces ), 1273 next = element.getNext( nonWhitespaces ); 1274 1275 if ( previous && ( previous.type == CKEDITOR.NODE_TEXT || !previous.isBlockBoundary( { br : 1 } ) ) ) 1276 element.append( 'br', 1 ); 1277 if ( next && ( next.type == CKEDITOR.NODE_TEXT || !next.isBlockBoundary( { br : 1 } ) ) ) 1278 element.append( 'br' ); 1279 1280 element.remove( true ); 1281 } 1282 else 1283 { 1284 // Removing elements may open points where merging is possible, 1285 // so let's cache the first and last nodes for later checking. 1286 var firstChild = element.getFirst(); 1287 var lastChild = element.getLast(); 1288 1289 element.remove( true ); 1290 1291 if ( firstChild ) 1292 { 1293 // Check the cached nodes for merging. 1294 firstChild.type == CKEDITOR.NODE_ELEMENT && firstChild.mergeSiblings(); 1295 1296 if ( lastChild && !firstChild.equals( lastChild ) 1297 && lastChild.type == CKEDITOR.NODE_ELEMENT ) 1298 lastChild.mergeSiblings(); 1299 } 1300 1301 } 1302 } 1303 } 1304 1305 function getElement( style, targetDocument, element ) 1306 { 1307 var el, 1308 def = style._.definition, 1309 elementName = style.element; 1310 1311 // The "*" element name will always be a span for this function. 1312 if ( elementName == '*' ) 1313 elementName = 'span'; 1314 1315 // Create the element. 1316 el = new CKEDITOR.dom.element( elementName, targetDocument ); 1317 1318 // #6226: attributes should be copied before the new ones are applied 1319 if ( element ) 1320 element.copyAttributes( el ); 1321 1322 el = setupElement( el, style ); 1323 1324 // Avoid ID duplication. 1325 if ( targetDocument.getCustomData( 'doc_processing_style' ) && el.hasAttribute( 'id' ) ) 1326 el.removeAttribute( 'id' ); 1327 else 1328 targetDocument.setCustomData( 'doc_processing_style', 1 ); 1329 1330 return el; 1331 } 1332 1333 function setupElement( el, style ) 1334 { 1335 var def = style._.definition, 1336 attributes = def.attributes, 1337 styles = CKEDITOR.style.getStyleText( def ); 1338 1339 // Assign all defined attributes. 1340 if ( attributes ) 1341 { 1342 for ( var att in attributes ) 1343 { 1344 el.setAttribute( att, attributes[ att ] ); 1345 } 1346 } 1347 1348 // Assign all defined styles. 1349 if( styles ) 1350 el.setAttribute( 'style', styles ); 1351 1352 return el; 1353 } 1354 1355 function replaceVariables( list, variablesValues ) 1356 { 1357 for ( var item in list ) 1358 { 1359 list[ item ] = list[ item ].replace( varRegex, function( match, varName ) 1360 { 1361 return variablesValues[ varName ]; 1362 }); 1363 } 1364 } 1365 1366 // Returns an object that can be used for style matching comparison. 1367 // Attributes names and values are all lowercased, and the styles get 1368 // merged with the style attribute. 1369 function getAttributesForComparison( styleDefinition ) 1370 { 1371 // If we have already computed it, just return it. 1372 var attribs = styleDefinition._AC; 1373 if ( attribs ) 1374 return attribs; 1375 1376 attribs = {}; 1377 1378 var length = 0; 1379 1380 // Loop through all defined attributes. 1381 var styleAttribs = styleDefinition.attributes; 1382 if ( styleAttribs ) 1383 { 1384 for ( var styleAtt in styleAttribs ) 1385 { 1386 length++; 1387 attribs[ styleAtt ] = styleAttribs[ styleAtt ]; 1388 } 1389 } 1390 1391 // Includes the style definitions. 1392 var styleText = CKEDITOR.style.getStyleText( styleDefinition ); 1393 if ( styleText ) 1394 { 1395 if ( !attribs[ 'style' ] ) 1396 length++; 1397 attribs[ 'style' ] = styleText; 1398 } 1399 1400 // Appends the "length" information to the object. 1401 attribs._length = length; 1402 1403 // Return it, saving it to the next request. 1404 return ( styleDefinition._AC = attribs ); 1405 } 1406 1407 /** 1408 * Get the the collection used to compare the elements and attributes, 1409 * defined in this style overrides, with other element. All information in 1410 * it is lowercased. 1411 * @param {CKEDITOR.style} style 1412 */ 1413 function getOverrides( style ) 1414 { 1415 if ( style._.overrides ) 1416 return style._.overrides; 1417 1418 var overrides = ( style._.overrides = {} ), 1419 definition = style._.definition.overrides; 1420 1421 if ( definition ) 1422 { 1423 // The override description can be a string, object or array. 1424 // Internally, well handle arrays only, so transform it if needed. 1425 if ( !CKEDITOR.tools.isArray( definition ) ) 1426 definition = [ definition ]; 1427 1428 // Loop through all override definitions. 1429 for ( var i = 0 ; i < definition.length ; i++ ) 1430 { 1431 var override = definition[i]; 1432 var elementName; 1433 var overrideEl; 1434 var attrs; 1435 1436 // If can be a string with the element name. 1437 if ( typeof override == 'string' ) 1438 elementName = override.toLowerCase(); 1439 // Or an object. 1440 else 1441 { 1442 elementName = override.element ? override.element.toLowerCase() : style.element; 1443 attrs = override.attributes; 1444 } 1445 1446 // We can have more than one override definition for the same 1447 // element name, so we attempt to simply append information to 1448 // it if it already exists. 1449 overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ); 1450 1451 if ( attrs ) 1452 { 1453 // The returning attributes list is an array, because we 1454 // could have different override definitions for the same 1455 // attribute name. 1456 var overrideAttrs = ( overrideEl.attributes = overrideEl.attributes || new Array() ); 1457 for ( var attName in attrs ) 1458 { 1459 // Each item in the attributes array is also an array, 1460 // where [0] is the attribute name and [1] is the 1461 // override value. 1462 overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ); 1463 } 1464 } 1465 } 1466 } 1467 1468 return overrides; 1469 } 1470 1471 // Make the comparison of attribute value easier by standardizing it. 1472 function normalizeProperty( name, value, isStyle ) 1473 { 1474 var temp = new CKEDITOR.dom.element( 'span' ); 1475 temp [ isStyle ? 'setStyle' : 'setAttribute' ]( name, value ); 1476 return temp[ isStyle ? 'getStyle' : 'getAttribute' ]( name ); 1477 } 1478 1479 // Make the comparison of style text easier by standardizing it. 1480 function normalizeCssText( unparsedCssText, nativeNormalize ) 1481 { 1482 var styleText; 1483 if ( nativeNormalize !== false ) 1484 { 1485 // Injects the style in a temporary span object, so the browser parses it, 1486 // retrieving its final format. 1487 var temp = new CKEDITOR.dom.element( 'span' ); 1488 temp.setAttribute( 'style', unparsedCssText ); 1489 styleText = temp.getAttribute( 'style' ) || ''; 1490 } 1491 else 1492 styleText = unparsedCssText; 1493 1494 // Normalize font-family property, ignore quotes and being case insensitive. (#7322) 1495 // http://www.w3.org/TR/css3-fonts/#font-family-the-font-family-property 1496 styleText = styleText.replace( /(font-family:)(.*?)(?=;|$)/, function ( match, prop, val ) 1497 { 1498 var names = val.split( ',' ); 1499 for ( var i = 0; i < names.length; i++ ) 1500 names[ i ] = CKEDITOR.tools.trim( names[ i ].replace( /["']/g, '' ) ); 1501 return prop + names.join( ',' ); 1502 }); 1503 1504 // Shrinking white-spaces around colon and semi-colon (#4147). 1505 // Compensate tail semi-colon. 1506 return styleText.replace( /\s*([;:])\s*/, '$1' ) 1507 .replace( /([^\s;])$/, '$1;') 1508 // Trimming spaces after comma(#4107), 1509 // remove quotations(#6403), 1510 // mostly for differences on "font-family". 1511 .replace( /,\s+/g, ',' ) 1512 .replace( /\"/g,'' ) 1513 .toLowerCase(); 1514 } 1515 1516 // Turn inline style text properties into one hash. 1517 function parseStyleText( styleText ) 1518 { 1519 var retval = {}; 1520 styleText 1521 .replace( /"/g, '"' ) 1522 .replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) 1523 { 1524 retval[ name ] = value; 1525 } ); 1526 return retval; 1527 } 1528 1529 /** 1530 * Compare two bunch of styles, with the speciality that value 'inherit' 1531 * is treated as a wildcard which will match any value. 1532 * @param {Object|String} source 1533 * @param {Object|String} target 1534 */ 1535 function compareCssText( source, target ) 1536 { 1537 typeof source == 'string' && ( source = parseStyleText( source ) ); 1538 typeof target == 'string' && ( target = parseStyleText( target ) ); 1539 for( var name in source ) 1540 { 1541 if ( !( name in target && 1542 ( target[ name ] == source[ name ] 1543 || source[ name ] == 'inherit' 1544 || target[ name ] == 'inherit' ) ) ) 1545 { 1546 return false; 1547 } 1548 } 1549 return true; 1550 } 1551 1552 function applyStyle( document, remove ) 1553 { 1554 var selection = document.getSelection(), 1555 // Bookmark the range so we can re-select it after processing. 1556 bookmarks = selection.createBookmarks( 1 ), 1557 ranges = selection.getRanges(), 1558 func = remove ? this.removeFromRange : this.applyToRange, 1559 range; 1560 1561 var iterator = ranges.createIterator(); 1562 while ( ( range = iterator.getNextRange() ) ) 1563 func.call( this, range ); 1564 1565 if ( bookmarks.length == 1 && bookmarks[ 0 ].collapsed ) 1566 { 1567 selection.selectRanges( ranges ); 1568 document.getById( bookmarks[ 0 ].startNode ).remove(); 1569 } 1570 else 1571 selection.selectBookmarks( bookmarks ); 1572 1573 document.removeCustomData( 'doc_processing_style' ); 1574 } 1575 })(); 1576 1577 CKEDITOR.styleCommand = function( style ) 1578 { 1579 this.style = style; 1580 }; 1581 1582 CKEDITOR.styleCommand.prototype.exec = function( editor ) 1583 { 1584 editor.focus(); 1585 1586 var doc = editor.document; 1587 1588 if ( doc ) 1589 { 1590 if ( this.state == CKEDITOR.TRISTATE_OFF ) 1591 this.style.apply( doc ); 1592 else if ( this.state == CKEDITOR.TRISTATE_ON ) 1593 this.style.remove( doc ); 1594 } 1595 1596 return !!doc; 1597 }; 1598 1599 /** 1600 * Manages styles registration and loading. See also {@link CKEDITOR.config.stylesSet}. 1601 * @namespace 1602 * @augments CKEDITOR.resourceManager 1603 * @constructor 1604 * @since 3.2 1605 * @example 1606 * // The set of styles for the <b>Styles</b> combo 1607 * CKEDITOR.stylesSet.add( 'default', 1608 * [ 1609 * // Block Styles 1610 * { name : 'Blue Title' , element : 'h3', styles : { 'color' : 'Blue' } }, 1611 * { name : 'Red Title' , element : 'h3', styles : { 'color' : 'Red' } }, 1612 * 1613 * // Inline Styles 1614 * { name : 'Marker: Yellow' , element : 'span', styles : { 'background-color' : 'Yellow' } }, 1615 * { name : 'Marker: Green' , element : 'span', styles : { 'background-color' : 'Lime' } }, 1616 * 1617 * // Object Styles 1618 * { 1619 * name : 'Image on Left', 1620 * element : 'img', 1621 * attributes : 1622 * { 1623 * 'style' : 'padding: 5px; margin-right: 5px', 1624 * 'border' : '2', 1625 * 'align' : 'left' 1626 * } 1627 * } 1628 * ]); 1629 */ 1630 CKEDITOR.stylesSet = new CKEDITOR.resourceManager( '', 'stylesSet' ); 1631 1632 // Backward compatibility (#5025). 1633 CKEDITOR.addStylesSet = CKEDITOR.tools.bind( CKEDITOR.stylesSet.add, CKEDITOR.stylesSet ); 1634 CKEDITOR.loadStylesSet = function( name, url, callback ) 1635 { 1636 CKEDITOR.stylesSet.addExternal( name, url, '' ); 1637 CKEDITOR.stylesSet.load( name, callback ); 1638 }; 1639 1640 1641 /** 1642 * Gets the current styleSet for this instance 1643 * @param {Function} callback The function to be called with the styles data. 1644 * @example 1645 * editor.getStylesSet( function( stylesDefinitions ) {} ); 1646 */ 1647 CKEDITOR.editor.prototype.getStylesSet = function( callback ) 1648 { 1649 if ( !this._.stylesDefinitions ) 1650 { 1651 var editor = this, 1652 // Respect the backwards compatible definition entry 1653 configStyleSet = editor.config.stylesCombo_stylesSet || editor.config.stylesSet || 'default'; 1654 1655 // #5352 Allow to define the styles directly in the config object 1656 if ( configStyleSet instanceof Array ) 1657 { 1658 editor._.stylesDefinitions = configStyleSet; 1659 callback( configStyleSet ); 1660 return; 1661 } 1662 1663 var partsStylesSet = configStyleSet.split( ':' ), 1664 styleSetName = partsStylesSet[ 0 ], 1665 externalPath = partsStylesSet[ 1 ], 1666 pluginPath = CKEDITOR.plugins.registered.styles.path; 1667 1668 CKEDITOR.stylesSet.addExternal( styleSetName, 1669 externalPath ? 1670 partsStylesSet.slice( 1 ).join( ':' ) : 1671 pluginPath + 'styles/' + styleSetName + '.js', '' ); 1672 1673 CKEDITOR.stylesSet.load( styleSetName, function( stylesSet ) 1674 { 1675 editor._.stylesDefinitions = stylesSet[ styleSetName ]; 1676 callback( editor._.stylesDefinitions ); 1677 } ) ; 1678 } 1679 else 1680 callback( this._.stylesDefinitions ); 1681 }; 1682 1683 /** 1684 * Indicates that fully selected read-only elements will be included when 1685 * applying the style (for inline styles only). 1686 * @name CKEDITOR.style.includeReadonly 1687 * @type Boolean 1688 * @default false 1689 * @since 3.5 1690 */ 1691 1692 /** 1693 * Disables inline styling on read-only elements. 1694 * @name CKEDITOR.config.disableReadonlyStyling 1695 * @type Boolean 1696 * @default false 1697 * @since 3.5 1698 */ 1699 1700 /** 1701 * The "styles definition set" to use in the editor. They will be used in the 1702 * styles combo and the Style selector of the div container. <br> 1703 * The styles may be defined in the page containing the editor, or can be 1704 * loaded on demand from an external file. In the second case, if this setting 1705 * contains only a name, the styles definition file will be loaded from the 1706 * "styles" folder inside the styles plugin folder. 1707 * Otherwise, this setting has the "name:url" syntax, making it 1708 * possible to set the URL from which loading the styles file.<br> 1709 * Previously this setting was available as config.stylesCombo_stylesSet<br> 1710 * @name CKEDITOR.config.stylesSet 1711 * @type String|Array 1712 * @default 'default' 1713 * @since 3.3 1714 * @example 1715 * // Load from the styles' styles folder (mystyles.js file). 1716 * config.stylesSet = 'mystyles'; 1717 * @example 1718 * // Load from a relative URL. 1719 * config.stylesSet = 'mystyles:/editorstyles/styles.js'; 1720 * @example 1721 * // Load from a full URL. 1722 * config.stylesSet = 'mystyles:http://www.example.com/editorstyles/styles.js'; 1723 * @example 1724 * // Load from a list of definitions. 1725 * config.stylesSet = [ 1726 * { name : 'Strong Emphasis', element : 'strong' }, 1727 * { name : 'Emphasis', element : 'em' }, ... ]; 1728 */ 1729