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 CKEDITOR.style = function( styleDefinition ) 84 { 85 var element = this.element = ( styleDefinition.element || '*' ).toLowerCase(); 86 87 this.type = 88 ( element == '#' || blockElements[ element ] ) ? 89 CKEDITOR.STYLE_BLOCK 90 : objectElements[ element ] ? 91 CKEDITOR.STYLE_OBJECT 92 : 93 CKEDITOR.STYLE_INLINE; 94 95 this._ = 96 { 97 definition : styleDefinition 98 }; 99 }; 100 101 CKEDITOR.style.prototype = 102 { 103 apply : function( document ) 104 { 105 // Get all ranges from the selection. 106 var selection = document.getSelection(); 107 var ranges = selection.getRanges(); 108 109 // Apply the style to the ranges. 110 for ( var i = 0 ; i < ranges.length ; i++ ) 111 this.applyToRange( ranges[ i ] ); 112 113 // Select the ranges again. 114 selection.selectRanges( ranges ); 115 }, 116 117 applyToRange : function( range ) 118 { 119 return ( this.applyToRange = 120 this.type == CKEDITOR.STYLE_INLINE ? 121 applyInlineStyle 122 : this.type == CKEDITOR.STYLE_BLOCK ? 123 applyBlockStyle 124 : null ).call( this, range ); 125 }, 126 127 /** 128 * Get the style state inside an element path. Returns "true" if the 129 * element is active in the path. 130 */ 131 checkActive : function( elementPath ) 132 { 133 switch ( this.type ) 134 { 135 case CKEDITOR.STYLE_BLOCK : 136 return this.checkElementRemovable( elementPath.block || elementPath.blockLimit, true ); 137 138 case CKEDITOR.STYLE_INLINE : 139 140 var elements = elementPath.elements; 141 142 for ( var i = 0, element ; i < elements.length ; i++ ) 143 { 144 element = elements[i]; 145 146 if ( element == elementPath.block || element == elementPath.blockLimit ) 147 continue; 148 149 if ( this.checkElementRemovable( element, true ) ) 150 return true; 151 } 152 } 153 return false; 154 }, 155 156 // Checks if an element, or any of its attributes, is removable by the 157 // current style definition. 158 checkElementRemovable : function( element, fullMatch ) 159 { 160 if ( !element || element.getName() != this.element ) 161 return false ; 162 163 var def = this._.definition; 164 var attribs = def.attributes; 165 var styles = def.styles; 166 167 // If no attributes are defined in the element. 168 if ( !fullMatch && !element.hasAttributes() ) 169 return true ; 170 171 for ( var attName in attribs ) 172 { 173 if ( element.getAttribute( attName ) == attribs[ attName ] ) 174 { 175 if ( !fullMatch ) 176 return true; 177 } 178 else if ( fullMatch ) 179 return false; 180 } 181 182 return true; 183 }, 184 185 /** 186 * Sets the value of a variable attribute or style, to be used when 187 * appliying the style. This function must be called before using any 188 * other function in this object. 189 */ 190 setVariable : function( name, value ) 191 { 192 var variables = this._.variables || ( this._variables = {} ); 193 variables[ name ] = value; 194 } 195 }; 196 197 var applyInlineStyle = function( range ) 198 { 199 var document = range.document; 200 201 if ( range.collapsed ) 202 { 203 // Create the element to be inserted in the DOM. 204 var collapsedElement = getElement( this, document ); 205 206 // Insert the empty element into the DOM at the range position. 207 range.insertNode( collapsedElement ); 208 209 // Place the selection right inside the empty element. 210 range.moveToPosition( collapsedElement, CKEDITOR.POSITION_BEFORE_END ); 211 212 return; 213 } 214 215 var elementName = this.element; 216 var def = this._.definition; 217 var isUnknownElement; 218 219 // Get the DTD definition for the element. Defaults to "span". 220 var dtd = CKEDITOR.dtd[ elementName ] || ( isUnknownElement = true, CKEDITOR.dtd.span ); 221 222 // Bookmark the range so we can re-select it after processing. 223 var bookmark = range.createBookmark(); 224 225 // Expand the range. 226 range.enlarge( CKEDITOR.ENLARGE_ELEMENT ); 227 range.trim(); 228 229 // Get the first node to be processed and the last, which concludes the 230 // processing. 231 var firstNode = range.startContainer.getChild( range.startOffset ) || range.startContainer.getNextSourceNode(); 232 var lastNode = range.endContainer.getChild( range.endOffset ) || ( range.endOffset ? range.endContainer.getNextSourceNode() : range.endContainer ); 233 234 var currentNode = firstNode; 235 236 var styleRange; 237 238 // Indicates that that some useful inline content has been found, so 239 // the style should be applied. 240 var hasContents; 241 242 while ( currentNode ) 243 { 244 var applyStyle = false; 245 246 if ( currentNode.equals( lastNode ) ) 247 { 248 currentNode = null; 249 applyStyle = true; 250 } 251 else 252 { 253 var nodeType = currentNode.type; 254 var nodeName = nodeType == CKEDITOR.NODE_ELEMENT ? currentNode.getName() : null; 255 256 if ( nodeName && currentNode.getAttribute( '_fck_bookmark' ) ) 257 { 258 currentNode = currentNode.getNextSourceNode( true ); 259 continue; 260 } 261 262 // Check if the current node can be a child of the style element. 263 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 ) ) ) 264 { 265 var currentParent = currentNode.getParent(); 266 267 // Check if the style element can be a child of the current 268 // node parent or if the element is not defined in the DTD. 269 if ( currentParent && ( ( currentParent.getDtd() || CKEDITOR.dtd.span )[ elementName ] || isUnknownElement ) ) 270 { 271 // This node will be part of our range, so if it has not 272 // been started, place its start right before the node. 273 // In the case of an element node, it will be included 274 // only if it is entirely inside the range. 275 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 ) ) ) 276 { 277 styleRange = new CKEDITOR.dom.range( document ); 278 styleRange.setStartBefore( currentNode ); 279 } 280 281 // Non element nodes, or empty elements can be added 282 // completely to the range. 283 if ( nodeType == CKEDITOR.NODE_TEXT || ( nodeType == CKEDITOR.NODE_ELEMENT && !currentNode.getChildCount() && currentNode.$.offsetWidth ) ) 284 { 285 var includedNode = currentNode; 286 var parentNode; 287 288 // This node is about to be included completelly, but, 289 // if this is the last node in its parent, we must also 290 // check if the parent itself can be added completelly 291 // to the range. 292 while ( !includedNode.$.nextSibling 293 && ( parentNode = includedNode.getParent(), dtd[ parentNode.getName() ] ) 294 && ( parentNode.getPosition( firstNode ) | CKEDITOR.POSITION_FOLLOWING | CKEDITOR.POSITION_IDENTICAL | CKEDITOR.POSITION_IS_CONTAINED ) == ( CKEDITOR.POSITION_FOLLOWING + CKEDITOR.POSITION_IDENTICAL + CKEDITOR.POSITION_IS_CONTAINED ) ) 295 { 296 includedNode = parentNode; 297 } 298 299 styleRange.setEndAfter( includedNode ); 300 301 // If the included node still is the last node in its 302 // parent, it means that the parent can't be included 303 // in this style DTD, so apply the style immediately. 304 if ( !includedNode.$.nextSibling ) 305 applyStyle = true; 306 307 if ( !hasContents ) 308 hasContents = ( nodeType != CKEDITOR.NODE_TEXT || (/[^\s\ufeff]/).test( currentNode.getText() ) ); 309 } 310 } 311 else 312 applyStyle = true; 313 } 314 else 315 applyStyle = true; 316 317 // Get the next node to be processed. 318 currentNode = currentNode.getNextSourceNode(); 319 } 320 321 // Apply the style if we have something to which apply it. 322 if ( applyStyle && hasContents && styleRange && !styleRange.collapsed ) 323 { 324 // Build the style element, based on the style object definition. 325 var styleNode = getElement( this, document ); 326 327 var parent = styleRange.getCommonAncestor(); 328 329 while ( styleNode && parent ) 330 { 331 if ( parent.getName() == elementName ) 332 { 333 for ( var attName in def.attribs ) 334 { 335 if ( styleNode.getAttribute( attName ) == parent.getAttribute( attName ) ) 336 styleNode.removeAttribute( attName ); 337 } 338 339 for ( var styleName in def.styles ) 340 { 341 if ( styleNode.getStyle( styleName ) == parent.getStyle( styleName ) ) 342 styleNode.removeStyle( styleName ); 343 } 344 345 if ( !styleNode.hasAttributes() ) 346 { 347 styleNode = null; 348 break; 349 } 350 } 351 352 parent = parent.getParent(); 353 } 354 355 if ( styleNode ) 356 { 357 // Move the contents of the range to the style element. 358 styleRange.extractContents().appendTo( styleNode ); 359 360 // Here we do some cleanup, removing all duplicated 361 // elements from the style element. 362 removeFromElement( this, styleNode ); 363 364 // Insert it into the range position (it is collapsed after 365 // extractContents. 366 styleRange.insertNode( styleNode ); 367 368 // Let's merge our new style with its neighbors, if possible. 369 mergeSiblings( styleNode ); 370 371 // As the style system breaks text nodes constantly, let's normalize 372 // things for performance. 373 // With IE, some paragraphs get broken when calling normalize() 374 // repeatedly. Also, for IE, we must normalize body, not documentElement. 375 // IE is also known for having a "crash effect" with normalize(). 376 // We should try to normalize with IE too in some way, somewhere. 377 if ( !CKEDITOR.env.ie ) 378 styleNode.$.normalize(); 379 } 380 381 // Style applied, let's release the range, so it gets 382 // re-initialization in the next loop. 383 styleRange = null; 384 } 385 } 386 387 // this._FixBookmarkStart( startNode ); 388 389 range.moveToBookmark( bookmark ); 390 }; 391 392 var applyBlockStyle = function( range ) 393 { 394 }; 395 396 // Removes a style from inside an element. 397 var removeFromElement = function( style, element ) 398 { 399 var def = style._.definition; 400 var attribs = def.attributes; 401 var styles = def.styles; 402 403 var innerElements = element.getElementsByTag( style.element ); 404 405 for ( var i = innerElements.count() ; --i >= 0 ; ) 406 { 407 var innerElement = innerElements.getItem( i ); 408 409 for ( var attName in attribs ) 410 { 411 // The 'class' element value must match (#1318). 412 if ( attName == 'class' && innerElement.getAttribute( 'class' ) != attribs[ attName ] ) 413 continue; 414 415 innerElement.removeAttribute( attName ); 416 } 417 418 for ( var styleName in styles ) 419 { 420 innerElement.removeStyle( styleName ); 421 } 422 423 removeNoAttribsElement( innerElement ); 424 } 425 }; 426 427 // If the element has no more attributes, remove it. 428 var removeNoAttribsElement = function( element ) 429 { 430 // If no more attributes remained in the element, remove it, 431 // leaving its children. 432 if ( !element.hasAttributes() ) 433 { 434 // Removing elements may open points where merging is possible, 435 // so let's cache the first and last nodes for later checking. 436 var firstChild = element.getFirst(); 437 var lastChild = element.getLast(); 438 439 element.remove( true ); 440 441 if ( firstChild ) 442 { 443 // Check the cached nodes for merging. 444 mergeSiblings( firstChild ); 445 446 if ( lastChild && !firstChild.equals( lastChild ) ) 447 mergeSiblings( lastChild ); 448 } 449 } 450 }; 451 452 // Get the the collection used to compare the attributes defined in this 453 // style with attributes in an element. All information in it is lowercased. 454 // V2 455 // var getAttribsForComparison = function( style ) 456 // { 457 // // If we have already computed it, just return it. 458 // var attribs = style._.attribsForComparison; 459 // if ( attribs ) 460 // return attribs; 461 462 // attribs = {}; 463 464 // var def = style._.definition; 465 466 // // Loop through all defined attributes. 467 // var styleAttribs = def.attributes; 468 // if ( styleAttribs ) 469 // { 470 // for ( var styleAtt in styleAttribs ) 471 // { 472 // attribs[ styleAtt.toLowerCase() ] = styleAttribs[ styleAtt ].toLowerCase(); 473 // } 474 // } 475 476 // // Includes the style definitions. 477 // if ( this._GetStyleText().length > 0 ) 478 // { 479 // attribs['style'] = this._GetStyleText().toLowerCase(); 480 // } 481 482 // // Appends the "length" information to the object. 483 // FCKTools.AppendLengthProperty( attribs, '_length' ); 484 485 // // Return it, saving it to the next request. 486 // return ( this._GetAttribsForComparison_$ = attribs ); 487 // }, 488 489 var mergeSiblings = function( element ) 490 { 491 if ( !element || element.type != CKEDITOR.NODE_ELEMENT || !CKEDITOR.dtd.$removeEmpty[ element.getName() ] ) 492 return; 493 494 mergeElements( element, element.getNext(), true ); 495 mergeElements( element, element.getPrevious() ); 496 }; 497 498 var mergeElements = function( element, sibling, isNext ) 499 { 500 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT ) 501 { 502 var hasBookmark = sibling.getAttribute( '_fck_bookmark' ); 503 504 if ( hasBookmark ) 505 sibling = isNext ? sibling.getNext() : sibling.getPrevious(); 506 507 if ( sibling && sibling.type == CKEDITOR.NODE_ELEMENT && sibling.getName() == element.getName() ) 508 { 509 // Save the last child to be checked too, to merge things like 510 // <b><i></i></b><b><i></i></b> => <b><i></i></b> 511 var innerSibling = isNext ? element.getLast() : element.getFirst(); 512 513 if ( hasBookmark ) 514 ( isNext ? sibling.getPrevious() : sibling.getNext() ).move( element, !isNext ); 515 516 sibling.moveChildren( element, !isNext ); 517 sibling.remove(); 518 519 // Now check the last inner child (see two comments above). 520 if ( innerSibling ) 521 mergeSiblings( innerSibling ); 522 } 523 } 524 }; 525 526 // Regex used to match all variables defined in an attribute or style 527 // value. The variable name is returned with $2. 528 var styleVariableAttNameRegex = /#\(\s*("|')(.+?)\1[^\)]*\s*\)/g; 529 530 var getElement = function( style, targetDocument ) 531 { 532 var el = style._.element; 533 534 if ( el ) 535 return el.clone(); 536 537 var def = style._.definition; 538 var variables = style._.variables; 539 540 var elementName = style.element; 541 var attributes = def.attributes; 542 var styles = def.styles; 543 544 // The "*" element name will always be a span for this function. 545 if ( elementName == '*' ) 546 elementName = 'span'; 547 548 // Create the element. 549 el = new CKEDITOR.dom.element( elementName, targetDocument ); 550 551 // Assign all defined attributes. 552 if ( attributes ) 553 { 554 for ( var att in attributes ) 555 { 556 var attValue = attributes[ att ]; 557 if ( attValue && variables ) 558 { 559 attValue = attValue.replace( styleVariableAttNameRegex, function() 560 { 561 // The second group in the regex is the variable name. 562 return variables[ arguments[2] ] || arguments[0]; 563 }); 564 } 565 el.setAttribute( att, attValue ); 566 } 567 } 568 569 // Assign all defined styles. 570 if ( styles ) 571 { 572 for ( var styleName in styles ) 573 el.setStyle( styleName, styles[ styleName ] ); 574 575 if ( variables ) 576 { 577 attValue = el.getAttribute( 'style' ).replace( styleVariableAttNameRegex, function() 578 { 579 // The second group in the regex is the variable name. 580 return variables[ arguments[2] ] || arguments[0]; 581 }); 582 el.setAttribute( 'style', attValue ); 583 } 584 } 585 586 // Save the created element. It will be reused on future calls. 587 return ( style._.element = el ); 588 }; 589 })(); 590 591 CKEDITOR.styleCommand = function( style ) 592 { 593 this.style = style; 594 }; 595 596 CKEDITOR.styleCommand.prototype.exec = function( editor ) 597 { 598 editor.focus(); 599 600 var doc = editor.document; 601 602 if ( doc ) 603 this.style.apply( doc ); 604 605 return !!doc; 606 }; 607