2 * FCKeditor - The text editor for Internet - http://www.fckeditor.net
\r
3 * Copyright (C) 2003-2009 Frederico Caldeira Knabben
\r
5 * == BEGIN LICENSE ==
\r
7 * Licensed under the terms of any of the following licenses at your
\r
10 * - GNU General Public License Version 2 or later (the "GPL")
\r
11 * http://www.gnu.org/licenses/gpl.html
\r
13 * - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
\r
14 * http://www.gnu.org/licenses/lgpl.html
\r
16 * - Mozilla Public License Version 1.1 or later (the "MPL")
\r
17 * http://www.mozilla.org/MPL/MPL-1.1.html
\r
21 * FCKStyle Class: contains a style definition, and all methods to work with
\r
22 * the style in a document.
\r
26 * @param {Object} styleDesc A "style descriptor" object, containing the raw
\r
27 * style definition in the following format:
\r
28 * '<style name>' : {
\r
29 * Element : '<element name>',
\r
31 * '<att name>' : '<att value>',
\r
35 * '<style name>' : '<style value>',
\r
38 * Overrides : '<element name>'|{
\r
39 * Element : '<element name>',
\r
41 * '<att name>' : '<att value>'|/<att regex>/
\r
44 * '<style name>' : '<style value>'|/<style regex>/
\r
49 var FCKStyle = function( styleDesc )
\r
51 this.Element = ( styleDesc.Element || 'span' ).toLowerCase() ;
\r
52 this._StyleDesc = styleDesc ;
\r
55 FCKStyle.prototype =
\r
58 * Get the style type, based on its element name:
\r
59 * - FCK_STYLE_BLOCK (0): Block Style
\r
60 * - FCK_STYLE_INLINE (1): Inline Style
\r
61 * - FCK_STYLE_OBJECT (2): Object Style
\r
63 GetType : function()
\r
65 var type = this.GetType_$ ;
\r
67 if ( type != undefined )
\r
70 var elementName = this.Element ;
\r
72 if ( elementName == '#' || FCKListsLib.StyleBlockElements[ elementName ] )
\r
73 type = FCK_STYLE_BLOCK ;
\r
74 else if ( FCKListsLib.StyleObjectElements[ elementName ] )
\r
75 type = FCK_STYLE_OBJECT ;
\r
77 type = FCK_STYLE_INLINE ;
\r
79 return ( this.GetType_$ = type ) ;
\r
83 * Apply the style to the current selection.
\r
85 ApplyToSelection : function( targetWindow )
\r
87 // Create a range for the current selection.
\r
88 var range = new FCKDomRange( targetWindow ) ;
\r
89 range.MoveToSelection() ;
\r
91 this.ApplyToRange( range, true ) ;
\r
95 * Apply the style to a FCKDomRange.
\r
97 ApplyToRange : function( range, selectIt, updateRange )
\r
99 // ApplyToRange is not valid for FCK_STYLE_OBJECT types.
\r
100 // Use ApplyToObject instead.
\r
102 switch ( this.GetType() )
\r
104 case FCK_STYLE_BLOCK :
\r
105 this.ApplyToRange = this._ApplyBlockStyle ;
\r
107 case FCK_STYLE_INLINE :
\r
108 this.ApplyToRange = this._ApplyInlineStyle ;
\r
114 this.ApplyToRange( range, selectIt, updateRange ) ;
\r
118 * Apply the style to an object. Valid for FCK_STYLE_BLOCK types only.
\r
120 ApplyToObject : function( objectElement )
\r
122 if ( !objectElement )
\r
125 this.BuildElement( null, objectElement ) ;
\r
129 * Remove the style from the current selection.
\r
131 RemoveFromSelection : function( targetWindow )
\r
133 // Create a range for the current selection.
\r
134 var range = new FCKDomRange( targetWindow ) ;
\r
135 range.MoveToSelection() ;
\r
137 this.RemoveFromRange( range, true ) ;
\r
141 * Remove the style from a FCKDomRange. Block type styles will have no
\r
144 RemoveFromRange : function( range, selectIt, updateRange )
\r
148 // Create the attribute list to be used later for element comparisons.
\r
149 var styleAttribs = this._GetAttribsForComparison() ;
\r
150 var styleOverrides = this._GetOverridesForComparison() ;
\r
152 // If collapsed, we are removing all conflicting styles from the range
\r
154 if ( range.CheckIsCollapsed() )
\r
156 // Bookmark the range so we can re-select it after processing.
\r
157 var bookmark = range.CreateBookmark( true ) ;
\r
159 // Let's start from the bookmark <span> parent.
\r
160 var bookmarkStart = range.GetBookmarkNode( bookmark, true ) ;
\r
162 var path = new FCKElementPath( bookmarkStart.parentNode ) ;
\r
164 // While looping through the path, we'll be saving references to
\r
165 // parent elements if the range is in one of their boundaries. In
\r
166 // this way, we are able to create a copy of those elements when
\r
167 // removing a style if the range is in a boundary limit (see #1270).
\r
168 var boundaryElements = [] ;
\r
170 // Check if the range is in the boundary limits of an element
\r
171 // (related to #1270).
\r
172 var isBoundaryRight = !FCKDomTools.GetNextSibling( bookmarkStart ) ;
\r
173 var isBoundary = isBoundaryRight || !FCKDomTools.GetPreviousSibling( bookmarkStart ) ;
\r
175 // This is the last element to be removed in the boundary situation
\r
176 // described at #1270.
\r
177 var lastBoundaryElement ;
\r
178 var boundaryLimitIndex = -1 ;
\r
180 for ( var i = 0 ; i < path.Elements.length ; i++ )
\r
182 var pathElement = path.Elements[i] ;
\r
183 if ( this.CheckElementRemovable( pathElement ) )
\r
186 && !FCKDomTools.CheckIsEmptyElement( pathElement,
\r
189 return ( el != bookmarkStart ) ;
\r
193 lastBoundaryElement = pathElement ;
\r
195 // We'll be continuously including elements in the
\r
196 // boundaryElements array, but only those added before
\r
197 // setting lastBoundaryElement must be used later, so
\r
198 // let's mark the current index here.
\r
199 boundaryLimitIndex = boundaryElements.length - 1 ;
\r
203 var pathElementName = pathElement.nodeName.toLowerCase() ;
\r
205 if ( pathElementName == this.Element )
\r
207 // Remove any attribute that conflict with this style, no
\r
208 // matter their values.
\r
209 for ( var att in styleAttribs )
\r
211 if ( FCKDomTools.HasAttribute( pathElement, att ) )
\r
216 this._RemoveStylesFromElement( pathElement ) ;
\r
220 // The 'class' element value must match (#1318).
\r
221 if ( FCKDomTools.GetAttributeValue( pathElement, att ) != this.GetFinalAttributeValue( att ) )
\r
227 FCKDomTools.RemoveAttribute( pathElement, att ) ;
\r
233 // Remove overrides defined to the same element name.
\r
234 this._RemoveOverrides( pathElement, styleOverrides[ pathElementName ] ) ;
\r
236 // Remove the element if no more attributes are available and it's an inline style element
\r
237 if ( this.GetType() == FCK_STYLE_INLINE)
\r
238 this._RemoveNoAttribElement( pathElement ) ;
\r
241 else if ( isBoundary )
\r
242 boundaryElements.push( pathElement ) ;
\r
244 // Check if we are still in a boundary (at the same side).
\r
245 isBoundary = isBoundary && ( ( isBoundaryRight && !FCKDomTools.GetNextSibling( pathElement ) ) || ( !isBoundaryRight && !FCKDomTools.GetPreviousSibling( pathElement ) ) ) ;
\r
247 // If we are in an element that is not anymore a boundary, or
\r
248 // we are at the last element, let's move things outside the
\r
249 // boundary (if available).
\r
250 if ( lastBoundaryElement && ( !isBoundary || ( i == path.Elements.length - 1 ) ) )
\r
252 // Remove the bookmark node from the DOM.
\r
253 var currentElement = FCKDomTools.RemoveNode( bookmarkStart ) ;
\r
255 // Build the collapsed group of elements that are not
\r
256 // removed by this style, but share the boundary.
\r
257 // (see comment 1 and 2 at #1270)
\r
258 for ( var j = 0 ; j <= boundaryLimitIndex ; j++ )
\r
260 var newElement = FCKDomTools.CloneElement( boundaryElements[j] ) ;
\r
261 newElement.appendChild( currentElement ) ;
\r
262 currentElement = newElement ;
\r
265 // Re-insert the bookmark node (and the collapsed elements)
\r
266 // in the DOM, in the new position next to the styled element.
\r
267 if ( isBoundaryRight )
\r
268 FCKDomTools.InsertAfterNode( lastBoundaryElement, currentElement ) ;
\r
270 lastBoundaryElement.parentNode.insertBefore( currentElement, lastBoundaryElement ) ;
\r
272 isBoundary = false ;
\r
273 lastBoundaryElement = null ;
\r
277 // Re-select the original range.
\r
279 range.SelectBookmark( bookmark ) ;
\r
282 range.MoveToBookmark( bookmark ) ;
\r
287 // Expand the range, if inside inline element boundaries.
\r
288 range.Expand( 'inline_elements' ) ;
\r
290 // Bookmark the range so we can re-select it after processing.
\r
291 bookmark = range.CreateBookmark( true ) ;
\r
293 // The style will be applied within the bookmark boundaries.
\r
294 var startNode = range.GetBookmarkNode( bookmark, true ) ;
\r
295 var endNode = range.GetBookmarkNode( bookmark, false ) ;
\r
297 range.Release( true ) ;
\r
299 // We need to check the selection boundaries (bookmark spans) to break
\r
300 // the code in a way that we can properly remove partially selected nodes.
\r
301 // For example, removing a <b> style from
\r
302 // <b>This is [some text</b> to show <b>the] problem</b>
\r
303 // ... where [ and ] represent the selection, must result:
\r
304 // <b>This is </b>[some text to show the]<b> problem</b>
\r
305 // The strategy is simple, we just break the partial nodes before the
\r
306 // removal logic, having something that could be represented this way:
\r
307 // <b>This is </b>[<b>some text</b> to show <b>the</b>]<b> problem</b>
\r
309 // Let's start checking the start boundary.
\r
310 var path = new FCKElementPath( startNode ) ;
\r
311 var pathElements = path.Elements ;
\r
314 for ( var i = 1 ; i < pathElements.length ; i++ )
\r
316 pathElement = pathElements[i] ;
\r
318 if ( pathElement == path.Block || pathElement == path.BlockLimit )
\r
321 // If this element can be removed (even partially).
\r
322 if ( this.CheckElementRemovable( pathElement ) )
\r
323 FCKDomTools.BreakParent( startNode, pathElement, range ) ;
\r
326 // Now the end boundary.
\r
327 path = new FCKElementPath( endNode ) ;
\r
328 pathElements = path.Elements ;
\r
330 for ( var i = 1 ; i < pathElements.length ; i++ )
\r
332 pathElement = pathElements[i] ;
\r
334 if ( pathElement == path.Block || pathElement == path.BlockLimit )
\r
337 elementName = pathElement.nodeName.toLowerCase() ;
\r
339 // If this element can be removed (even partially).
\r
340 if ( this.CheckElementRemovable( pathElement ) )
\r
341 FCKDomTools.BreakParent( endNode, pathElement, range ) ;
\r
344 // Navigate through all nodes between the bookmarks.
\r
345 var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ;
\r
347 while ( currentNode )
\r
349 // Cache the next node to be processed. Do it now, because
\r
350 // currentNode may be removed.
\r
351 var nextNode = FCKDomTools.GetNextSourceNode( currentNode ) ;
\r
353 // Remove elements nodes that match with this style rules.
\r
354 if ( currentNode.nodeType == 1 )
\r
356 var elementName = currentNode.nodeName.toLowerCase() ;
\r
358 var mayRemove = ( elementName == this.Element ) ;
\r
361 // Remove any attribute that conflict with this style, no matter
\r
363 for ( var att in styleAttribs )
\r
365 if ( FCKDomTools.HasAttribute( currentNode, att ) )
\r
370 this._RemoveStylesFromElement( currentNode ) ;
\r
374 // The 'class' element value must match (#1318).
\r
375 if ( FCKDomTools.GetAttributeValue( currentNode, att ) != this.GetFinalAttributeValue( att ) )
\r
381 FCKDomTools.RemoveAttribute( currentNode, att ) ;
\r
387 mayRemove = !!styleOverrides[ elementName ] ;
\r
391 // Remove overrides defined to the same element name.
\r
392 this._RemoveOverrides( currentNode, styleOverrides[ elementName ] ) ;
\r
394 // Remove the element if no more attributes are available.
\r
395 this._RemoveNoAttribElement( currentNode ) ;
\r
399 // If we have reached the end of the selection, stop looping.
\r
400 if ( nextNode == endNode )
\r
403 currentNode = nextNode ;
\r
406 this._FixBookmarkStart( startNode ) ;
\r
408 // Re-select the original range.
\r
410 range.SelectBookmark( bookmark ) ;
\r
413 range.MoveToBookmark( bookmark ) ;
\r
417 * Checks if an element, or any of its attributes, is removable by the
\r
418 * current style definition.
\r
420 CheckElementRemovable : function( element, fullMatch )
\r
425 var elementName = element.nodeName.toLowerCase() ;
\r
427 // If the element name is the same as the style name.
\r
428 if ( elementName == this.Element )
\r
430 // If no attributes are defined in the element.
\r
431 if ( !fullMatch && !FCKDomTools.HasAttributes( element ) )
\r
434 // If any attribute conflicts with the style attributes.
\r
435 var attribs = this._GetAttribsForComparison() ;
\r
436 var allMatched = ( attribs._length == 0 ) ;
\r
437 for ( var att in attribs )
\r
439 if ( att == '_length' )
\r
442 if ( this._CompareAttributeValues( att, FCKDomTools.GetAttributeValue( element, att ), ( this.GetFinalAttributeValue( att ) || '' ) ) )
\r
444 allMatched = true ;
\r
450 allMatched = false ;
\r
459 // Check if the element can be somehow overriden.
\r
460 var override = this._GetOverridesForComparison()[ elementName ] ;
\r
463 // If no attributes have been defined, remove the element.
\r
464 if ( !( attribs = override.Attributes ) ) // Only one "="
\r
467 for ( var i = 0 ; i < attribs.length ; i++ )
\r
469 var attName = attribs[i][0] ;
\r
470 if ( FCKDomTools.HasAttribute( element, attName ) )
\r
472 var attValue = attribs[i][1] ;
\r
474 // Remove the attribute if:
\r
475 // - The override definition value is null ;
\r
476 // - The override definition valie is a string that
\r
477 // matches the attribute value exactly.
\r
478 // - The override definition value is a regex that
\r
479 // has matches in the attribute value.
\r
480 if ( attValue == null ||
\r
481 ( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) ||
\r
482 attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) )
\r
492 * Get the style state for an element path. Returns "true" if the element
\r
493 * is active in the path.
\r
495 CheckActive : function( elementPath )
\r
497 switch ( this.GetType() )
\r
499 case FCK_STYLE_BLOCK :
\r
500 return this.CheckElementRemovable( elementPath.Block || elementPath.BlockLimit, true ) ;
\r
502 case FCK_STYLE_INLINE :
\r
504 var elements = elementPath.Elements ;
\r
506 for ( var i = 0 ; i < elements.length ; i++ )
\r
508 var element = elements[i] ;
\r
510 if ( element == elementPath.Block || element == elementPath.BlockLimit )
\r
513 if ( this.CheckElementRemovable( element, true ) )
\r
521 * Removes an inline style from inside an element tree. The element node
\r
522 * itself is not checked or removed, only the child tree inside of it.
\r
524 RemoveFromElement : function( element )
\r
526 var attribs = this._GetAttribsForComparison() ;
\r
527 var overrides = this._GetOverridesForComparison() ;
\r
529 // Get all elements with the same name.
\r
530 var innerElements = element.getElementsByTagName( this.Element ) ;
\r
532 for ( var i = innerElements.length - 1 ; i >= 0 ; i-- )
\r
534 var innerElement = innerElements[i] ;
\r
536 // Remove any attribute that conflict with this style, no matter
\r
538 for ( var att in attribs )
\r
540 if ( FCKDomTools.HasAttribute( innerElement, att ) )
\r
545 this._RemoveStylesFromElement( innerElement ) ;
\r
549 // The 'class' element value must match (#1318).
\r
550 if ( FCKDomTools.GetAttributeValue( innerElement, att ) != this.GetFinalAttributeValue( att ) )
\r
556 FCKDomTools.RemoveAttribute( innerElement, att ) ;
\r
561 // Remove overrides defined to the same element name.
\r
562 this._RemoveOverrides( innerElement, overrides[ this.Element ] ) ;
\r
564 // Remove the element if no more attributes are available.
\r
565 this._RemoveNoAttribElement( innerElement ) ;
\r
568 // Now remove any other element with different name that is
\r
569 // defined to be overriden.
\r
570 for ( var overrideElement in overrides )
\r
572 if ( overrideElement != this.Element )
\r
574 // Get all elements.
\r
575 innerElements = element.getElementsByTagName( overrideElement ) ;
\r
577 for ( var i = innerElements.length - 1 ; i >= 0 ; i-- )
\r
579 var innerElement = innerElements[i] ;
\r
580 this._RemoveOverrides( innerElement, overrides[ overrideElement ] ) ;
\r
581 this._RemoveNoAttribElement( innerElement ) ;
\r
587 _RemoveStylesFromElement : function( element )
\r
589 var elementStyle = element.style.cssText ;
\r
590 var pattern = this.GetFinalStyleValue() ;
\r
592 if ( elementStyle.length > 0 && pattern.length == 0 )
\r
595 pattern = '(^|;)\\s*(' +
\r
596 pattern.replace( /\s*([^ ]+):.*?(;|$)/g, '$1|' ).replace( /\|$/, '' ) +
\r
599 var regex = new RegExp( pattern, 'gi' ) ;
\r
601 elementStyle = elementStyle.replace( regex, '' ).Trim() ;
\r
603 if ( elementStyle.length == 0 || elementStyle == ';' )
\r
604 FCKDomTools.RemoveAttribute( element, 'style' ) ;
\r
606 element.style.cssText = elementStyle.replace( regex, '' ) ;
\r
610 * Remove all attributes that are defined to be overriden,
\r
612 _RemoveOverrides : function( element, override )
\r
614 var attributes = override && override.Attributes ;
\r
618 for ( var i = 0 ; i < attributes.length ; i++ )
\r
620 var attName = attributes[i][0] ;
\r
622 if ( FCKDomTools.HasAttribute( element, attName ) )
\r
624 var attValue = attributes[i][1] ;
\r
626 // Remove the attribute if:
\r
627 // - The override definition value is null ;
\r
628 // - The override definition valie is a string that
\r
629 // matches the attribute value exactly.
\r
630 // - The override definition value is a regex that
\r
631 // has matches in the attribute value.
\r
632 if ( attValue == null ||
\r
633 ( attValue.test && attValue.test( FCKDomTools.GetAttributeValue( element, attName ) ) ) ||
\r
634 ( typeof attValue == 'string' && FCKDomTools.GetAttributeValue( element, attName ) == attValue ) )
\r
635 FCKDomTools.RemoveAttribute( element, attName ) ;
\r
642 * If the element has no more attributes, remove it.
\r
644 _RemoveNoAttribElement : function( element )
\r
646 // If no more attributes remained in the element, remove it,
\r
647 // leaving its children.
\r
648 if ( !FCKDomTools.HasAttributes( element ) )
\r
650 // Removing elements may open points where merging is possible,
\r
651 // so let's cache the first and last nodes for later checking.
\r
652 var firstChild = element.firstChild ;
\r
653 var lastChild = element.lastChild ;
\r
655 FCKDomTools.RemoveNode( element, true ) ;
\r
657 // Check the cached nodes for merging.
\r
658 this._MergeSiblings( firstChild ) ;
\r
660 if ( firstChild != lastChild )
\r
661 this._MergeSiblings( lastChild ) ;
\r
666 * Creates a DOM element for this style object.
\r
668 BuildElement : function( targetDoc, element )
\r
670 // Create the element.
\r
671 var el = element || targetDoc.createElement( this.Element ) ;
\r
673 // Assign all defined attributes.
\r
674 var attribs = this._StyleDesc.Attributes ;
\r
678 for ( var att in attribs )
\r
680 attValue = this.GetFinalAttributeValue( att ) ;
\r
682 if ( att.toLowerCase() == 'class' )
\r
683 el.className = attValue ;
\r
685 el.setAttribute( att, attValue ) ;
\r
689 // Assign the style attribute.
\r
690 if ( this._GetStyleText().length > 0 )
\r
691 el.style.cssText = this.GetFinalStyleValue() ;
\r
696 _CompareAttributeValues : function( attName, valueA, valueB )
\r
698 if ( attName == 'style' && valueA && valueB )
\r
700 valueA = valueA.replace( /;$/, '' ).toLowerCase() ;
\r
701 valueB = valueB.replace( /;$/, '' ).toLowerCase() ;
\r
704 // Return true if they match or if valueA is null and valueB is an empty string
\r
705 return ( valueA == valueB || ( ( valueA === null || valueA === '' ) && ( valueB === null || valueB === '' ) ) )
\r
708 GetFinalAttributeValue : function( attName )
\r
710 var attValue = this._StyleDesc.Attributes ;
\r
711 var attValue = attValue ? attValue[ attName ] : null ;
\r
713 if ( !attValue && attName == 'style' )
\r
714 return this.GetFinalStyleValue() ;
\r
716 if ( attValue && this._Variables )
\r
717 // Using custom Replace() to guarantee the correct scope.
\r
718 attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ;
\r
723 GetFinalStyleValue : function()
\r
725 var attValue = this._GetStyleText() ;
\r
727 if ( attValue.length > 0 && this._Variables )
\r
729 // Using custom Replace() to guarantee the correct scope.
\r
730 attValue = attValue.Replace( FCKRegexLib.StyleVariableAttName, this._GetVariableReplace, this ) ;
\r
731 attValue = FCKTools.NormalizeCssText( attValue ) ;
\r
737 _GetVariableReplace : function()
\r
739 // The second group in the regex is the variable name.
\r
740 return this._Variables[ arguments[2] ] || arguments[0] ;
\r
744 * Set the value of a variable attribute or style, to be used when
\r
745 * appliying the style.
\r
747 SetVariable : function( name, value )
\r
749 var variables = this._Variables ;
\r
752 variables = this._Variables = {} ;
\r
754 this._Variables[ name ] = value ;
\r
758 * Converting from a PRE block to a non-PRE block in formatting operations.
\r
760 _FromPre : function( doc, block, newBlock )
\r
762 var innerHTML = block.innerHTML ;
\r
764 // Trim the first and last linebreaks immediately after and before <pre>, </pre>,
\r
766 // This is done because the linebreaks are not rendered.
\r
767 innerHTML = innerHTML.replace( /(\r\n|\r)/g, '\n' ) ;
\r
768 innerHTML = innerHTML.replace( /^[ \t]*\n/, '' ) ;
\r
769 innerHTML = innerHTML.replace( /\n$/, '' ) ;
\r
771 // 1. Convert spaces or tabs at the beginning or at the end to
\r
772 innerHTML = innerHTML.replace( /^[ \t]+|[ \t]+$/g, function( match, offset, s )
\r
774 if ( match.length == 1 ) // one space, preserve it
\r
776 else if ( offset == 0 ) // beginning of block
\r
777 return new Array( match.length ).join( ' ' ) + ' ' ;
\r
778 else // end of block
\r
779 return ' ' + new Array( match.length ).join( ' ' ) ;
\r
782 // 2. Convert \n to <BR>.
\r
783 // 3. Convert contiguous (i.e. non-singular) spaces or tabs to
\r
784 var htmlIterator = new FCKHtmlIterator( innerHTML ) ;
\r
786 htmlIterator.Each( function( isTag, value )
\r
790 value = value.replace( /\n/g, '<br>' ) ;
\r
791 value = value.replace( /[ \t]{2,}/g,
\r
794 return new Array( match.length ).join( ' ' ) + ' ' ;
\r
797 results.push( value ) ;
\r
799 newBlock.innerHTML = results.join( '' ) ;
\r
804 * Converting from a non-PRE block to a PRE block in formatting operations.
\r
806 _ToPre : function( doc, block, newBlock )
\r
808 // Handle converting from a regular block to a <pre> block.
\r
809 var innerHTML = block.innerHTML.Trim() ;
\r
811 // 1. Delete ANSI whitespaces immediately before and after <BR> because
\r
812 // they are not visible.
\r
813 // 2. Mark down any <BR /> nodes here so they can be turned into \n in
\r
814 // the next step and avoid being compressed.
\r
815 innerHTML = innerHTML.replace( /[ \t\r\n]*(<br[^>]*>)[ \t\r\n]*/gi, '<br />' ) ;
\r
817 // 3. Compress other ANSI whitespaces since they're only visible as one
\r
818 // single space previously.
\r
819 // 4. Convert to spaces since is no longer needed in <PRE>.
\r
820 // 5. Convert any <BR /> to \n. This must not be done earlier because
\r
821 // the \n would then get compressed.
\r
822 var htmlIterator = new FCKHtmlIterator( innerHTML ) ;
\r
824 htmlIterator.Each( function( isTag, value )
\r
827 value = value.replace( /([ \t\n\r]+| )/g, ' ' ) ;
\r
828 else if ( isTag && value == '<br />' )
\r
830 results.push( value ) ;
\r
833 // Assigning innerHTML to <PRE> in IE causes all linebreaks to be
\r
834 // reduced to spaces.
\r
835 // Assigning outerHTML to <PRE> in IE doesn't work if the <PRE> isn't
\r
836 // contained in another node since the node reference is changed after
\r
837 // outerHTML assignment.
\r
838 // So, we need some hacks to workaround IE bugs here.
\r
839 if ( FCKBrowserInfo.IsIE )
\r
841 var temp = doc.createElement( 'div' ) ;
\r
842 temp.appendChild( newBlock ) ;
\r
843 newBlock.outerHTML = '<pre>\n' + results.join( '' ) + '</pre>' ;
\r
844 newBlock = temp.removeChild( temp.firstChild ) ;
\r
847 newBlock.innerHTML = results.join( '' ) ;
\r
853 * Merge a <pre> block with a previous <pre> block, if available.
\r
855 _CheckAndMergePre : function( previousBlock, preBlock )
\r
857 // Check if the previous block and the current block are next
\r
859 if ( previousBlock != FCKDomTools.GetPreviousSourceElement( preBlock, true ) )
\r
862 // Merge the previous <pre> block contents into the current <pre>
\r
865 // Another thing to be careful here is that currentBlock might contain
\r
866 // a '\n' at the beginning, and previousBlock might contain a '\n'
\r
867 // towards the end. These new lines are not normally displayed but they
\r
868 // become visible after merging.
\r
869 var innerHTML = previousBlock.innerHTML.replace( /\n$/, '' ) + '\n\n' +
\r
870 preBlock.innerHTML.replace( /^\n/, '' ) ;
\r
872 // Buggy IE normalizes innerHTML from <pre>, breaking whitespaces.
\r
873 if ( FCKBrowserInfo.IsIE )
\r
874 preBlock.outerHTML = '<pre>' + innerHTML + '</pre>' ;
\r
876 preBlock.innerHTML = innerHTML ;
\r
878 // Remove the previous <pre> block.
\r
880 // The preBlock must not be moved or deleted from the DOM tree. This
\r
881 // guarantees the FCKDomRangeIterator in _ApplyBlockStyle would not
\r
882 // get lost at the next iteration.
\r
883 FCKDomTools.RemoveNode( previousBlock ) ;
\r
886 _CheckAndSplitPre : function( newBlock )
\r
890 var cursor = newBlock.firstChild ;
\r
892 // We are not splitting <br><br> at the beginning of the block, so
\r
893 // we'll start from the second child.
\r
894 cursor = cursor && cursor.nextSibling ;
\r
898 var next = cursor.nextSibling ;
\r
900 // If we have two <BR>s, and they're not at the beginning or the end,
\r
901 // then we'll split up the contents following them into another block.
\r
902 // Stop processing if we are at the last child couple.
\r
903 if ( next && next.nextSibling && cursor.nodeName.IEquals( 'br' ) && next.nodeName.IEquals( 'br' ) )
\r
905 // Remove the first <br>.
\r
906 FCKDomTools.RemoveNode( cursor ) ;
\r
908 // Move to the node after the second <br>.
\r
909 cursor = next.nextSibling ;
\r
911 // Remove the second <br>.
\r
912 FCKDomTools.RemoveNode( next ) ;
\r
914 // Create the block that will hold the child nodes from now on.
\r
915 lastNewBlock = FCKDomTools.InsertAfterNode( lastNewBlock || newBlock, FCKDomTools.CloneElement( newBlock ) ) ;
\r
920 // If we split it, then start moving the nodes to the new block.
\r
921 if ( lastNewBlock )
\r
923 cursor = cursor.previousSibling ;
\r
924 FCKDomTools.MoveNode(cursor.nextSibling, lastNewBlock ) ;
\r
927 cursor = cursor.nextSibling ;
\r
932 * Apply an inline style to a FCKDomRange.
\r
935 * - Implement the "#" style handling.
\r
936 * - Properly handle block containers like <div> and <blockquote>.
\r
938 _ApplyBlockStyle : function( range, selectIt, updateRange )
\r
940 // Bookmark the range so we can re-select it after processing.
\r
944 bookmark = range.CreateBookmark() ;
\r
946 var iterator = new FCKDomRangeIterator( range ) ;
\r
947 iterator.EnforceRealBlocks = true ;
\r
950 var doc = range.Window.document ;
\r
951 var previousPreBlock ;
\r
953 while( ( block = iterator.GetNextParagraph() ) ) // Only one =
\r
955 // Create the new node right before the current one.
\r
956 var newBlock = this.BuildElement( doc ) ;
\r
958 // Check if we are changing from/to <pre>.
\r
959 var newBlockIsPre = newBlock.nodeName.IEquals( 'pre' ) ;
\r
960 var blockIsPre = block.nodeName.IEquals( 'pre' ) ;
\r
962 var toPre = newBlockIsPre && !blockIsPre ;
\r
963 var fromPre = !newBlockIsPre && blockIsPre ;
\r
965 // Move everything from the current node to the new one.
\r
967 newBlock = this._ToPre( doc, block, newBlock ) ;
\r
968 else if ( fromPre )
\r
969 newBlock = this._FromPre( doc, block, newBlock ) ;
\r
970 else // Convering from a regular block to another regular block.
\r
971 FCKDomTools.MoveChildren( block, newBlock ) ;
\r
973 // Replace the current block.
\r
974 block.parentNode.insertBefore( newBlock, block ) ;
\r
975 FCKDomTools.RemoveNode( block ) ;
\r
977 // Complete other tasks after inserting the node in the DOM.
\r
978 if ( newBlockIsPre )
\r
980 if ( previousPreBlock )
\r
981 this._CheckAndMergePre( previousPreBlock, newBlock ) ; // Merge successive <pre> blocks.
\r
982 previousPreBlock = newBlock ;
\r
984 else if ( fromPre )
\r
985 this._CheckAndSplitPre( newBlock ) ; // Split <br><br> in successive <pre>s.
\r
988 // Re-select the original range.
\r
990 range.SelectBookmark( bookmark ) ;
\r
993 range.MoveToBookmark( bookmark ) ;
\r
997 * Apply an inline style to a FCKDomRange.
\r
1000 * - Merge elements, when applying styles to similar elements that enclose
\r
1001 * the entire selection, outputing:
\r
1002 * <span style="color: #ff0000; background-color: #ffffff">XYZ</span>
\r
1004 * <span style="color: #ff0000;"><span style="background-color: #ffffff">XYZ</span></span>
\r
1006 _ApplyInlineStyle : function( range, selectIt, updateRange )
\r
1008 var doc = range.Window.document ;
\r
1010 if ( range.CheckIsCollapsed() )
\r
1012 // Create the element to be inserted in the DOM.
\r
1013 var collapsedElement = this.BuildElement( doc ) ;
\r
1014 range.InsertNode( collapsedElement ) ;
\r
1015 range.MoveToPosition( collapsedElement, 2 ) ;
\r
1021 // The general idea here is navigating through all nodes inside the
\r
1022 // current selection, working on distinct range blocks, defined by the
\r
1023 // DTD compatibility between the style element and the nodes inside the
\r
1026 // For example, suppose we have the following selection (where [ and ]
\r
1027 // are the boundaries), and we apply a <b> style there:
\r
1029 // <p>Here we [have <b>some</b> text.<p>
\r
1030 // <p>And some here] here.</p>
\r
1032 // Two different ranges will be detected:
\r
1034 // "have <b>some</b> text."
\r
1035 // "And some here"
\r
1037 // Both ranges will be extracted, moved to a <b> element, and
\r
1038 // re-inserted, resulting in the following output:
\r
1040 // <p>Here we [<b>have some text.</b><p>
\r
1041 // <p><b>And some here</b>] here.</p>
\r
1043 // Note that the <b> element at <b>some</b> is also removed because it
\r
1044 // is not needed anymore.
\r
1046 var elementName = this.Element ;
\r
1048 // Get the DTD definition for the element. Defaults to "span".
\r
1049 var elementDTD = FCK.DTD[ elementName ] || FCK.DTD.span ;
\r
1051 // Create the attribute list to be used later for element comparisons.
\r
1052 var styleAttribs = this._GetAttribsForComparison() ;
\r
1055 // Expand the range, if inside inline element boundaries.
\r
1056 range.Expand( 'inline_elements' ) ;
\r
1058 // Bookmark the range so we can re-select it after processing.
\r
1059 var bookmark = range.CreateBookmark( true ) ;
\r
1061 // The style will be applied within the bookmark boundaries.
\r
1062 var startNode = range.GetBookmarkNode( bookmark, true ) ;
\r
1063 var endNode = range.GetBookmarkNode( bookmark, false ) ;
\r
1065 // We'll be reusing the range to apply the styles. So, release it here
\r
1066 // to indicate that it has not been initialized.
\r
1067 range.Release( true ) ;
\r
1069 // Let's start the nodes lookup from the node right after the bookmark
\r
1071 var currentNode = FCKDomTools.GetNextSourceNode( startNode, true ) ;
\r
1073 while ( currentNode )
\r
1075 var applyStyle = false ;
\r
1077 var nodeType = currentNode.nodeType ;
\r
1078 var nodeName = nodeType == 1 ? currentNode.nodeName.toLowerCase() : null ;
\r
1080 // Check if the current node can be a child of the style element.
\r
1081 if ( !nodeName || elementDTD[ nodeName ] )
\r
1083 // Check if the style element can be a child of the current
\r
1084 // node parent or if the element is not defined in the DTD.
\r
1085 if ( ( FCK.DTD[ currentNode.parentNode.nodeName.toLowerCase() ] || FCK.DTD.span )[ elementName ] || !FCK.DTD[ elementName ] )
\r
1087 // This node will be part of our range, so if it has not
\r
1088 // been started, place its start right before the node.
\r
1089 if ( !range.CheckHasRange() )
\r
1090 range.SetStart( currentNode, 3 ) ;
\r
1092 // Non element nodes, or empty elements can be added
\r
1093 // completely to the range.
\r
1094 if ( nodeType != 1 || currentNode.childNodes.length == 0 )
\r
1096 var includedNode = currentNode ;
\r
1097 var parentNode = includedNode.parentNode ;
\r
1099 // This node is about to be included completelly, but,
\r
1100 // if this is the last node in its parent, we must also
\r
1101 // check if the parent itself can be added completelly
\r
1103 while ( includedNode == parentNode.lastChild
\r
1104 && elementDTD[ parentNode.nodeName.toLowerCase() ] )
\r
1106 includedNode = parentNode ;
\r
1109 range.SetEnd( includedNode, 4 ) ;
\r
1111 // If the included node is the last node in its parent
\r
1112 // and its parent can't be inside the style node, apply
\r
1113 // the style immediately.
\r
1114 if ( includedNode == includedNode.parentNode.lastChild && !elementDTD[ includedNode.parentNode.nodeName.toLowerCase() ] )
\r
1115 applyStyle = true ;
\r
1119 // Element nodes will not be added directly. We need to
\r
1120 // check their children because the selection could end
\r
1121 // inside the node, so let's place the range end right
\r
1122 // before the element.
\r
1123 range.SetEnd( currentNode, 3 ) ;
\r
1127 applyStyle = true ;
\r
1130 applyStyle = true ;
\r
1132 // Get the next node to be processed.
\r
1133 currentNode = FCKDomTools.GetNextSourceNode( currentNode ) ;
\r
1135 // If we have reached the end of the selection, just apply the
\r
1136 // style ot the range, and stop looping.
\r
1137 if ( currentNode == endNode )
\r
1139 currentNode = null ;
\r
1140 applyStyle = true ;
\r
1143 // Apply the style if we have something to which apply it.
\r
1144 if ( applyStyle && range.CheckHasRange() && !range.CheckIsCollapsed() )
\r
1146 // Build the style element, based on the style object definition.
\r
1147 styleNode = this.BuildElement( doc ) ;
\r
1149 // Move the contents of the range to the style element.
\r
1150 range.ExtractContents().AppendTo( styleNode ) ;
\r
1152 // If it is not empty.
\r
1153 if ( styleNode.innerHTML.RTrim().length > 0 )
\r
1155 // Insert it in the range position (it is collapsed after
\r
1156 // ExtractContents.
\r
1157 range.InsertNode( styleNode ) ;
\r
1159 // Here we do some cleanup, removing all duplicated
\r
1160 // elements from the style element.
\r
1161 this.RemoveFromElement( styleNode ) ;
\r
1163 // Let's merge our new style with its neighbors, if possible.
\r
1164 this._MergeSiblings( styleNode, this._GetAttribsForComparison() ) ;
\r
1166 // As the style system breaks text nodes constantly, let's normalize
\r
1167 // things for performance.
\r
1168 // With IE, some paragraphs get broken when calling normalize()
\r
1169 // repeatedly. Also, for IE, we must normalize body, not documentElement.
\r
1170 // IE is also known for having a "crash effect" with normalize().
\r
1171 // We should try to normalize with IE too in some way, somewhere.
\r
1172 if ( !FCKBrowserInfo.IsIE )
\r
1173 styleNode.normalize() ;
\r
1176 // Style applied, let's release the range, so it gets marked to
\r
1177 // re-initialization in the next loop.
\r
1178 range.Release( true ) ;
\r
1182 this._FixBookmarkStart( startNode ) ;
\r
1184 // Re-select the original range.
\r
1186 range.SelectBookmark( bookmark ) ;
\r
1188 if ( updateRange )
\r
1189 range.MoveToBookmark( bookmark ) ;
\r
1192 _FixBookmarkStart : function( startNode )
\r
1194 // After appliying or removing an inline style, the start boundary of
\r
1195 // the selection must be placed inside all inline elements it is
\r
1197 var startSibling ;
\r
1198 while ( ( startSibling = startNode.nextSibling ) ) // Only one "=".
\r
1200 if ( startSibling.nodeType == 1
\r
1201 && FCKListsLib.InlineNonEmptyElements[ startSibling.nodeName.toLowerCase() ] )
\r
1203 // If it is an empty inline element, we can safely remove it.
\r
1204 if ( !startSibling.firstChild )
\r
1205 FCKDomTools.RemoveNode( startSibling ) ;
\r
1207 FCKDomTools.MoveNode( startNode, startSibling, true ) ;
\r
1211 // Empty text nodes can be safely removed to not disturb.
\r
1212 if ( startSibling.nodeType == 3 && startSibling.length == 0 )
\r
1214 FCKDomTools.RemoveNode( startSibling ) ;
\r
1223 * Merge an element with its similar siblings.
\r
1224 * "attribs" is and object computed with _CreateAttribsForComparison.
\r
1226 _MergeSiblings : function( element, attribs )
\r
1228 if ( !element || element.nodeType != 1 || !FCKListsLib.InlineNonEmptyElements[ element.nodeName.toLowerCase() ] )
\r
1231 this._MergeNextSibling( element, attribs ) ;
\r
1232 this._MergePreviousSibling( element, attribs ) ;
\r
1236 * Merge an element with its similar siblings after it.
\r
1237 * "attribs" is and object computed with _CreateAttribsForComparison.
\r
1239 _MergeNextSibling : function( element, attribs )
\r
1241 // Check the next sibling.
\r
1242 var sibling = element.nextSibling ;
\r
1244 // Check if the next sibling is a bookmark element. In this case, jump it.
\r
1245 var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ;
\r
1246 if ( hasBookmark )
\r
1247 sibling = sibling.nextSibling ;
\r
1249 if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName )
\r
1252 attribs = this._CreateElementAttribsForComparison( element ) ;
\r
1254 if ( this._CheckAttributesMatch( sibling, attribs ) )
\r
1256 // Save the last child to be checked too (to merge things like <b><i></i></b><b><i></i></b>).
\r
1257 var innerSibling = element.lastChild ;
\r
1259 if ( hasBookmark )
\r
1260 FCKDomTools.MoveNode( element.nextSibling, element ) ;
\r
1262 // Move contents from the sibling.
\r
1263 FCKDomTools.MoveChildren( sibling, element ) ;
\r
1264 FCKDomTools.RemoveNode( sibling ) ;
\r
1266 // Now check the last inner child (see two comments above).
\r
1267 if ( innerSibling )
\r
1268 this._MergeNextSibling( innerSibling ) ;
\r
1274 * Merge an element with its similar siblings before it.
\r
1275 * "attribs" is and object computed with _CreateAttribsForComparison.
\r
1277 _MergePreviousSibling : function( element, attribs )
\r
1279 // Check the previous sibling.
\r
1280 var sibling = element.previousSibling ;
\r
1282 // Check if the previous sibling is a bookmark element. In this case, jump it.
\r
1283 var hasBookmark = ( sibling && sibling.nodeType == 1 && sibling.getAttribute( '_fck_bookmark' ) ) ;
\r
1284 if ( hasBookmark )
\r
1285 sibling = sibling.previousSibling ;
\r
1287 if ( sibling && sibling.nodeType == 1 && sibling.nodeName == element.nodeName )
\r
1290 attribs = this._CreateElementAttribsForComparison( element ) ;
\r
1292 if ( this._CheckAttributesMatch( sibling, attribs ) )
\r
1294 // Save the first child to be checked too (to merge things like <b><i></i></b><b><i></i></b>).
\r
1295 var innerSibling = element.firstChild ;
\r
1297 if ( hasBookmark )
\r
1298 FCKDomTools.MoveNode( element.previousSibling, element, true ) ;
\r
1300 // Move contents to the sibling.
\r
1301 FCKDomTools.MoveChildren( sibling, element, true ) ;
\r
1302 FCKDomTools.RemoveNode( sibling ) ;
\r
1304 // Now check the first inner child (see two comments above).
\r
1305 if ( innerSibling )
\r
1306 this._MergePreviousSibling( innerSibling ) ;
\r
1312 * Build the cssText based on the styles definition.
\r
1314 _GetStyleText : function()
\r
1316 var stylesDef = this._StyleDesc.Styles ;
\r
1318 // Builds the StyleText.
\r
1319 var stylesText = ( this._StyleDesc.Attributes ? this._StyleDesc.Attributes['style'] || '' : '' ) ;
\r
1321 if ( stylesText.length > 0 )
\r
1322 stylesText += ';' ;
\r
1324 for ( var style in stylesDef )
\r
1325 stylesText += style + ':' + stylesDef[style] + ';' ;
\r
1327 // Browsers make some changes to the style when applying them. So, here
\r
1328 // we normalize it to the browser format. We'll not do that if there
\r
1329 // are variables inside the style.
\r
1330 if ( stylesText.length > 0 && !( /#\(/.test( stylesText ) ) )
\r
1332 stylesText = FCKTools.NormalizeCssText( stylesText ) ;
\r
1335 return (this._GetStyleText = function() { return stylesText ; })() ;
\r
1339 * Get the the collection used to compare the attributes defined in this
\r
1340 * style with attributes in an element. All information in it is lowercased.
\r
1342 _GetAttribsForComparison : function()
\r
1344 // If we have already computed it, just return it.
\r
1345 var attribs = this._GetAttribsForComparison_$ ;
\r
1349 attribs = new Object() ;
\r
1351 // Loop through all defined attributes.
\r
1352 var styleAttribs = this._StyleDesc.Attributes ;
\r
1353 if ( styleAttribs )
\r
1355 for ( var styleAtt in styleAttribs )
\r
1357 attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase() ;
\r
1361 // Includes the style definitions.
\r
1362 if ( this._GetStyleText().length > 0 )
\r
1364 attribs['style'] = this._GetStyleText().toLowerCase() ;
\r
1367 // Appends the "length" information to the object.
\r
1368 FCKTools.AppendLengthProperty( attribs, '_length' ) ;
\r
1370 // Return it, saving it to the next request.
\r
1371 return ( this._GetAttribsForComparison_$ = attribs ) ;
\r
1375 * Get the the collection used to compare the elements and attributes,
\r
1376 * defined in this style overrides, with other element. All information in
\r
1377 * it is lowercased.
\r
1379 _GetOverridesForComparison : function()
\r
1381 // If we have already computed it, just return it.
\r
1382 var overrides = this._GetOverridesForComparison_$ ;
\r
1384 return overrides ;
\r
1386 overrides = new Object() ;
\r
1388 var overridesDesc = this._StyleDesc.Overrides ;
\r
1390 if ( overridesDesc )
\r
1392 // The override description can be a string, object or array.
\r
1393 // Internally, well handle arrays only, so transform it if needed.
\r
1394 if ( !FCKTools.IsArray( overridesDesc ) )
\r
1395 overridesDesc = [ overridesDesc ] ;
\r
1397 // Loop through all override definitions.
\r
1398 for ( var i = 0 ; i < overridesDesc.length ; i++ )
\r
1400 var override = overridesDesc[i] ;
\r
1405 // If can be a string with the element name.
\r
1406 if ( typeof override == 'string' )
\r
1407 elementName = override.toLowerCase() ;
\r
1411 elementName = override.Element ? override.Element.toLowerCase() : this.Element ;
\r
1412 attrs = override.Attributes ;
\r
1415 // We can have more than one override definition for the same
\r
1416 // element name, so we attempt to simply append information to
\r
1417 // it if it already exists.
\r
1418 overrideEl = overrides[ elementName ] || ( overrides[ elementName ] = {} ) ;
\r
1422 // The returning attributes list is an array, because we
\r
1423 // could have different override definitions for the same
\r
1424 // attribute name.
\r
1425 var overrideAttrs = ( overrideEl.Attributes = overrideEl.Attributes || new Array() ) ;
\r
1426 for ( var attName in attrs )
\r
1428 // Each item in the attributes array is also an array,
\r
1429 // where [0] is the attribute name and [1] is the
\r
1430 // override value.
\r
1431 overrideAttrs.push( [ attName.toLowerCase(), attrs[ attName ] ] ) ;
\r
1437 return ( this._GetOverridesForComparison_$ = overrides ) ;
\r
1441 * Create and object containing all attributes specified in an element,
\r
1442 * added by a "_length" property. All values are lowercased.
\r
1444 _CreateElementAttribsForComparison : function( element )
\r
1446 var attribs = new Object() ;
\r
1447 var attribsCount = 0 ;
\r
1449 for ( var i = 0 ; i < element.attributes.length ; i++ )
\r
1451 var att = element.attributes[i] ;
\r
1453 if ( att.specified )
\r
1455 attribs[ att.nodeName.toLowerCase() ] = FCKDomTools.GetAttributeValue( element, att ).toLowerCase() ;
\r
1460 attribs._length = attribsCount ;
\r
1466 * Checks is the element attributes have a perfect match with the style
\r
1469 _CheckAttributesMatch : function( element, styleAttribs )
\r
1471 // Loop through all specified attributes. The same number of
\r
1472 // attributes must be found and their values must match to
\r
1473 // declare them as equal.
\r
1475 var elementAttrbs = element.attributes ;
\r
1476 var matchCount = 0 ;
\r
1478 for ( var i = 0 ; i < elementAttrbs.length ; i++ )
\r
1480 var att = elementAttrbs[i] ;
\r
1481 if ( att.specified )
\r
1483 var attName = att.nodeName.toLowerCase() ;
\r
1484 var styleAtt = styleAttribs[ attName ] ;
\r
1486 // The attribute is not defined in the style.
\r
1490 // The values are different.
\r
1491 if ( styleAtt != FCKDomTools.GetAttributeValue( element, att ).toLowerCase() )
\r
1498 return ( matchCount == styleAttribs._length ) ;
\r