1/* 2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net 3 * Copyright (C) 2003-2007 Frederico Caldeira Knabben 4 * 5 * == BEGIN LICENSE == 6 * 7 * Licensed under the terms of any of the following licenses at your 8 * choice: 9 * 10 * - GNU General Public License Version 2 or later (the "GPL") 11 * http://www.gnu.org/licenses/gpl.html 12 * 13 * - GNU Lesser General Public License Version 2.1 or later (the "LGPL") 14 * http://www.gnu.org/licenses/lgpl.html 15 * 16 * - Mozilla Public License Version 1.1 or later (the "MPL") 17 * http://www.mozilla.org/MPL/MPL-1.1.html 18 * 19 * == END LICENSE == 20 * 21 * FCKStyle Class: contains a style definition, and all methods to work with 22 * the style in a document. 23 */ 24 25/** 26 * @param {Object} styleDesc A "style descriptor" object, containing the raw 27 * style definition in the following format: 28 * '<style name>' : { 29 * Element : '<element name>', 30 * Attributes : { 31 * '<att name>' : '<att value>', 32 * ... 33 * }, 34 * Styles : { 35 * '<style name>' : '<style value>', 36 * ... 37 * }, 38 * Overrides : '<element name>'|{ 39 * Element : '<element name>', 40 * Attributes : { 41 * '<att name>' : '<att value>'|/<att regex>/ 42 * }, 43 * Styles : { 44 * '<style name>' : '<style value>'|/<style regex>/ 45 * }, 46 * } 47 * } 48 */ 49var FCKStyle = function( styleDesc ) 50{ 51 this.Element = ( styleDesc.Element || 'span' ).toLowerCase() ; 52 this._StyleDesc = styleDesc ; 53} 54 55FCKStyle.prototype = 56{ 57 /** 58 * Get the style type, based on its element name: 59 * - FCK_STYLE_BLOCK (0): Block Style 60 * - FCK_STYLE_INLINE (1): Inline Style 61 * - FCK_STYLE_OBJECT (2): Object Style 62 */ 63 GetType : function() 64 { 65 var type = this.GetType_$ ; 66 67 if ( type != undefined ) 68 return type ; 69 70 var elementName = this.Element ; 71 72 if ( elementName == '#' || FCKListsLib.StyleBlockElements[ elementName ] ) 73 type = FCK_STYLE_BLOCK ; 74 else if ( FCKListsLib.StyleObjectElements[ elementName ] ) 75 type = FCK_STYLE_OBJECT ; 76 else 77 type = FCK_STYLE_INLINE ; 78 79 return ( this.GetType_$ = type ) ; 80 }, 81 82 /** 83 * Apply the style to the current selection. 84 */ 85 ApplyToSelection : function( targetWindow ) 86 { 87 // Create a range for the current selection. 88 var range = new FCKDomRange( targetWindow ) ; 89 range.MoveToSelection() ; 90 91 this.ApplyToRange( range, true ) ; 92 }, 93 94 /** 95 * Apply the style to a FCKDomRange. 96 */ 97 ApplyToRange : function( range, selectIt ) 98 { 99 // ApplyToRange is not valid for FCK_STYLE_OBJECT types. 100 // Use ApplyToObject instead. 101 102 switch ( this.GetType() ) 103 { 104 case FCK_STYLE_BLOCK : 105 this.ApplyToRange = this._ApplyBlockStyle ; 106 break ; 107 case FCK_STYLE_INLINE : 108 this.ApplyToRange = this._ApplyInlineStyle ; 109 break ; 110 default : 111 return ; 112 } 113 114 this.ApplyToRange( range, selectIt ) ; 115 }, 116 117 /** 118 * Apply the style to an object. Valid for FCK_STYLE_BLOCK types only. 119 */ 120 ApplyToObject : function( objectElement ) 121 { 122 if ( !objectElement ) 123 return ; 124 125 this.BuildElement( null, objectElement ) ; 126 }, 127 128 /** 129 * Remove the style from the current selection. 130 */ 131 RemoveFromSelection : function( targetWindow ) 132 { 133 // Create a range for the current selection. 134 var range = new FCKDomRange( targetWindow ) ; 135 range.MoveToSelection() ; 136 137 this.RemoveFromRange( range, true ) ; 138 }, 139 140 /** 141 * Remove the style from a FCKDomRange. Block type styles will have no 142 * effect. 143 */ 144 RemoveFromRange : function( range, selectIt ) 145 { 146 var bookmark ; 147 148 // Create the attribute list to be used later for element comparisons. 149 var styleAttribs = this._GetAttribsForComparison() ; 150 var styleOverrides = this._GetOverridesForComparison() ; 151 152 // If collapsed, we are removing all conflicting styles from the range 153 // parent tree. 154 if ( range.CheckIsCollapsed() ) 155 { 156 // Bookmark the range so we can re-select it after processing. 157 var bookmark = range.CreateBookmark( true ) ; 158 159 // Let's start from the bookmark <span> parent. 160 var bookmarkStart = range.GetBookmarkNode( bookmark, true ) ; 161 162 var path = new FCKElementPath( bookmarkStart.parentNode ) ; 163 164 // While looping through the path, we'll be saving references to 165 // parent elements if the range is in one of their boundaries. In 166 // this way, we are able to create a copy of those elements when 167 // removing a style if the range is in a boundary limit (see #1270). 168 var boundaryElements = [] ; 169 170 // Check if the range is in the boundary limits of an element 171 // (related to #1270). 172 var isBoundaryRight = !FCKDomTools.GetNextSibling( bookmarkStart ) ; 173 var isBoundary = isBoundaryRight || !FCKDomTools.GetPreviousSibling( bookmarkStart ) ; 174 175 // This is the last element to be removed in the boundary situation 176 // described at #1270. 177 var lastBoundaryElement ; 178 var boundaryLimitIndex = -1 ; 179 180 for ( var i = 0 ; i < path.Elements.length ; i++ ) 181 { 182 var pathElement = path.Elements[i] ; 183 if ( this.CheckElementRemovable( pathElement ) ) 184 { 185 if ( isBoundary 186 && !FCKDomTools.CheckIsEmptyElement( pathElement, 187 function( el ) 188 { 189 return ( el != bookmarkStart ) ; 190 } ) 191 ) 192 { 193 lastBoundaryElement = pathElement ; 194 195 // We'll be continuously including elements in the 196 // boundaryElements array, but only those added before 197 // setting lastBoundaryElement must be used later, so 198 // let's mark the current index here. 199 boundaryLimitIndex = boundaryElements.length - 1 ; 200 } 201 else 202 { 203 var pathElementName = pathElement.nodeName.toLowerCase() ; 204 205 if ( pathElementName == this.Element ) 206 { 207 // Remove any attribute that conflict with this style, no 208 // matter their values. 209 for ( var att in styleAttribs ) 210 { 211 if ( FCKDomTools.HasAttribute( pathElement, att ) ) 212 { 213 switch ( att ) 214 { 215 case 'style' : 216 this._RemoveStylesFromElement( pathElement ) ; 217 break ; 218 219 case 'class' : 220 // The 'class' element value must match (#1318). 221 if ( FCKDomTools.GetAttributeValue( pathElement, att ) != this.GetFinalAttributeValue( att ) ) 222 continue ; 223 224 default : 225 FCKDomTools.RemoveAttribute( pathElement, att ) ; 226 } 227 } 228 } 229 } 230 231 // Remove overrides defined to the same element name. 232 this._RemoveOverrides( pathElement, styleOverrides[ pathElementName ] ) ; 233 234 // Remove the element if no more attributes are available. 235 this._RemoveNoAttribElement( pathElement ) ; 236 } 237 } 238 else if ( isBoundary ) 239 boundaryElements.push( pathElement ) ; 240 241 // Check if we are still in a boundary (at the same side). 242 isBoundary = isBoundary && ( ( isBoundaryRight && !FCKDomTools.GetNextSibling( pathElement ) ) || ( !isBoundaryRight && !FCKDomTools.GetPreviousSibling( pathElement ) ) ) ; 243 244 // If we are in an element that is not anymore a boundary, or 245 // we are at the last element, let's move things outside the 246 // boundary (if available). 247 if ( lastBoundaryElement && ( !isBoundary || ( i == path.Elements.length - 1 ) ) ) 248 { 249 // Remove the bookmark node from the DOM. 250 var currentElement = FCKDomTools.RemoveNode( bookmarkStart ) ; 251 252 // Build the collapsed group of elements that are not 253 // removed by this style, but share the boundary. 254 // (see comment 1 and 2 at #1270) 255 for ( var j = 0 ; j <= boundaryLimitIndex ; j++ ) 256 { 257 var newElement = FCKDomTools.CloneElement( boundaryElements[j] ) ; 258 newElement.appendChild( currentElement ) ; 259 currentElement = newElement ; 260 } 261 262 // Re-insert the bookmark node (and the collapsed elements) 263 // in the DOM, in the new position next to the styled element. 264 if ( isBoundaryRight ) 265 FCKDomTools.InsertAfterNode( lastBoundaryElement, currentElement ) ; 266 else 267 lastBoundaryElement.parentNode.insertBefore( currentElement, lastBoundaryElement ) ; 268 269 isBoundary = false ; 270 lastBoundaryElement = null ; 271 } 272 } 273 274 // Re-select the original range. 275 if ( selectIt ) 276 range.SelectBookmark( bookmark ) ; 277 278 return ; 279 } 280 281 // Expand the range, if inside inline element boundaries. 282 range.Expand( 'inline_elements' ) ; 283 284 // Bookmark the range so we can re-select it after processing. 285 var bookmark = range.CreateBookmark( true ) ; 286 287 // The style will be applied within the bookmark boundaries. 288 var startNode = range.GetBookmarkNode( bookmark, true ) ; 289 var endNode = range.GetBookmarkNode( bookmark, false ) ; 290 291 range.Release( true ) ; 292 293 // We need to check the selection boundaries (bookmark spans) to break 294 // the code in a way that we can properly remove partially selected nodes. 295 // For example, removing a <b> style from 296 // <b>This is [some text</b> to show <b>the] problem</b> 297 // ... where [ and ] represent the selection, must result: 298 // <b>This is </b>[some text to show the]<b> problem</b> 299 // The strategy is simple, we just break the partial nodes before the 300 // removal logic, having something that could be represented this way: 301 // <b>This is </b>[<b>some text</b> to show <b>the</b>]<b> problem</b> 302 303 // Let's start checking the start boundary. 304 var path = new FCKElementPath( startNode ) ; 305 var pathElements = path.Elements ; 306 var pathElement ; 307 308 for ( var i = 1 ; i < pathElements.length ; i++ ) 309 { 310 pathElement = pathElements[i] ; 311 312 if ( pathElement == path.Block || pathElement == path.BlockLimit ) 313 break ; 314 315 // If this element can be removed (even partially). 316 if ( this.CheckElementRemovable( pathElement ) ) 317 FCKDomTools.BreakParent( startNode, pathElement, range ) ; 318 } 319 320 // Now the end boundary. 321 path = new FCKElementPath( endNode ) ; 322 pathElements = path.Elements ; 323 324 for ( var i = 1 ; i < pathElements.length ; i++ ) 325 { 326 pathElement = pathElements[i] ; 327 328 if ( pathElement == path.Block || pathElement == path.BlockLimit ) 329 break ; 330 331 elementName = pathElement.nodeName.toLowerCase() ; 332 333 // If this element can be removed (even partially). 334 if ( this.CheckElementRemovable( pathElement ) ) 335 FCKDomTools.BreakParent( endNode, pathElement, range ) ; 336 } 337 338 // Navigate through all nodes between the bookmarks. 339 var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ; 340 341 while ( currentNode ) 342 { 343 // Cache the next node to be processed. Do it now, because 344 // currentNode may be removed. 345 var nextNode = FCKDomTools.GetNextSourceNode( currentNode ) ; 346 347 // Remove elements nodes that match with this style rules. 348 if ( currentNode.nodeType == 1 ) 349 { 350 var elementName = currentNode.nodeName.toLowerCase() ; 351 352 var mayRemove = ( elementName == this.Element ) ; 353 if ( mayRemove ) 354 { 355 // Remove any attribute that conflict with this style, no matter 356 // their values. 357 for ( var att in styleAttribs ) 358 { 359 if ( FCKDomTools.HasAttribute( currentNode, att ) ) 360 { 361 switch ( att ) 362 { 363 case 'style' : 364 this._RemoveStylesFromElement( currentNode ) ; 365 break ; 366 367 case 'class' : 368 // The 'class' element value must match (#1318). 369 if ( FCKDomTools.GetAttributeValue( currentNode, att ) != this.GetFinalAttributeValue( att ) ) 370 continue ; 371 372 default : 373 FCKDomTools.RemoveAttribute( currentNode, att ) ; 374 } 375 } 376 } 377 } 378 else 379 mayRemove = !!styleOverrides[ elementName ] ; 380 381 if ( mayRemove ) 382 { 383 // Remove overrides defined to the same element name. 384 this._RemoveOverrides( currentNode, styleOverrides[ elementName ] ) ; 385 386 // Remove the element if no more attributes are available. 387 this._RemoveNoAttribElement( currentNode ) ; 388 } 389 } 390 391 // If we have reached the end of the selection, stop looping. 392 if ( nextNode == endNode ) 393 break ; 394 395 currentNode = nextNode ; 396 } 397 398 this._FixBookmarkStart( startNode ) ; 399 400 // Re-select the original range. 401 if ( selectIt ) 402 range.SelectBookmark( bookmark ) ; 403 }, 404 405 /** 406 * Checks if an element, or any of its attributes, is removable by the 407 * current style definition. 408 */ 409 CheckElementRemovable : function( element, fullMatch ) 410 { 411 if ( !element ) 412 return false ; 413 414 var elementName = element.nodeName.toLowerCase() ; 415 416 // If the element name is the same as the style name. 417 if ( elementName == this.Element ) 418 { 419 // If no attributes are defined in the element. 420 if ( !fullMatch && !FCKDomTools.HasAttributes( element ) ) 421 return true ; 422 423 // If any attribute conflicts with the style attributes. 424 var attribs = this._GetAttribsForComparison() ; 425 var allMatched = ( attribs._length == 0 ) ; 426 for ( var att in attribs ) 427 { 428 if ( att == '_length' ) 429 continue ; 430 431 if ( this._CompareAttributeValues( att, FCKDomTools.GetAttributeValue( element, att ), ( this.GetFinalAttributeValue( att ) || '' ) ) ) 432 { 433 allMatched = true ; 434 if ( !fullMatch ) 435 break ; 436 } 437 else 438 { 439 allMatched = false ; 440 if ( fullMatch ) 441 return false ; 442 } 443 } 444 if ( allMatched ) 445 return true ; 446 } 447 448 // Check if the element can be somehow overriden. 449 var override = this._GetOverridesForComparison()[ elementName ] ; 450 if ( override ) 451 { 452 // If no attributes have been defined, remove the element. 453 if ( !( attribs = override.Attributes ) ) // Only one "=" 454 return true ; 455 456 for ( var i = 0 ; i < attribs.length ; i++ ) 457 { 458 var attName = attribs[i][0] ; 459 if ( FCKDomTools.HasAttribute( element, attName ) ) 460 { 461 var attValue = attribs[i][1] ; 462 463 // Remove the attribute if: 464 // - The override definition value is null ; 465 // - The override definition valie is a string that 466 // matches the attribute value exactly. 467 // - The override definition value is a regex that 468 // has matches in the attribute value. 469 if ( attValue == null || 470 ( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) || 471 attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) ) 472 return true ; 473 } 474 } 475 } 476 477 return false ; 478 }, 479 480 /** 481 * Get the style state for an element path. Returns "true" if the element 482 * is active in the path. 483 */ 484 CheckActive : function( elementPath ) 485 { 486 switch ( this.GetType() ) 487 { 488 case FCK_STYLE_BLOCK : 489 return this.CheckElementRemovable( elementPath.Block || elementPath.BlockLimit ) ; 490 491 case FCK_STYLE_INLINE : 492 493 var elements = elementPath.Elements ; 494 495 for ( var i = 0 ; i < elements.length ; i++ ) 496 { 497 var element = elements[i] ; 498 499 if ( element == elementPath.Block || element == elementPath.BlockLimit ) 500 continue ; 501 502 if ( this.CheckElementRemovable( element, true ) ) 503 return true ; 504 } 505 } 506 return false ; 507 }, 508 509 /** 510 * Removes an inline style from inside an element tree. The element node 511 * itself is not checked or removed, only the child tree inside of it. 512 */ 513 RemoveFromElement : function( element ) 514 { 515 var attribs = this._GetAttribsForComparison() ; 516 var overrides = this._GetOverridesForComparison() ; 517 518 // Get all elements with the same name. 519 var innerElements = element.getElementsByTagName( this.Element ) ; 520 521 for ( var i = innerElements.length - 1 ; i >= 0 ; i-- ) 522 { 523 var innerElement = innerElements[i] ; 524 525 // Remove any attribute that conflict with this style, no matter 526 // their values. 527 for ( var att in attribs ) 528 { 529 if ( FCKDomTools.HasAttribute( innerElement, att ) ) 530 { 531 switch ( att ) 532 { 533 case 'style' : 534 this._RemoveStylesFromElement( innerElement ) ; 535 break ; 536 537 case 'class' : 538 // The 'class' element value must match (#1318). 539 if ( FCKDomTools.GetAttributeValue( innerElement, att ) != this.GetFinalAttributeValue( att ) ) 540 continue ; 541 542 default : 543 FCKDomTools.RemoveAttribute( innerElement, att ) ; 544 } 545 } 546 } 547 548 // Remove overrides defined to the same element name. 549 this._RemoveOverrides( innerElement, overrides[ this.Element ] ) ; 550 551 // Remove the element if no more attributes are available. 552 this._RemoveNoAttribElement( innerElement ) ; 553 } 554 555 // Now remove any other element with different name that is 556 // defined to be overriden. 557 for ( var overrideElement in overrides ) 558 { 559 if ( overrideElement != this.Element ) 560 { 561 // Get all elements. 562 innerElements = element.getElementsByTagName( overrideElement ) ; 563 564 for ( var i = innerElements.length - 1 ; i >= 0 ; i-- ) 565 { 566 var innerElement = innerElements[i] ; 567 this._RemoveOverrides( innerElement, overrides[ overrideElement ] ) ; 568 this._RemoveNoAttribElement( innerElement ) ; 569 } 570 } 571 } 572 }, 573 574 _RemoveStylesFromElement : function( element ) 575 { 576 var elementStyle = element.style.cssText ; 577 var pattern = this.GetFinalStyleValue() ; 578 579 if ( elementStyle.length > 0 && pattern.length == 0 ) 580 return ; 581 582 pattern = '(^|;)\\s*(' + 583 pattern.replace( /\s*([^ ]+):.*?(;|$)/g, '$1|' ).replace( /\|$/, '' ) + 584 '):[^;]+' ; 585 586 var regex = new RegExp( pattern, 'gi' ) ; 587 588 elementStyle = elementStyle.replace( regex, '' ).Trim() ; 589 590 if ( elementStyle.length == 0 || elementStyle == ';' ) 591 FCKDomTools.RemoveAttribute( element, 'style' ) ; 592 else 593 element.style.cssText = elementStyle.replace( regex, '' ) ; 594 }, 595 596 /** 597 * Remove all attributes that are defined to be overriden, 598 */ 599 _RemoveOverrides : function( element, override ) 600 { 601 var attributes = override && override.Attributes ; 602 603 if ( attributes ) 604 { 605 for ( var i = 0 ; i < attributes.length ; i++ ) 606 { 607 var attName = attributes[i][0] ; 608 609 if ( FCKDomTools.HasAttribute( element, attName ) ) 610 { 611 var attValue = attributes[i][1] ; 612 613 // Remove the attribute if: 614 // - The override definition value is null ; 615 // - The override definition valie is a string that 616 // matches the attribute value exactly. 617 // - The override definition value is a regex that 618 // has matches in the attribute value. 619 if ( attValue == null || 620 ( attValue.test && attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) ) || 621 ( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) ) 622 FCKDomTools.RemoveAttribute( element, attName ) ; 623 } 624 } 625 } 626 }, 627 628 /** 629 * If the element has no more attributes, remove it. 630 */ 631 _RemoveNoAttribElement : function( element ) 632 { 633 // If no more attributes remained in the element, remove it, 634 // leaving its children. 635 if ( !FCKDomTools.HasAttributes( element ) ) 636 { 637 // Removing elements may open points where merging is possible, 638 // so let's cache the first and last nodes for later checking. 639 var firstChild = element.firstChild ; 640 var lastChild = element.lastChild ; 641 642 FCKDomTools.RemoveNode( element, true ) ; 643 644 // Check the cached nodes for merging. 645 this._MergeSiblings( firstChild ) ; 646 647 if ( firstChild != lastChild ) 648 this._MergeSiblings( lastChild ) ; 649 } 650 }, 651 652 /** 653 * Creates a DOM element for this style object. 654 */ 655 BuildElement : function( targetDoc, element ) 656 { 657 // Create the element. 658 var el = element || targetDoc.createElement( this.Element ) ; 659 660 // Assign all defined attributes. 661 var attribs = this._StyleDesc.Attributes ; 662 var attValue ; 663 if ( attribs ) 664 { 665 for ( var att in attribs ) 666 { 667 attValue = this.GetFinalAttributeValue( att ) ; 668 669 if ( att.toLowerCase() == 'class' ) 670 el.className = attValue ; 671 else 672 el.setAttribute( att, attValue ) ; 673 } 674 } 675 676 // Assign the style attribute. 677 if ( this._GetStyleText().length > 0 ) 678 el.style.cssText = this.GetFinalStyleValue() ; 679 680 return el ; 681 }, 682 683 _CompareAttributeValues : function( attName, valueA, valueB ) 684 { 685 if ( attName == 'style' && valueA && valueB ) 686 { 687 valueA = valueA.replace( /;$/, '' ).toLowerCase() ; 688 valueB = valueB.replace( /;$/, '' ).toLowerCase() ; 689 } 690 691 return ( valueA == valueB ) 692 }, 693 694 GetFinalAttributeValue : function( attName ) 695 { 696 var attValue = this._StyleDesc.Attributes ; 697 var attValue = attValue ? attValue[ attName ] : null ; 698 699 if ( !attValue && attName == 'style' ) 700 return this.GetFinalStyleValue() ; 701 702 if ( attValue && this._Variables ) 703 // Using custom Replace() to guarantee the correct scope. 704 attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ; 705 706 return attValue ; 707 }, 708 709 GetFinalStyleValue : function() 710 { 711 var attValue = this._GetStyleText() ; 712 713 if ( attValue.length > 0 && this._Variables ) 714 { 715 // Using custom Replace() to guarantee the correct scope. 716 attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ; 717 attValue = FCKTools.NormalizeCssText( attValue ) ; 718 } 719 720 return attValue ; 721 }, 722 723 _GetVariableReplace : function() 724 { 725 // The second group in the regex is the variable name. 726 return this._Variables[ arguments[2] ] || arguments[0] ; 727 }, 728 729 /** 730 * Set the value of a variable attribute or style, to be used when 731 * appliying the style. 732 */ 733 SetVariable : function( name, value ) 734 { 735 var variables = this._Variables ; 736 737 if ( !variables ) 738 variables = this._Variables = {} ; 739 740 this._Variables[ name ] = value ; 741 }, 742 743 /** 744 * Apply an inline style to a FCKDomRange. 745 * 746 * TODO 747 * - Implement the "#" style handling. 748 * - Properly handle block containers like <div> and <blockquote>. 749 */ 750 _ApplyBlockStyle : function( range, selectIt ) 751 { 752 // Bookmark the range so we can re-select it after processing. 753 var bookmark ; 754 755 if ( selectIt ) 756 bookmark = range.CreateBookmark( true ) ; 757 758 var iterator = new FCKDomRangeIterator( range ) ; 759 iterator.EnforceRealBlocks = true ; 760 761 var block ; 762 while( ( block = iterator.GetNextParagraph() ) ) // Only one = 763 { 764 // Create the new node right before the current one. 765 var newBlock = block.parentNode.insertBefore( this.BuildElement( range.Window.document ), block ) ; 766 767 // Move everything from the current node to the new one. 768 FCKDomTools.MoveChildren( block, newBlock ) ; 769 770 // Delete the current node. 771 FCKDomTools.RemoveNode( block ) ; 772 } 773 774 // Re-select the original range. 775 if ( selectIt ) 776 range.SelectBookmark( bookmark ) ; 777 }, 778 779 /** 780 * Apply an inline style to a FCKDomRange. 781 * 782 * TODO 783 * - Merge elements, when applying styles to similar elements that enclose 784 * the entire selection, outputing: 785 * <span style="color: #ff0000; background-color: #ffffff">XYZ</span> 786 * instead of: 787 * <span style="color: #ff0000;"><span style="background-color: #ffffff">XYZ</span></span> 788 */ 789 _ApplyInlineStyle : function( range, selectIt ) 790 { 791 var doc = range.Window.document ; 792 793 if ( range.CheckIsCollapsed() ) 794 { 795 // Create the element to be inserted in the DOM. 796 var collapsedElement = this.BuildElement( doc ) ; 797 range.InsertNode( collapsedElement ) ; 798 range.MoveToPosition( collapsedElement, 2 ) ; 799 range.Select() ; 800 801 return ; 802 } 803 804 // The general idea here is navigating through all nodes inside the 805 // current selection, working on distinct range blocks, defined by the 806 // DTD compatibility between the style element and the nodes inside the 807 // ranges. 808 // 809 // For example, suppose we have the following selection (where [ and ] 810 // are the boundaries), and we apply a <b> style there: 811 // 812 // <p>Here we [have <b>some</b> text.<p> 813 // <p>And some here] here.</p> 814 // 815 // Two different ranges will be detected: 816 // 817 // "have <b>some</b> text." 818 // "And some here" 819 // 820 // Both ranges will be extracted, moved to a <b> element, and 821 // re-inserted, resulting in the following output: 822 // 823 // <p>Here we [<b>have some text.</b><p> 824 // <p><b>And some here</b>] here.</p> 825 // 826 // Note that the <b> element at <b>some</b> is also removed because it 827 // is not needed anymore. 828 829 var elementName = this.Element ; 830 831 // Get the DTD definition for the element. Defaults to "span". 832 var elementDTD = FCK.DTD[ elementName ] || FCK.DTD.span ; 833 834 // Create the attribute list to be used later for element comparisons. 835 var styleAttribs = this._GetAttribsForComparison() ; 836 var styleNode ; 837 838 // Expand the range, if inside inline element boundaries. 839 range.Expand( 'inline_elements' ) ; 840 841 // Bookmark the range so we can re-select it after processing. 842 var bookmark = range.CreateBookmark( true ) ; 843 844 // The style will be applied within the bookmark boundaries. 845 var startNode = range.GetBookmarkNode( bookmark, true ) ; 846 var endNode = range.GetBookmarkNode( bookmark, false ) ; 847 848 // We'll be reusing the range to apply the styles. So, release it here 849 // to indicate that it has not been initialized. 850 range.Release( true ) ; 851 852 // Let's start the nodes lookup from the node right after the bookmark 853 // span. 854 var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ; 855 856 while ( currentNode ) 857 { 858 var applyStyle = false ; 859 860 var nodeType = currentNode.nodeType ; 861 var nodeName = nodeType == 1 ? currentNode.nodeName.toLowerCase() : null ; 862 863 // Check if the current node can be a child of the style element. 864 if ( !nodeName || elementDTD[ nodeName ] ) 865 { 866 // Check if the style element can be a child of the current 867 // node parent. 868 if ( ( FCK.DTD[ currentNode.parentNode.nodeName.toLowerCase() ] || FCK.DTD.span )[ elementName ] ) 869 { 870 // This node will be part of our range, so if it has not 871 // been started, place its start right before the node. 872 if ( !range.CheckHasRange() ) 873 range.SetStart( currentNode, 3 ) ; 874 875 // Non element nodes, or empty elements can be added 876 // completely to the range. 877 if ( nodeType != 1 || currentNode.childNodes.length == 0 ) 878 { 879 var includedNode = currentNode ; 880 var parentNode = includedNode.parentNode ; 881 882 // This node is about to be included completelly, but, 883 // if this is the last node in its parent, we must also 884 // check if the parent itself can be added completelly 885 // to the range. 886 while ( includedNode == parentNode.lastChild 887 && elementDTD[ parentNode.nodeName.toLowerCase() ] ) 888 { 889 includedNode = parentNode ; 890 } 891 892 range.SetEnd( includedNode, 4 ) ; 893 894 // If the included node is the last node in its parent 895 // and its parent can't be inside the style node, apply 896 // the style immediately. 897 if ( includedNode == includedNode.parentNode.lastChild && !elementDTD[ includedNode.parentNode.nodeName.toLowerCase() ] ) 898 applyStyle = true ; 899 } 900 else 901 { 902 // Element nodes will not be added directly. We need to 903 // check their children because the selection could end 904 // inside the node, so let's place the range end right 905 // before the element. 906 range.SetEnd( currentNode, 3 ) ; 907 } 908 } 909 else 910 applyStyle = true ; 911 } 912 else 913 applyStyle = true ; 914 915 // Get the next node to be processed. 916 currentNode = FCKDomTools.GetNextSourceNode( currentNode ) ; 917 918 // If we have reached the end of the selection, just apply the 919 // style ot the range, and stop looping. 920 if ( currentNode == endNode ) 921 { 922 currentNode = null ; 923 applyStyle = true ; 924 } 925 926 // Apply the style if we have something to which apply it. 927 if ( applyStyle && range.CheckHasRange() && !range.CheckIsCollapsed() ) 928 { 929 // Build the style element, based on the style object definition. 930 styleNode = this.BuildElement( doc ) ; 931 932 // Move the contents of the range to the style element. 933 range.ExtractContents().AppendTo( styleNode ) ; 934 935 // If it is not empty. 936 if ( styleNode.innerHTML.RTrim().length > 0 ) 937 { 938 // Insert it in the range position (it is collapsed after 939 // ExtractContents. 940 range.InsertNode( styleNode ) ; 941 942 // Here we do some cleanup, removing all duplicated 943 // elements from the style element. 944 this.RemoveFromElement( styleNode ) ; 945 946 // Let's merge our new style with its neighbors, if possible. 947 this._MergeSiblings( styleNode, this._GetAttribsForComparison() ) ; 948 949 // As the style system breaks text nodes constantly, let's normalize 950 // things for performance. 951 // With IE, some paragraphs get broken when calling normalize() 952 // repeatedly. Also, for IE, we must normalize body, not documentElement. 953 // IE is also known for having a "crash effect" with normalize(). 954 // We should try to normalize with IE too in some way, somewhere. 955 if ( !FCKBrowserInfo.IsIE ) 956 styleNode.normalize() ; 957 } 958 959 // Style applied, let's release the range, so it gets marked to 960 // re-initialization in the next loop. 961 range.Release( true ) ; 962 } 963 } 964 965 this._FixBookmarkStart( startNode ) ; 966 967 // Re-select the original range. 968 if ( selectIt ) 969 range.SelectBookmark( bookmark ) ; 970 }, 971 972 _FixBookmarkStart : function( startNode ) 973 { 974 // After appliying or removing an inline style, the start boundary of 975 // the selection must be placed inside all inline elements it is 976 // bordering. 977 var startSibling ; 978 while ( ( startSibling = startNode.nextSibling ) ) // Only one "=". 979 { 980 if ( startSibling.nodeType == 1 981 && FCKListsLib.InlineNonEmptyElements[ startSibling.nodeName.toLowerCase() ] ) 982 { 983 // If it is an empty inline element, we can safely remove it. 984 if ( !startSibling.firstChild ) 985 FCKDomTools.RemoveNode( startSibling ) ; 986 else 987 FCKDomTools.MoveNode( startNode, startSibling, true ) ; 988 continue ; 989 } 990 991 // Empty text nodes can be safely removed to not disturb. 992 if ( startSibling.nodeType == 3 && startSibling.length == 0 ) 993 { 994 FCKDomTools.RemoveNode( startSibling ) ; 995 continue ; 996 } 997 998 break ; 999 } 1000 }, 1001 1002 /** 1003 * Merge an element with its similar siblings. 1004 * "attribs" is and object computed with _CreateAttribsForComparison. 1005 */ 1006 _MergeSiblings : function( element, attribs ) 1007 { 1008 if ( !element || element.nodeType != 1 || !FCKListsLib.InlineNonEmptyElements[ element.nodeName.toLowerCase() ] ) 1009 return ; 1010 1011 this._MergeNextSibling( element, attribs ) ; 1012 this._MergePreviousSibling( element, attribs ) ; 1013 }, 1014 1015 /** 1016 * Merge an element with its similar siblings after it. 1017 * "attribs" is and object computed with _CreateAttribsForComparison. 1018 */ 1019 _MergeNextSibling : function( element, attribs ) 1020 { 1021 // Check the next sibling. 1022 var sibling = element.nextSibling ; 1023 1024 // Check if the next sibling is a bookmark element. In this case, jump it. 1025 var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ; 1026 if ( hasBookmark ) 1027 sibling = sibling.nextSibling ; 1028 1029 if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName ) 1030 { 1031 if ( !attribs ) 1032 attribs = this._CreateElementAttribsForComparison( element ) ; 1033 1034 if ( this._CheckAttributesMatch( sibling, attribs ) ) 1035 { 1036 // Save the last child to be checked too (to merge things like <b><i></i></b><b><i></i></b>). 1037 var innerSibling = element.lastChild ; 1038 1039 if ( hasBookmark ) 1040 FCKDomTools.MoveNode( element.nextSibling, element ) ; 1041 1042 // Move contents from the sibling. 1043 FCKDomTools.MoveChildren( sibling, element ) ; 1044 FCKDomTools.RemoveNode( sibling ) ; 1045 1046 // Now check the last inner child (see two comments above). 1047 if ( innerSibling ) 1048 this._MergeNextSibling( innerSibling ) ; 1049 } 1050 } 1051 }, 1052 1053 /** 1054 * Merge an element with its similar siblings before it. 1055 * "attribs" is and object computed with _CreateAttribsForComparison. 1056 */ 1057 _MergePreviousSibling : function( element, attribs ) 1058 { 1059 // Check the previous sibling. 1060 var sibling = element.previousSibling ; 1061 1062 // Check if the previous sibling is a bookmark element. In this case, jump it. 1063 var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ; 1064 if ( hasBookmark ) 1065 sibling = sibling.previousSibling ; 1066 1067 if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName ) 1068 { 1069 if ( !attribs ) 1070 attribs = this._CreateElementAttribsForComparison( element ) ; 1071 1072 if ( this._CheckAttributesMatch( sibling, attribs ) ) 1073 { 1074 // Save the first child to be checked too (to merge things like <b><i></i></b><b><i></i></b>). 1075 var innerSibling = element.firstChild ; 1076 1077 if ( hasBookmark ) 1078 FCKDomTools.MoveNode( element.previousSibling, element, true ) ; 1079 1080 // Move contents to the sibling. 1081 FCKDomTools.MoveChildren( sibling, element, true ) ; 1082 FCKDomTools.RemoveNode( sibling ) ; 1083 1084 // Now check the first inner child (see two comments above). 1085 if ( innerSibling ) 1086 this._MergePreviousSibling( innerSibling ) ; 1087 } 1088 } 1089 }, 1090 1091 /** 1092 * Build the cssText based on the styles definition. 1093 */ 1094 _GetStyleText : function() 1095 { 1096 var stylesDef = this._StyleDesc.Styles ; 1097 1098 // Builds the StyleText. 1099 var stylesText = ( this._StyleDesc.Attributes ? this._StyleDesc.Attributes['style'] || '' : '' ) ; 1100 1101 if ( stylesText.length > 0 ) 1102 stylesText += ';' ; 1103 1104 for ( var style in stylesDef ) 1105 stylesText += style + ':' + stylesDef[style] + ';' ; 1106 1107 // Browsers make some changes to the style when applying them. So, here 1108 // we normalize it to the browser format. We'll not do that if there 1109 // are variables inside the style. 1110 if ( stylesText.length > 0 && !( /#\(/.test( stylesText ) ) ) 1111 { 1112 stylesText = FCKTools.NormalizeCssText( stylesText ) ; 1113 } 1114 1115 return (this._GetStyleText = function() { return stylesText ; })() ; 1116 }, 1117 1118 /** 1119 * Get the the collection used to compare the attributes defined in this 1120 * style with attributes in an element. All information in it is lowercased. 1121 */ 1122 _GetAttribsForComparison : function() 1123 { 1124 // If we have already computed it, just return it. 1125 var attribs = this._GetAttribsForComparison_$ ; 1126 if ( attribs ) 1127 return attribs ; 1128 1129 attribs = new Object() ; 1130 1131 // Loop through all defined attributes. 1132 var styleAttribs = this._StyleDesc.Attributes ; 1133 if ( styleAttribs ) 1134 { 1135 for ( var styleAtt in styleAttribs ) 1136 { 1137 attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase() ; 1138 } 1139 } 1140 1141 // Includes the style definitions. 1142 if ( this._GetStyleText().length > 0 ) 1143 { 1144 attribs['style'] = this._GetStyleText().toLowerCase() ; 1145 } 1146 1147 // Appends the "length" information to the object. 1148 FCKTools.AppendLengthProperty( attribs, '_length' ) ; 1149 1150 // Return it, saving it to the next request. 1151 return ( this._GetAttribsForComparison_$ = attribs ) ; 1152 }, 1153 1154 /** 1155 * Get the the collection used to compare the elements and attributes, 1156 * defined in this style overrides, with other element. All information in 1157 * it is lowercased. 1158 */ 1159 _GetOverridesForComparison : function() 1160 { 1161 // If we have already computed it, just return it. 1162 var overrides = this._GetOverridesForComparison_$ ; 1163 if ( overrides ) 1164 return overrides ; 1165 1166 overrides = new Object() ; 1167 1168 var overridesDesc = this._StyleDesc.Overrides ; 1169 1170 if ( overridesDesc ) 1171 { 1172 // The override description can be a string, object or array. 1173 // Internally, well handle arrays only, so transform it if needed. 1174 if ( !FCKTools.IsArray( overridesDesc ) ) 1175 overridesDesc = [ overridesDesc ] ; 1176 1177 // Loop through all override definitions. 1178 for ( var i = 0 ; i < overridesDesc.length ; i++ ) 1179 { 1180 var override = overridesDesc[i] ; 1181 var elementName ; 1182 var overrideEl ; 1183 var attrs ; 1184 1185 // If can be a string with the element name. 1186 if ( typeof override == 'string' ) 1187 elementName = override.toLowerCase() ; 1188 // Or an object. 1189 else 1190 { 1191 elementName = override.Element ? override.Element.toLowerCase() : this.Element ; 1192 attrs = override.Attributes ; 1193 } 1194 1195 // We can have more than one override definition for the same 1196 // element name, so we attempt to simply append information to 1197 // it if it already exists. 1198 overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ) ; 1199 1200 if ( attrs ) 1201 { 1202 // The returning attributes list is an array, because we 1203 // could have different override definitions for the same 1204 // attribute name. 1205 var overrideAttrs = ( overrideEl.Attributes = overrideEl.Attributes || new Array() ) ; 1206 for ( var attName in attrs ) 1207 { 1208 // Each item in the attributes array is also an array, 1209 // where [0] is the attribute name and [1] is the 1210 // override value. 1211 overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ) ; 1212 } 1213 } 1214 } 1215 } 1216 1217 return ( this._GetOverridesForComparison_$ = overrides ) ; 1218 }, 1219 1220 /* 1221 * Create and object containing all attributes specified in an element, 1222 * added by a "_length" property. All values are lowercased. 1223 */ 1224 _CreateElementAttribsForComparison : function( element ) 1225 { 1226 var attribs = new Object() ; 1227 var attribsCount = 0 ; 1228 1229 for ( var i = 0 ; i < element.attributes.length ; i++ ) 1230 { 1231 var att = element.attributes[i] ; 1232 1233 if ( att.specified ) 1234 { 1235 attribs[ att.nodeName.toLowerCase() ] = FCKDomTools.GetAttributeValue( element, att ).toLowerCase() ; 1236 attribsCount++ ; 1237 } 1238 } 1239 1240 attribs._length = attribsCount ; 1241 1242 return attribs ; 1243 }, 1244 1245 /** 1246 * Checks is the element attributes have a perfect match with the style 1247 * attributes. 1248 */ 1249 _CheckAttributesMatch : function( element, styleAttribs ) 1250 { 1251 // Loop through all specified attributes. The same number of 1252 // attributes must be found and their values must match to 1253 // declare them as equal. 1254 1255 var elementAttrbs = element.attributes ; 1256 var matchCount = 0 ; 1257 1258 for ( var i = 0 ; i < elementAttrbs.length ; i++ ) 1259 { 1260 var att = elementAttrbs[i] ; 1261 if ( att.specified ) 1262 { 1263 var attName = att.nodeName.toLowerCase() ; 1264 var styleAtt = styleAttribs[ attName ] ; 1265 1266 // The attribute is not defined in the style. 1267 if ( !styleAtt ) 1268 break ; 1269 1270 // The values are different. 1271 if ( styleAtt != FCKDomTools.GetAttributeValue( element, att ).toLowerCase() ) 1272 break ; 1273 1274 matchCount++ ; 1275 } 1276 } 1277 1278 return ( matchCount == styleAttribs._length ) ; 1279 } 1280} ; 1281