1 /* 2 Copyright (c) 2003-2009, 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 }); 10 11 /** 12 * Registers a function to be called whenever a style changes its state in the 13 * editing area. The current state is passed to the function. The possible 14 * states are {@link CKEDITOR.TRISTATE_ON} and {@link CKEDITOR.TRISTATE_OFF}. 15 * @param {CKEDITOR.style} The style to be watched. 16 * @param {Function} The function to be called when the style state changes. 17 * @example 18 * // Create a style object for the <b> element. 19 * var style = new CKEDITOR.style( { element : 'b' } ); 20 * var editor = CKEDITOR.instances.editor1; 21 * editor.attachStyleStateChange( style, function( state ) 22 * { 23 * if ( state == CKEDITOR.TRISTATE_ON ) 24 * alert( 'The current state for the B element is ON' ); 25 * else 26 * alert( 'The current state for the B element is OFF' ); 27 * }); 28 */ 29 CKEDITOR.editor.prototype.attachStyleStateChange = function( style, callback ) 30 { 31 // Try to get the list of attached callbacks. 32 var styleStateChangeCallbacks = this._.styleStateChangeCallbacks; 33 34 // If it doesn't exist, it means this is the first call. So, let's create 35 // all the structure to manage the style checks and the callback calls. 36 if ( !styleStateChangeCallbacks ) 37 { 38 // Create the callbacks array. 39 styleStateChangeCallbacks = this._.styleStateChangeCallbacks = []; 40 41 // Attach to the selectionChange event, so we can check the styles at 42 // that point. 43 this.on( 'selectionChange', function( ev ) 44 { 45 // Loop throw all registered callbacks. 46 for ( var i = 0 ; i < styleStateChangeCallbacks.length ; i++ ) 47 { 48 var callback = styleStateChangeCallbacks[ i ]; 49 50 // Check the current state for the style defined for that 51 // callback. 52 var currentState = callback.style.checkActive( ev.data.path ) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF; 53 54 // If the state changed since the last check. 55 if ( callback.state !== currentState ) 56 { 57 // Call the callback function, passing the current 58 // state to it. 59 callback.fn.call( this, currentState ); 60 61 // Save the current state, so it can be compared next 62 // time. 63 callback.state !== currentState; 64 } 65 } 66 }); 67 } 68 69 // Save the callback info, so it can be checked on the next occurence of 70 // selectionChange. 71 styleStateChangeCallbacks.push( { style : style, fn : callback } ); 72 }; 73 74 CKEDITOR.STYLE_BLOCK = 1; 75 CKEDITOR.STYLE_INLINE = 2; 76 CKEDITOR.STYLE_OBJECT = 3; 77 78 (function() 79 { 80 var blockElements = { address:1,div:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,p:1,pre:1 }; 81 var objectElements = { a:1,embed:1,hr:1,img:1,li:1,object:1,ol:1,table:1,td:1,tr:1,ul:1 }; 82 83 var semicolonFixRegex = /\s*(?:;\s*|$)/; 84 85 CKEDITOR.style = function( styleDefinition, variablesValues ) 86 { 87 if ( variablesValues ) 88 { 89 styleDefinition = CKEDITOR.tools.clone( styleDefinition ); 90 91 replaceVariables( styleDefinition.attributes, variablesValues ); 92 replaceVariables( styleDefinition.styles, variablesValues ); 93 } 94 95 var element = this.element = ( styleDefinition.element || '*' ).toLowerCase(); 96 97 this.type = 98 ( element == '#' || blockElements[ element ] ) ? 99 CKEDITOR.STYLE_BLOCK 100 : objectElements[ element ] ? 101 CKEDITOR.STYLE_OBJECT 102 : 103 CKEDITOR.STYLE_INLINE; 104 105 this._ = 106 { 107 definition : styleDefinition 108 }; 109 }; 110 111 CKEDITOR.style.prototype = 112 { 113 apply : function( document ) 114 { 115 applyStyle.call( this, document, false ); 116 }, 117 118 remove : function( document ) 119 { 120 applyStyle.call( this, document, true ); 121 }, 122 123 applyToRange : function( range ) 124 { 125 return ( this.applyToRange = 126 this.type == CKEDITOR.STYLE_INLINE ? 127 applyInlineStyle 128 : this.type == CKEDITOR.STYLE_BLOCK ? 129 applyBlockStyle 130 : null ).call( this, range ); 131 }, 132 133 removeFromRange : function( range ) 134 { 135 return ( this.removeFromRange = 136 this.type == CKEDITOR.STYLE_INLINE ? 137 removeInlineStyle 138 : null ).call( this, range ); 139 }, 140 141 applyToObject : function( element ) 142 { 143 setupElement( element, this ); 144 }, 145 146 /** 147 * Get the style state inside an element path. Returns "true" if the 148 * element is active in the path. 149 */ 150 checkActive : function( elementPath ) 151 { 152 switch ( this.type ) 153 { 154 case CKEDITOR.STYLE_BLOCK : 155 return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true ); 156 157 case CKEDITOR.STYLE_INLINE : 158 159 var elements = elementPath.elements; 160 161 for ( var i = 0, element ; i < elements.length ; i++ ) 162 { 163 element = elements[i]; 164 165 if ( element == elementPath.block || element == elementPath.blockLimit ) 166 continue; 167 168 if ( this.checkElementRemovable( element, true ) ) 169 return true; 170 } 171 } 172 return false; 173 }, 174 175 // Checks if an element, or any of its attributes, is removable by the 176 // current style definition. 177 checkElementRemovable : function( element, fullMatch ) 178 { 179 if ( !element ) 180 return false; 181 182 var def = this._.definition, 183 attribs; 184 185 // If the element name is the same as the style name. 186 if ( element.getName() == this.element ) 187 { 188 // If no attributes are defined in the element. 189 if ( !fullMatch && !element.hasAttributes() ) 190 return true; 191 192 attribs = getAttributesForComparison( def ); 193 194 if ( attribs._length ) 195 { 196 for ( var attName in attribs ) 197 { 198 if ( attName == '_length' ) 199 continue; 200 if ( attribs[attName] == element.getAttribute( attName ) ) 201 { 202 if ( !fullMatch ) 203 return true; 204 } 205 else if ( fullMatch ) 206 return false; 207 } 208 if( fullMatch ) 209 return true; 210 } 211 else 212 return true; 213 } 214 215 // Check if the element can be somehow overriden. 216 var override = getOverrides( this )[ element.getName() ] ; 217 if ( override ) 218 { 219 // If no attributes have been defined, remove the element. 220 if ( !( attribs = override.attributes ) ) 221 return true; 222 223 for ( var i = 0 ; i < attribs.length ; i++ ) 224 { 225 attName = attribs[i][0]; 226 var actualAttrValue = element.getAttribute( attName ); 227 if ( actualAttrValue ) 228 { 229 var attValue = attribs[i][1]; 230 231 // Remove the attribute if: 232 // - The override definition value is null; 233 // - The override definition value is a string that 234 // matches the attribute value exactly. 235 // - The override definition value is a regex that 236 // has matches in the attribute value. 237 if ( attValue === null || 238 ( typeof attValue == 'string' && actualAttrValue == attValue ) || 239 attValue.test( actualAttrValue ) ) 240 return true; 241 } 242 } 243 } 244 return false; 245 } 246 }; 247 248 // Build the cssText based on the styles definition. 249 CKEDITOR.style.getStyleText = function( styleDefinition ) 250 { 251 // If we have already computed it, just return it. 252 var stylesDef = styleDefinition._ST; 253 if ( stylesDef ) 254 return stylesDef; 255 256 stylesDef = styleDefinition.styles; 257 258 // Builds the StyleText. 259 260 var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || ''; 261 262 if ( stylesText.length ) 263 stylesText = stylesText.replace( semicolonFixRegex, ';' ); 264 265 for ( var style in stylesDef ) 266 stylesText += style + ':' + stylesDef[ style ] + ';'; 267 268 // Browsers make some changes to the style when applying them. So, here 269 // we normalize it to the browser format. 270 if ( stylesText.length ) 271 stylesText = normalizeCssText( stylesText ); 272 273 // Return it, saving it to the next request. 274 return ( styleDefinition._ST = stylesText ); 275 }; 276 277 function applyInlineStyle( range ) 278 { 279 var document = range.document; 280 281 if ( range.collapsed ) 282 { 283 // Create the element to be inserted in the DOM. 284 var collapsedElement = getElement( this, document ); 285 286 // Insert the empty element into the DOM at the range position. 287 range.insertNode( collapsedElement ); 288 289 // Place the selection right inside the empty element. 290 range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END ); 291 292 return; 293 } 294 295 var elementName = this.element; 296 var def = this._.definition; 297 var isUnknownElement; 298 299 // Get the DTD definition for the element. Defaults to "span". 300 var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span ); 301 302 // Bookmark the range so we can re-select it after processing. 303 var bookmark = range.createBookmark(); 304 305 // Expand the range. 306 range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); 307 range.trim(); 308 309 // Get the first node to be processed and the last, which concludes the 310 // processing. 311 var boundaryNodes = range.getBoundaryNodes(); 312 var firstNode = boundaryNodes.startNode; 313 var lastNode = boundaryNodes.endNode.getNextSourceNode( true ); 314 315 // Probably the document end is reached, we need a marker node. 316 if ( !lastNode ) 317 { 318 lastNode = document.createText( '' ); 319 lastNode.insertAfter( range.endContainer ); 320 } 321 // The detection algorithm below skips the contents inside bookmark nodes, so 322 // we'll need to make sure lastNode isn't the inside a bookmark node. 323 var lastParent = lastNode.getParent(); 324 if ( lastParent && lastParent.getAttribute( '_fck_bookmark' ) ) 325 lastNode = lastParent; 326 327 if ( lastNode.equals( firstNode ) ) 328 { 329 // If the last node is the same as the the first one, we must move 330 // it to the next one, otherwise the first one will not be 331 // processed. 332 lastNode = lastNode.getNextSourceNode( true ); 333 334 // It may happen that there are no more nodes after it (the end of 335 // the document), so we must add something there to make our code 336 // simpler. 337 if ( !lastNode ) 338 { 339 lastNode = document.createText( '' ); 340 lastNode.insertAfter( firstNode ); 341 } 342 } 343 344 var currentNode = firstNode; 345 346 var styleRange; 347 348 // Indicates that that some useful inline content has been found, so 349 // the style should be applied. 350 var hasContents; 351 352 while ( currentNode ) 353 { 354 var applyStyle = false; 355 356 if ( currentNode.equals( lastNode ) ) 357 { 358 currentNode = null; 359 applyStyle = true; 360 } 361 else 362 { 363 var nodeType = currentNode.type; 364 var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null; 365 366 if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) ) 367 { 368 currentNode = currentNode.getNextSourceNode( true ); 369 continue; 370 } 371 372 // Check if the current node can be a child of the style element. 373 if ( !nodeName || ( dtd[ nodeName ] && ( currentNode.getPosition( lastNode ) | CKEDITOR.POSITION_PRECEDING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_PRECEDING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) ) 374 { 375 var currentParent = currentNode.getParent(); 376 377 // Check if the style element can be a child of the current 378 // node parent or if the element is not defined in the DTD. 379 if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) ) 380 { 381 // This node will be part of our range, so if it has not 382 // been started, place its start right before the node. 383 // In the case of an element node, it will be included 384 // only if it is entirely inside the range. 385 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 ) ) ) 386 { 387 styleRange = new CKEDITOR.dom.range( document ); 388 styleRange.setStartBefore( currentNode ); 389 } 390 391 // Non element nodes, or empty elements can be added 392 // completely to the range. 393 if ( nodeType == CKEDITOR.NODE_TEXT || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() ) ) 394 { 395 var includedNode = currentNode; 396 var parentNode; 397 398 // This node is about to be included completelly, but, 399 // if this is the last node in its parent, we must also 400 // check if the parent itself can be added completelly 401 // to the range. 402 while ( !includedNode.$.nextSibling 403 && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) 404 && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) 405 { 406 includedNode = parentNode; 407 } 408 409 styleRange.setEndAfter( includedNode ); 410 411 // If the included node still is the last node in its 412 // parent, it means that the parent can't be included 413 // in this style DTD, so apply the style immediately. 414 if ( !includedNode.$.nextSibling ) 415 applyStyle = true; 416 417 if ( !hasContents ) 418 hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) ); 419 } 420 } 421 else 422 applyStyle = true; 423 } 424 else 425 applyStyle = true; 426 427 // Get the next node to be processed. 428 currentNode = currentNode.getNextSourceNode(); 429 } 430 431 // Apply the style if we have something to which apply it. 432 if ( applyStyle && hasContents && styleRange && !styleRange.collapsed ) 433 { 434 // Build the style element, based on the style object definition. 435 var styleNode = getElement( this, document ); 436 437 // Get the element that holds the entire range. 438 var parent = styleRange.getCommonAncestor(); 439 440 // Loop through the parents, removing the redundant attributes 441 // from the element to be applied. 442 while ( styleNode && parent ) 443 { 444 if ( parent.getName() == elementName ) 445 { 446 for ( var attName in def.attribs ) 447 { 448 if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) ) 449 styleNode.removeAttribute( attName ); 450 } 451 452 for ( var styleName in def.styles ) 453 { 454 if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) ) 455 styleNode.removeStyle( styleName ); 456 } 457 458 if ( !styleNode.hasAttributes() ) 459 { 460 styleNode = null; 461 break; 462 } 463 } 464 465 parent = parent.getParent(); 466 } 467 468 if ( styleNode ) 469 { 470 // Move the contents of the range to the style element. 471 styleRange.extractContents().appendTo( styleNode ); 472 473 // Here we do some cleanup, removing all duplicated 474 // elements from the style element. 475 removeFromInsideElement( this, styleNode ); 476 477 // Insert it into the range position (it is collapsed after 478 // extractContents. 479 styleRange.insertNode( styleNode ); 480 481 // Let's merge our new style with its neighbors, if possible. 482 mergeSiblings( styleNode ); 483 484 // As the style system breaks text nodes constantly, let's normalize 485 // things for performance. 486 // With IE, some paragraphs get broken when calling normalize() 487 // repeatedly. Also, for IE, we must normalize body, not documentElement. 488 // IE is also known for having a "crash effect" with normalize(). 489 // We should try to normalize with IE too in some way, somewhere. 490 if ( !CKEDITOR.env.ie ) 491 styleNode.$.normalize(); 492 } 493 494 // Style applied, let's release the range, so it gets 495 // re-initialization in the next loop. 496 styleRange = null; 497 } 498 } 499 500 // this._FixBookmarkStart( startNode ); 501 502 range.moveToBookmark( bookmark ); 503 } 504 505 function removeInlineStyle( range ) 506 { 507 /* 508 * Make sure our range has included all "collpased" parent inline nodes so 509 * that our operation logic can be simpler. 510 */ 511 range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); 512 513 var bookmark = range.createBookmark(), 514 startNode = bookmark.startNode; 515 516 if ( range.collapsed ) 517 { 518 519 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), 520 // The topmost element in elementspatch which we should jump out of. 521 boundaryElement; 522 523 524 for ( var i = 0, element ; i < startPath.elements.length 525 && ( element = startPath.elements[i] ) ; i++ ) 526 { 527 /* 528 * 1. If it's collaped inside text nodes, try to remove the style from the whole element. 529 * 530 * 2. Otherwise if it's collapsed on element boundaries, moving the selection 531 * outside the styles instead of removing the whole tag, 532 * also make sure other inner styles were well preserverd.(#3309) 533 */ 534 if ( element == startPath.block || element == startPath.blockLimit ) 535 break; 536 537 if ( this.checkElementRemovable( element ) ) 538 { 539 var endOfElement = range.checkBoundaryOfElement( element, CKEDITOR.END ), 540 startOfElement = !endOfElement && range.checkBoundaryOfElement( element, CKEDITOR.START ); 541 if ( startOfElement || endOfElement ) 542 { 543 boundaryElement = element; 544 boundaryElement.match = startOfElement ? 'start' : 'end'; 545 } 546 else 547 { 548 /* 549 * Before removing the style node, there may be a sibling to the style node 550 * that's exactly the same to the one to be removed. To the user, it makes 551 * no difference that they're separate entities in the DOM tree. So, merge 552 * them before removal. 553 */ 554 mergeSiblings( element ); 555 removeFromElement( this, element ); 556 557 } 558 } 559 } 560 561 // Re-create the style tree after/before the boundary element, 562 // the replication start from bookmark start node to define the 563 // new range. 564 if ( boundaryElement ) 565 { 566 var clonedElement = startNode; 567 for ( i = 0 ;; i++ ) 568 { 569 var newElement = startPath.elements[ i ]; 570 if ( newElement.equals( boundaryElement ) ) 571 break; 572 // Avoid copying any matched element. 573 else if( newElement.match ) 574 continue; 575 else 576 newElement = newElement.clone(); 577 newElement.append( clonedElement ); 578 clonedElement = newElement; 579 } 580 clonedElement[ boundaryElement.match == 'start' ? 581 'insertBefore' : 'insertAfter' ]( boundaryElement ); 582 } 583 } 584 else 585 { 586 /* 587 * Now our range isn't collapsed. Lets walk from the start node to the end 588 * node via DFS and remove the styles one-by-one. 589 */ 590 var endNode = bookmark.endNode, 591 me = this; 592 593 /* 594 * Find out the style ancestor that needs to be broken down at startNode 595 * and endNode. 596 */ 597 function breakNodes() 598 { 599 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), 600 endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ), 601 breakStart = null, 602 breakEnd = null; 603 for ( var i = 0 ; i < startPath.elements.length ; i++ ) 604 { 605 var element = startPath.elements[ i ]; 606 607 if ( element == startPath.block || element == startPath.blockLimit ) 608 break; 609 610 if ( me.checkElementRemovable( element ) ) 611 breakStart = element; 612 } 613 for ( i = 0 ; i < endPath.elements.length ; i++ ) 614 { 615 element = endPath.elements[ i ]; 616 617 if ( element == endPath.block || element == endPath.blockLimit ) 618 break; 619 620 if ( me.checkElementRemovable( element ) ) 621 breakEnd = element; 622 } 623 624 if ( breakEnd ) 625 endNode.breakParent( breakEnd ); 626 if ( breakStart ) 627 startNode.breakParent( breakStart ); 628 } 629 breakNodes(); 630 631 // Now, do the DFS walk. 632 var currentNode = startNode.getNext(); 633 while ( !currentNode.equals( endNode ) ) 634 { 635 /* 636 * Need to get the next node first because removeFromElement() can remove 637 * the current node from DOM tree. 638 */ 639 var nextNode = currentNode.getNextSourceNode(); 640 if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) 641 { 642 // Remove style from element or overriding element. 643 if( currentNode.getName() == this.element ) 644 removeFromElement( this, currentNode ); 645 else 646 removeOverrides( currentNode, getOverrides( this )[ currentNode.getName() ] ); 647 648 /* 649 * removeFromElement() may have merged the next node with something before 650 * the startNode via mergeSiblings(). In that case, the nextNode would 651 * contain startNode and we'll have to call breakNodes() again and also 652 * reassign the nextNode to something after startNode. 653 */ 654 if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) ) 655 { 656 breakNodes(); 657 nextNode = startNode.getNext(); 658 } 659 } 660 currentNode = nextNode; 661 } 662 } 663 664 range.moveToBookmark( bookmark ); 665 } 666 667 function applyBlockStyle( range ) 668 { 669 // Bookmark the range so we can re-select it after processing. 670 var bookmark = range.createBookmark(); 671 672 var iterator = range.createIterator(); 673 iterator.enforceRealBlocks = true; 674 675 var block; 676 var doc = range.document; 677 var previousPreBlock; 678 679 while( ( block = iterator.getNextParagraph() ) ) // Only one = 680 { 681 // Create the new node right before the current one. 682 var newBlock = getElement( this, doc ); 683 684 // Check if we are changing from/to <pre>. 685 // var newBlockIsPre = newBlock.nodeName.IEquals( 'pre' ); 686 // var blockIsPre = block.nodeName.IEquals( 'pre' ); 687 688 // var toPre = newBlockIsPre && !blockIsPre; 689 // var fromPre = !newBlockIsPre && blockIsPre; 690 691 // Move everything from the current node to the new one. 692 // if ( toPre ) 693 // newBlock = this._ToPre( doc, block, newBlock ); 694 // else if ( fromPre ) 695 // newBlock = this._FromPre( doc, block, newBlock ); 696 // else // Convering from a regular block to another regular block. 697 block.moveChildren( newBlock ); 698 699 // Replace the current block. 700 newBlock.insertBefore( block ); 701 block.remove(); 702 703 // Complete other tasks after inserting the node in the DOM. 704 // if ( newBlockIsPre ) 705 // { 706 // if ( previousPreBlock ) 707 // this._CheckAndMergePre( previousPreBlock, newBlock ) ; // Merge successive <pre> blocks. 708 // previousPreBlock = newBlock; 709 // } 710 // else if ( fromPre ) 711 // this._CheckAndSplitPre( newBlock ) ; // Split <br><br> in successive <pre>s. 712 } 713 714 range.moveToBookmark( bookmark ); 715 } 716 717 // Removes a style from an element itself, don't care about its subtree. 718 function removeFromElement( style, element ) 719 { 720 var def = style._.definition, 721 attributes = def.attributes, 722 styles = def.styles, 723 overrides = getOverrides( style ); 724 725 function removeAttrs() 726 { 727 for ( var attName in attributes ) 728 { 729 // The 'class' element value must match (#1318). 730 if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] ) 731 continue; 732 element.removeAttribute( attName ); 733 } 734 } 735 736 // Remove definition attributes/style from the elemnt. 737 removeAttrs(); 738 for ( var styleName in styles ) 739 element.removeStyle( styleName ); 740 741 // Now remove override styles on the element. 742 attributes = overrides[ element.getName() ]; 743 if( attributes ) 744 removeAttrs(); 745 removeNoAttribsElement( element ); 746 } 747 748 // Removes a style from inside an element. 749 function removeFromInsideElement( style, element ) 750 { 751 var def = style._.definition, 752 attribs = def.attributes, 753 styles = def.styles, 754 overrides = getOverrides( style ); 755 756 var innerElements = element.getElementsByTag( style.element ); 757 758 for ( var i = innerElements.count(); --i >= 0 ; ) 759 removeFromElement( style, innerElements.getItem( i ) ); 760 761 // Now remove any other element with different name that is 762 // defined to be overriden. 763 for ( var overrideElement in overrides ) 764 { 765 if ( overrideElement != style.element ) 766 { 767 innerElements = element.getElementsByTag( overrideElement ) ; 768 for ( i = innerElements.count() - 1 ; i >= 0 ; i-- ) 769 { 770 var innerElement = innerElements.getItem( i ); 771 removeOverrides( innerElement, overrides[ overrideElement ] ) ; 772 } 773 } 774 } 775 776 } 777 778 /** 779 * Remove overriding styles/attributes from the specific element. 780 * Note: Remove the element if no attributes remain. 781 * @param {Object} element 782 * @param {Object} overrides 783 */ 784 function removeOverrides( element, overrides ) 785 { 786 var attributes = overrides && overrides.attributes ; 787 788 if ( attributes ) 789 { 790 for ( var i = 0 ; i < attributes.length ; i++ ) 791 { 792 var attName = attributes[i][0], actualAttrValue ; 793 794 if ( ( actualAttrValue = element.getAttribute( attName ) ) ) 795 { 796 var attValue = attributes[i][1] ; 797 798 // Remove the attribute if: 799 // - The override definition value is null ; 800 // - The override definition valie is a string that 801 // matches the attribute value exactly. 802 // - The override definition value is a regex that 803 // has matches in the attribute value. 804 if ( attValue === null || 805 ( attValue.test && attValue.test( actualAttrValue ) ) || 806 ( typeof attValue == 'string' && actualAttrValue == attValue ) ) 807 element.removeAttribute( attName ) ; 808 } 809 } 810 } 811 812 removeNoAttribsElement( element ); 813 } 814 815 // If the element has no more attributes, remove it. 816 function removeNoAttribsElement( element ) 817 { 818 // If no more attributes remained in the element, remove it, 819 // leaving its children. 820 if ( !element.hasAttributes() ) 821 { 822 // Removing elements may open points where merging is possible, 823 // so let's cache the first and last nodes for later checking. 824 var firstChild = element.getFirst(); 825 var lastChild = element.getLast(); 826 827 element.remove( true ); 828 829 if ( firstChild ) 830 { 831 // Check the cached nodes for merging. 832 mergeSiblings( firstChild ); 833 834 if ( lastChild && !firstChild.equals( lastChild ) ) 835 mergeSiblings( lastChild ); 836 } 837 } 838 } 839 840 function mergeSiblings( element ) 841 { 842 if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) 843 return; 844 845 mergeElements( element, element.getNext(), true ); 846 mergeElements( element, element.getPrevious() ); 847 } 848 849 function mergeElements( element, sibling, isNext ) 850 { 851 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) 852 { 853 var hasBookmark = sibling.getAttribute( '_fck_bookmark' ); 854 855 if ( hasBookmark ) 856 sibling = isNext ? sibling.getNext() : sibling.getPrevious(); 857 858 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && element.isIdentical( sibling ) ) 859 { 860 // Save the last child to be checked too, to merge things like 861 // <b><i></i></b><b><i></i></b> => <b><i></i></b> 862 var innerSibling = isNext ? element.getLast() : element.getFirst(); 863 864 if ( hasBookmark ) 865 ( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext ); 866 867 sibling.moveChildren( element, !isNext ); 868 sibling.remove(); 869 870 // Now check the last inner child (see two comments above). 871 if ( innerSibling ) 872 mergeSiblings( innerSibling ); 873 } 874 } 875 } 876 877 function getElement( style, targetDocument ) 878 { 879 var el; 880 881 var def = style._.definition; 882 883 var elementName = style.element; 884 885 // The "*" element name will always be a span for this function. 886 if ( elementName == '*' ) 887 elementName = 'span'; 888 889 // Create the element. 890 el = new CKEDITOR.dom.element( elementName, targetDocument ); 891 892 return setupElement( el, style ); 893 } 894 895 function setupElement( el, style ) 896 { 897 var def = style._.definition; 898 var attributes = def.attributes; 899 var styles = CKEDITOR.style.getStyleText( def ); 900 901 // Assign all defined attributes. 902 if ( attributes ) 903 { 904 for ( var att in attributes ) 905 { 906 el.setAttribute( att, attributes[ att ] ); 907 } 908 } 909 910 // Assign all defined styles. 911 if ( styles ) 912 el.setAttribute( 'style', styles ); 913 914 return el; 915 } 916 917 var varRegex = /#\((.+?)\)/g; 918 function replaceVariables( list, variablesValues ) 919 { 920 for ( var item in list ) 921 { 922 list[ item ] = list[ item ].replace( varRegex, function( match, varName ) 923 { 924 return variablesValues[ varName ]; 925 }); 926 } 927 } 928 929 930 // Returns an object that can be used for style matching comparison. 931 // Attributes names and values are all lowercased, and the styles get 932 // merged with the style attribute. 933 function getAttributesForComparison( styleDefinition ) 934 { 935 // If we have already computed it, just return it. 936 var attribs = styleDefinition._AC; 937 if ( attribs ) 938 return attribs; 939 940 attribs = {}; 941 942 var length = 0; 943 944 // Loop through all defined attributes. 945 var styleAttribs = styleDefinition.attributes; 946 if ( styleAttribs ) 947 { 948 for ( var styleAtt in styleAttribs ) 949 { 950 length++; 951 attribs[ styleAtt ] = styleAttribs[ styleAtt ]; 952 } 953 } 954 955 // Includes the style definitions. 956 var styleText = CKEDITOR.style.getStyleText( styleDefinition ); 957 if ( styleText ) 958 { 959 if ( !attribs[ 'style' ] ) 960 length++; 961 attribs[ 'style' ] = styleText; 962 } 963 964 // Appends the "length" information to the object. 965 attribs._length = length; 966 967 // Return it, saving it to the next request. 968 return ( styleDefinition._AC = attribs ); 969 } 970 971 /** 972 * Get the the collection used to compare the elements and attributes, 973 * defined in this style overrides, with other element. All information in 974 * it is lowercased. 975 * @param {CKEDITOR.style} style 976 */ 977 function getOverrides( style ) 978 { 979 if( style._.overrides ) 980 return style._.overrides; 981 982 var overrides = ( style._.overrides = {} ), 983 definition = style._.definition.overrides; 984 985 if ( definition ) 986 { 987 // The override description can be a string, object or array. 988 // Internally, well handle arrays only, so transform it if needed. 989 if ( !CKEDITOR.tools.isArray( definition ) ) 990 definition = [ definition ]; 991 992 // Loop through all override definitions. 993 for ( var i = 0 ; i < definition.length ; i++ ) 994 { 995 var override = definition[i]; 996 var elementName; 997 var overrideEl; 998 var attrs; 999 1000 // If can be a string with the element name. 1001 if ( typeof override == 'string' ) 1002 elementName = override.toLowerCase(); 1003 // Or an object. 1004 else 1005 { 1006 elementName = override.element ? override.element.toLowerCase() : style.element; 1007 attrs = override.attributes; 1008 } 1009 1010 // We can have more than one override definition for the same 1011 // element name, so we attempt to simply append information to 1012 // it if it already exists. 1013 overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ); 1014 1015 if ( attrs ) 1016 { 1017 // The returning attributes list is an array, because we 1018 // could have different override definitions for the same 1019 // attribute name. 1020 var overrideAttrs = ( overrideEl.attributes = overrideEl.attributes || new Array() ); 1021 for ( var attName in attrs ) 1022 { 1023 // Each item in the attributes array is also an array, 1024 // where [0] is the attribute name and [1] is the 1025 // override value. 1026 overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ); 1027 } 1028 } 1029 } 1030 } 1031 1032 return overrides; 1033 } 1034 1035 function normalizeCssText( unparsedCssText ) 1036 { 1037 // Injects the style in a temporary span object, so the browser parses it, 1038 // retrieving its final format. 1039 var temp = new CKEDITOR.dom.element( 'span' ); 1040 temp.setAttribute( 'style', unparsedCssText ); 1041 return temp.getAttribute( 'style' ); 1042 } 1043 1044 function applyStyle( document, remove ) 1045 { 1046 // Get all ranges from the selection. 1047 var selection = document.getSelection(); 1048 var ranges = selection.getRanges(); 1049 var func = remove ? this.removeFromRange : this.applyToRange; 1050 1051 // Apply the style to the ranges. 1052 for ( var i = 0 ; i < ranges.length ; i++ ) 1053 func.call( this, ranges[ i ] ); 1054 1055 // Select the ranges again. 1056 selection.selectRanges( ranges ); 1057 } 1058 })(); 1059 1060 CKEDITOR.styleCommand = function( style ) 1061 { 1062 this.style = style; 1063 }; 1064 1065 CKEDITOR.styleCommand.prototype.exec = function( editor ) 1066 { 1067 editor.focus(); 1068 1069 var doc = editor.document; 1070 1071 if ( doc ) 1072 { 1073 if ( this.state == CKEDITOR.TRISTATE_OFF ) 1074 this.style.apply( doc ); 1075 else if ( this.state == CKEDITOR.TRISTATE_ON ) 1076 this.style.remove( doc ); 1077 } 1078 1079 return !!doc; 1080 }; 1081