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