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