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 || element.getName() != this.element ) 180 return false; 181 182 var def = this._.definition, 183 attribs; 184 185 // If no attributes are defined in the element. 186 if ( !fullMatch && !element.hasAttributes() ) 187 return true; 188 189 attribs = getAttributesForComparison( def ); 190 191 if ( attribs._length ) 192 { 193 for ( var attName in attribs ) 194 { 195 if ( attName == '_length' ) 196 continue; 197 198 if ( compareAttributeValues( attName, attribs[ attName ], element.getAttribute( attName ) ) ) 199 { 200 if ( !fullMatch ) 201 return true; 202 } 203 else if ( fullMatch ) 204 return false; 205 } 206 } 207 208 return true; 209 } 210 }; 211 212 // Build the cssText based on the styles definition. 213 CKEDITOR.style.getStyleText = function( styleDefinition ) 214 { 215 // If we have already computed it, just return it. 216 var stylesDef = styleDefinition._ST; 217 if ( stylesDef ) 218 return stylesDef; 219 220 stylesDef = styleDefinition.styles; 221 222 // Builds the StyleText. 223 224 var stylesText = ( styleDefinition.attributes && styleDefinition.attributes[ 'style' ] ) || ''; 225 226 if ( stylesText.length ) 227 stylesText = stylesText.replace( semicolonFixRegex, ';' ); 228 229 for ( var style in stylesDef ) 230 stylesText += style + ':' + stylesDef[ style ] + ';'; 231 232 // Browsers make some changes to the style when applying them. So, here 233 // we normalize it to the browser format. 234 if ( stylesText.length ) 235 { 236 stylesText = normalizeCssText( stylesText ); 237 238 if ( stylesText.length ) 239 stylesText = stylesText.replace( semicolonFixRegex, ';' ); 240 } 241 242 // Return it, saving it to the next request. 243 return ( styleDefinition._ST = stylesText ); 244 }; 245 246 function applyInlineStyle( range ) 247 { 248 var document = range.document; 249 250 if ( range.collapsed ) 251 { 252 // Create the element to be inserted in the DOM. 253 var collapsedElement = getElement( this, document ); 254 255 // Insert the empty element into the DOM at the range position. 256 range.insertNode( collapsedElement ); 257 258 // Place the selection right inside the empty element. 259 range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END ); 260 261 return; 262 } 263 264 var elementName = this.element; 265 var def = this._.definition; 266 var isUnknownElement; 267 268 // Get the DTD definition for the element. Defaults to "span". 269 var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span ); 270 271 // Bookmark the range so we can re-select it after processing. 272 var bookmark = range.createBookmark(); 273 274 // Expand the range. 275 range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); 276 range.trim(); 277 278 // Get the first node to be processed and the last, which concludes the 279 // processing. 280 var firstNode = range.startContainer.getChild( range.startOffset ) || range.startContainer.getNextSourceNode(); 281 var lastNode = range.endContainer.getChild( range.endOffset ) || ( range.endOffset ? range.endContainer.getNextSourceNode() : range.endContainer ); 282 283 if ( lastNode.equals( firstNode ) ) 284 { 285 // If the last node is the same as the the first one, we must move 286 // it to the next one, otherwise the first one will not be 287 // processed. 288 lastNode = lastNode.getNextSourceNode( true ); 289 290 // It may happen that there are no more nodes after it (the end of 291 // the document), so we must add something there to make our code 292 // simpler. 293 if ( !lastNode ) 294 { 295 lastNode = document.createText( '' ); 296 lastNode.insertAfter( firstNode ); 297 } 298 } 299 300 var currentNode = firstNode; 301 302 var styleRange; 303 304 // Indicates that that some useful inline content has been found, so 305 // the style should be applied. 306 var hasContents; 307 308 while ( currentNode ) 309 { 310 var applyStyle = false; 311 312 if ( currentNode.equals( lastNode ) ) 313 { 314 currentNode = null; 315 applyStyle = true; 316 } 317 else 318 { 319 var nodeType = currentNode.type; 320 var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null; 321 322 if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) ) 323 { 324 currentNode = currentNode.getNextSourceNode( true ); 325 continue; 326 } 327 328 // Check if the current node can be a child of the style element. 329 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 ) ) ) 330 { 331 var currentParent = currentNode.getParent(); 332 333 // Check if the style element can be a child of the current 334 // node parent or if the element is not defined in the DTD. 335 if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) ) 336 { 337 // This node will be part of our range, so if it has not 338 // been started, place its start right before the node. 339 // In the case of an element node, it will be included 340 // only if it is entirely inside the range. 341 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 ) ) ) 342 { 343 styleRange = new CKEDITOR.dom.range( document ); 344 styleRange.setStartBefore( currentNode ); 345 } 346 347 // Non element nodes, or empty elements can be added 348 // completely to the range. 349 if ( nodeType == CKEDITOR.NODE_TEXT || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() && currentNode.$.offsetWidth ) ) 350 { 351 var includedNode = currentNode; 352 var parentNode; 353 354 // This node is about to be included completelly, but, 355 // if this is the last node in its parent, we must also 356 // check if the parent itself can be added completelly 357 // to the range. 358 while ( !includedNode.$.nextSibling 359 && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) 360 && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) 361 { 362 includedNode = parentNode; 363 } 364 365 styleRange.setEndAfter( includedNode ); 366 367 // If the included node still is the last node in its 368 // parent, it means that the parent can't be included 369 // in this style DTD, so apply the style immediately. 370 if ( !includedNode.$.nextSibling ) 371 applyStyle = true; 372 373 if ( !hasContents ) 374 hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) ); 375 } 376 } 377 else 378 applyStyle = true; 379 } 380 else 381 applyStyle = true; 382 383 // Get the next node to be processed. 384 currentNode = currentNode.getNextSourceNode(); 385 } 386 387 // Apply the style if we have something to which apply it. 388 if ( applyStyle && hasContents && styleRange && !styleRange.collapsed ) 389 { 390 // Build the style element, based on the style object definition. 391 var styleNode = getElement( this, document ); 392 393 // Get the element that holds the entire range. 394 var parent = styleRange.getCommonAncestor(); 395 396 // Loop through the parents, removing the redundant attributes 397 // from the element to be applied. 398 while ( styleNode && parent ) 399 { 400 if ( parent.getName() == elementName ) 401 { 402 for ( var attName in def.attribs ) 403 { 404 if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) ) 405 styleNode.removeAttribute( attName ); 406 } 407 408 for ( var styleName in def.styles ) 409 { 410 if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) ) 411 styleNode.removeStyle( styleName ); 412 } 413 414 if ( !styleNode.hasAttributes() ) 415 { 416 styleNode = null; 417 break; 418 } 419 } 420 421 parent = parent.getParent(); 422 } 423 424 if ( styleNode ) 425 { 426 // Move the contents of the range to the style element. 427 styleRange.extractContents().appendTo( styleNode ); 428 429 // Here we do some cleanup, removing all duplicated 430 // elements from the style element. 431 removeFromInsideElement( this, styleNode ); 432 433 // Insert it into the range position (it is collapsed after 434 // extractContents. 435 styleRange.insertNode( styleNode ); 436 437 // Let's merge our new style with its neighbors, if possible. 438 mergeSiblings( styleNode ); 439 440 // As the style system breaks text nodes constantly, let's normalize 441 // things for performance. 442 // With IE, some paragraphs get broken when calling normalize() 443 // repeatedly. Also, for IE, we must normalize body, not documentElement. 444 // IE is also known for having a "crash effect" with normalize(). 445 // We should try to normalize with IE too in some way, somewhere. 446 if ( !CKEDITOR.env.ie ) 447 styleNode.$.normalize(); 448 } 449 450 // Style applied, let's release the range, so it gets 451 // re-initialization in the next loop. 452 styleRange = null; 453 } 454 } 455 456 // this._FixBookmarkStart( startNode ); 457 458 range.moveToBookmark( bookmark ); 459 } 460 461 function removeInlineStyle( range ) 462 { 463 /* 464 * Make sure our range has included all "collpased" parent inline nodes so 465 * that our operation logic can be simpler. 466 */ 467 range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); 468 469 var bookmark = range.createBookmark( true ), 470 startNode = range.document.getById( bookmark.startNode ); 471 472 if ( range.collapsed ) 473 { 474 /* 475 * If the range is collapsed, try to remove the style from all ancestor 476 * elements, until a block boundary is reached. 477 */ 478 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ); 479 for ( var i = 0, element ; i < startPath.elements.length && ( element = startPath.elements[i] ) ; i++ ) 480 { 481 if ( element == startPath.block || element == startPath.blockLimit ) 482 break; 483 484 if ( this.checkElementRemovable( element ) ) 485 { 486 /* 487 * Before removing the style node, there may be a sibling to the style node 488 * that's exactly the same to the one to be removed. To the user, it makes 489 * no difference that they're separate entities in the DOM tree. So, merge 490 * them before removal. 491 */ 492 mergeSiblings( element ); 493 removeFromElement( this, element ); 494 } 495 } 496 } 497 else 498 { 499 /* 500 * Now our range isn't collapsed. Lets walk from the start node to the end 501 * node via DFS and remove the styles one-by-one. 502 */ 503 var endNode = range.document.getById( bookmark.endNode ), 504 me = this; 505 506 /* 507 * Find out the style ancestor that needs to be broken down at startNode 508 * and endNode. 509 */ 510 function breakNodes() 511 { 512 var startPath = new CKEDITOR.dom.elementPath( startNode.getParent() ), 513 endPath = new CKEDITOR.dom.elementPath( endNode.getParent() ), 514 breakStart = null, 515 breakEnd = null; 516 for ( var i = 0 ; i < startPath.elements.length ; i++ ) 517 { 518 var element = startPath.elements[ i ]; 519 520 if ( element == startPath.block || element == startPath.blockLimit ) 521 break; 522 523 if ( me.checkElementRemovable( element ) ) 524 breakStart = element; 525 } 526 for ( var i = 0 ; i < endPath.elements.length ; i++ ) 527 { 528 var element = endPath.elements[ i ]; 529 530 if ( element == endPath.block || element == endPath.blockLimit ) 531 break; 532 533 if ( me.checkElementRemovable( element ) ) 534 breakEnd = element; 535 } 536 537 if ( breakEnd ) 538 endNode.breakParent( breakEnd ); 539 if ( breakStart ) 540 startNode.breakParent( breakStart ); 541 } 542 breakNodes(); 543 544 // Now, do the DFS walk. 545 var currentNode = startNode.getNext(); 546 while ( !currentNode.equals( endNode ) ) 547 { 548 /* 549 * Need to get the next node first because removeFromElement() can remove 550 * the current node from DOM tree. 551 */ 552 var nextNode = currentNode.getNextSourceNode(); 553 if ( currentNode.type == CKEDITOR.NODE_ELEMENT && this.checkElementRemovable( currentNode ) ) 554 { 555 removeFromElement( this, currentNode ); 556 557 /* 558 * removeFromElement() may have merged the next node with something before 559 * the startNode via mergeSiblings(). In that case, the nextNode would 560 * contain startNode and we'll have to call breakNodes() again and also 561 * reassign the nextNode to something after startNode. 562 */ 563 if ( nextNode.type == CKEDITOR.NODE_ELEMENT && nextNode.contains( startNode ) ) 564 { 565 breakNodes(); 566 nextNode = startNode.getNext(); 567 } 568 } 569 currentNode = nextNode; 570 } 571 } 572 573 range.moveToBookmark( bookmark ); 574 } 575 576 function applyBlockStyle( range ) 577 { 578 // Bookmark the range so we can re-select it after processing. 579 var bookmark = range.createBookmark(); 580 581 var iterator = range.createIterator(); 582 iterator.enforceRealBlocks = true; 583 584 var block; 585 var doc = range.document; 586 var previousPreBlock; 587 588 while( ( block = iterator.getNextParagraph() ) ) // Only one = 589 { 590 // Create the new node right before the current one. 591 var newBlock = getElement( this, doc ); 592 593 // Check if we are changing from/to <pre>. 594 // var newBlockIsPre = newBlock.nodeName.IEquals( 'pre' ); 595 // var blockIsPre = block.nodeName.IEquals( 'pre' ); 596 597 // var toPre = newBlockIsPre && !blockIsPre; 598 // var fromPre = !newBlockIsPre && blockIsPre; 599 600 // Move everything from the current node to the new one. 601 // if ( toPre ) 602 // newBlock = this._ToPre( doc, block, newBlock ); 603 // else if ( fromPre ) 604 // newBlock = this._FromPre( doc, block, newBlock ); 605 // else // Convering from a regular block to another regular block. 606 block.moveChildren( newBlock ); 607 608 // Replace the current block. 609 newBlock.insertBefore( block ); 610 block.remove(); 611 612 // Complete other tasks after inserting the node in the DOM. 613 // if ( newBlockIsPre ) 614 // { 615 // if ( previousPreBlock ) 616 // this._CheckAndMergePre( previousPreBlock, newBlock ) ; // Merge successive <pre> blocks. 617 // previousPreBlock = newBlock; 618 // } 619 // else if ( fromPre ) 620 // this._CheckAndSplitPre( newBlock ) ; // Split <br><br> in successive <pre>s. 621 } 622 623 range.moveToBookmark( bookmark ); 624 } 625 626 // Removes a style from an element itself, don't care about its subtree. 627 function removeFromElement( style, element ) 628 { 629 var def = style._.definition, 630 attributes = def.attributes, 631 styles = def.styles; 632 633 for ( var attName in attributes ) 634 { 635 // The 'class' element value must match (#1318). 636 if ( attName == 'class' && element.getAttribute( attName ) != attributes[ attName ] ) 637 continue; 638 element.removeAttribute( attName ); 639 } 640 641 for ( var styleName in styles ) 642 element.removeStyle( styleName ); 643 644 removeNoAttribsElement( element ); 645 } 646 647 // Removes a style from inside an element. 648 function removeFromInsideElement( style, element ) 649 { 650 var def = style._.definition; 651 var attribs = def.attributes; 652 var styles = def.styles; 653 654 var innerElements = element.getElementsByTag( style.element ); 655 656 for ( var i = innerElements.count() ; --i >= 0 ; ) 657 removeFromElement( style, innerElements.getItem( i ) ); 658 } 659 660 // If the element has no more attributes, remove it. 661 function removeNoAttribsElement( element ) 662 { 663 // If no more attributes remained in the element, remove it, 664 // leaving its children. 665 if ( !element.hasAttributes() ) 666 { 667 // Removing elements may open points where merging is possible, 668 // so let's cache the first and last nodes for later checking. 669 var firstChild = element.getFirst(); 670 var lastChild = element.getLast(); 671 672 element.remove( true ); 673 674 if ( firstChild ) 675 { 676 // Check the cached nodes for merging. 677 mergeSiblings( firstChild ); 678 679 if ( lastChild && !firstChild.equals( lastChild ) ) 680 mergeSiblings( lastChild ); 681 } 682 } 683 } 684 685 function mergeSiblings( element ) 686 { 687 if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) 688 return; 689 690 mergeElements( element, element.getNext(), true ); 691 mergeElements( element, element.getPrevious() ); 692 } 693 694 function mergeElements( element, sibling, isNext ) 695 { 696 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) 697 { 698 var hasBookmark = sibling.getAttribute( '_fck_bookmark' ); 699 700 if ( hasBookmark ) 701 sibling = isNext ? sibling.getNext() : sibling.getPrevious(); 702 703 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && element.isIdentical( sibling ) ) 704 { 705 // Save the last child to be checked too, to merge things like 706 // <b><i></i></b><b><i></i></b> => <b><i></i></b> 707 var innerSibling = isNext ? element.getLast() : element.getFirst(); 708 709 if ( hasBookmark ) 710 ( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext ); 711 712 sibling.moveChildren( element, !isNext ); 713 sibling.remove(); 714 715 // Now check the last inner child (see two comments above). 716 if ( innerSibling ) 717 mergeSiblings( innerSibling ); 718 } 719 } 720 } 721 722 function getElement( style, targetDocument ) 723 { 724 var el; 725 726 var def = style._.definition; 727 728 var elementName = style.element; 729 730 // The "*" element name will always be a span for this function. 731 if ( elementName == '*' ) 732 elementName = 'span'; 733 734 // Create the element. 735 el = new CKEDITOR.dom.element( elementName, targetDocument ); 736 737 return setupElement( el, style ); 738 } 739 740 function setupElement( el, style ) 741 { 742 var def = style._.definition; 743 var attributes = def.attributes; 744 var styles = CKEDITOR.style.getStyleText( def ); 745 746 // Assign all defined attributes. 747 if ( attributes ) 748 { 749 for ( var att in attributes ) 750 { 751 el.setAttribute( att, attributes[ att ] ); 752 } 753 } 754 755 // Assign all defined styles. 756 if ( styles ) 757 el.setAttribute( 'style', styles ); 758 759 return el; 760 } 761 762 var varRegex = /#\((.+?)\)/g; 763 function replaceVariables( list, variablesValues ) 764 { 765 for ( var item in list ) 766 { 767 list[ item ] = list[ item ].replace( varRegex, function( match, varName ) 768 { 769 return variablesValues[ varName ]; 770 }); 771 } 772 } 773 774 var spacesRegex = /\s+/g; 775 776 // Returns an object that can be used for style matching comparison. 777 // Attributes names and values are all lowercased, and the styles get 778 // merged with the style attribute. 779 function getAttributesForComparison( styleDefinition ) 780 { 781 // If we have already computed it, just return it. 782 var attribs = styleDefinition._AC; 783 if ( attribs ) 784 return attribs; 785 786 attribs = {}; 787 788 var length = 0; 789 790 // Loop through all defined attributes. 791 var styleAttribs = styleDefinition.attributes; 792 if ( styleAttribs ) 793 { 794 for ( var styleAtt in styleAttribs ) 795 { 796 length++; 797 attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase(); 798 } 799 } 800 801 // Includes the style definitions. 802 var styleText = CKEDITOR.style.getStyleText( styleDefinition ); 803 if ( styleText.length > 0 ) 804 { 805 if ( !attribs[ 'style' ] ) 806 length++; 807 808 attribs['style'] = styleText.replace( spacesRegex, '' ).toLowerCase(); 809 } 810 811 // Appends the "length" information to the object. 812 attribs._length = length; 813 814 // Return it, saving it to the next request. 815 return ( styleDefinition._AC = attribs ); 816 } 817 818 function normalizeCssText( unparsedCssText ) 819 { 820 // Injects the style in a temporary span object, so the browser parses it, 821 // retrieving its final format. 822 var tempSpan = document.createElement( 'span' ); 823 tempSpan.style.cssText = unparsedCssText; 824 return tempSpan.style.cssText; 825 } 826 827 // valueA is our internal "for comparison" value. 828 // valueB is the value retrieved from the element. 829 function compareAttributeValues( attName, valueA, valueB ) 830 { 831 if ( valueA == valueB || ( !valueA && !valueB ) ) 832 return true; 833 else if ( !valueA || !valueB ) 834 return false; 835 836 valueB = valueB.toLowerCase(); 837 838 if ( attName == 'style' ) 839 { 840 valueB = valueB.replace( spacesRegex, '' ); 841 if ( valueB.charAt( valueB.length - 1 ) != ';' ) 842 valueB += ';'; 843 } 844 845 // Return true if they match or if valueA is null and valueB is an empty string 846 return ( valueA == valueB ); 847 } 848 849 function applyStyle( document, remove ) 850 { 851 // Get all ranges from the selection. 852 var selection = document.getSelection(); 853 var ranges = selection.getRanges(); 854 var func = remove ? this.removeFromRange : this.applyToRange; 855 856 // Apply the style to the ranges. 857 for ( var i = 0 ; i < ranges.length ; i++ ) 858 func.call( this, ranges[ i ] ); 859 860 // Select the ranges again. 861 selection.selectRanges( ranges ); 862 } 863 })(); 864 865 CKEDITOR.styleCommand = function( style ) 866 { 867 this.style = style; 868 }; 869 870 CKEDITOR.styleCommand.prototype.exec = function( editor ) 871 { 872 editor.focus(); 873 874 var doc = editor.document; 875 876 if ( doc ) 877 { 878 if ( this.state == CKEDITOR.TRISTATE_OFF ) 879 this.style.apply( doc ); 880 else if ( this.state == CKEDITOR.TRISTATE_ON ) 881 this.style.remove( doc ); 882 } 883 884 return !!doc; 885 }; 886