      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 /**
      7  * @file Blockquote.
      8  */
      9
     10 (function()
     11 {
     12 	function getState( editor, path )
     13 	{
     14 		var firstBlock = path.block || path.blockLimit;
     15
     16 		if ( !firstBlock || firstBlock.getName() == 'body' )
     17 			return CKEDITOR.TRISTATE_OFF;
     18
     19 		// See if the first block has a blockquote parent.
     20 		if ( firstBlock.getAscendant( 'blockquote', true ) )
     21 			return CKEDITOR.TRISTATE_ON;
     22
     23 		return CKEDITOR.TRISTATE_OFF;
     24 	}
     25
     26 	function onSelectionChange( evt )
     27 	{
     28 		var editor = evt.editor,
     29 			command = editor.getCommand( 'blockquote' );
     30 		command.state = getState( editor, evt.data.path );
     31 		command.fire( 'state' );
     32 	}
     33
     34 	function noBlockLeft( bqBlock )
     35 	{
     36 		for ( var i = 0, length = bqBlock.getChildCount(), child ; i < length && ( child = bqBlock.getChild( i ) ) ; i++ )
     37 		{
     38 			if ( child.type == CKEDITOR.NODE_ELEMENT && child.isBlockBoundary() )
     39 				return false;
     40 		}
     41 		return true;
     42 	}
     43
     44 	var commandObject =
     45 	{
     46 		exec : function( editor )
     47 		{
     48 			var state = editor.getCommand( 'blockquote' ).state,
     49 				selection = editor.getSelection(),
     50 				range = selection && selection.getRanges()[0];
     51
     52 			if ( !range )
     53 				return;
     54
     55 			var bookmarks = selection.createBookmarks();
     56
     57 			// Kludge for #1592: if the bookmark nodes are in the beginning of
     58 			// blockquote, then move them to the nearest block element in the
     59 			// blockquote.
     60 			if ( CKEDITOR.env.ie )
     61 			{
     62 				var bookmarkStart = bookmarks[0].startNode,
     63 					bookmarkEnd = bookmarks[0].endNode,
     64 					cursor;
     65
     66 				if ( bookmarkStart && bookmarkStart.getParent().getName() == 'blockquote' )
     67 				{
     68 					cursor = bookmarkStart;
     69 					while ( ( cursor = cursor.getNext() ) )
     70 					{
     71 						if ( cursor.type == CKEDITOR.NODE_ELEMENT &&
     72 								cursor.isBlockBoundary() )
     73 						{
     74 							bookmarkStart.move( cursor, true );
     75 							break;
     76 						}
     77 					}
     78 				}
     79
     80 				if ( bookmarkEnd
     81 						&& bookmarkEnd.getParent().getName() == 'blockquote' )
     82 				{
     83 					cursor = bookmarkEnd;
     84 					while ( ( cursor = cursor.getPrevious() ) )
     85 					{
     86 						if ( cursor.type == CKEDITOR.NODE_ELEMENT &&
     87 								cursor.isBlockBoundary() )
     88 						{
     89 							bookmarkEnd.move( cursor );
     90 							break;
     91 						}
     92 					}
     93 				}
     94 			}
     95
     96 			var iterator = range.createIterator(),
     97 				block;
     98
     99 			if ( state == CKEDITOR.TRISTATE_OFF )
    100 			{
    101 				var paragraphs = [];
    102 				while ( ( block = iterator.getNextParagraph() ) )
    103 					paragraphs.push( block );
    104
    105 				// If no paragraphs, create one from the current selection position.
    106 				if ( paragraphs.length < 1 )
    107 				{
    108 					var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ),
    109 						firstBookmark = bookmarks.shift();
    110 					range.insertNode( para );
    111 					para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) );
    112 					range.moveToBookmark( firstBookmark );
    113 					range.selectNodeContents( para );
    114 					range.collapse( true );
    115 					firstBookmark = range.createBookmark();
    116 					paragraphs.push( para );
    117 					bookmarks.unshift( firstBookmark );
    118 				}
    119
    120 				// Make sure all paragraphs have the same parent.
    121 				var commonParent = paragraphs[0].getParent(),
    122 					tmp = [];
    123 				for ( var i = 0 ; i < paragraphs.length ; i++ )
    124 				{
    125 					block = paragraphs[i];
    126 					commonParent = commonParent.getCommonAncestor( block.getParent() );
    127 				}
    128
    129 				// The common parent must not be the following tags: table, tbody, tr, ol, ul.
    130 				var denyTags = { table : 1, tbody : 1, tr : 1, ol : 1, ul : 1 };
    131 				while ( denyTags[ commonParent.getName() ] )
    132 					commonParent = commonParent.getParent();
    133
    134 				// Reconstruct the block list to be processed such that all resulting blocks
    135 				// satisfy parentNode.equals( commonParent ).
    136 				var lastBlock = null;
    137 				while ( paragraphs.length > 0 )
    138 				{
    139 					block = paragraphs.shift();
    140 					while ( !block.getParent().equals( commonParent ) )
    141 						block = block.getParent();
    142 					if ( !block.equals( lastBlock ) )
    143 						tmp.push( block );
    144 					lastBlock = block;
    145 				}
    146
    147 				// If any of the selected blocks is a blockquote, remove it to prevent
    148 				// nested blockquotes.
    149 				while ( tmp.length > 0 )
    150 				{
    151 					block = tmp.shift();
    152 					if ( block.getName() == 'blockquote' )
    153 					{
    154 						var docFrag = new CKEDITOR.dom.documentFragment( editor.document );
    155 						while ( block.getFirst() )
    156 						{
    157 							docFrag.append( block.getFirst().remove() );
    158 							paragraphs.push( docFrag.getLast() );
    159 						}
    160
    161 						docFrag.replace( block );
    162 					}
    163 					else
    164 						paragraphs.push( block );
    165 				}
    166
    167 				// Now we have all the blocks to be included in a new blockquote node.
    168 				var bqBlock = editor.document.createElement( 'blockquote' );
    169 				bqBlock.insertBefore( paragraphs[0] );
    170 				while ( paragraphs.length > 0 )
    171 				{
    172 					block = paragraphs.shift();
    173 					bqBlock.append( block );
    174 				}
    175 			}
    176 			else if ( state == CKEDITOR.TRISTATE_ON )
    177 			{
    178 				var moveOutNodes = [],
    179 					database = {};
    180
    181 				while ( ( block = iterator.getNextParagraph() ) )
    182 				{
    183 					var bqParent = null,
    184 						bqChild = null;
    185 					while ( block.getParent() )
    186 					{
    187 						if ( block.getParent().getName() == 'blockquote' )
    188 						{
    189 							bqParent = block.getParent();
    190 							bqChild = block;
    191 							break;
    192 						}
    193 						block = block.getParent();
    194 					}
    195
    196 					// Remember the blocks that were recorded down in the moveOutNodes array
    197 					// to prevent duplicates.
    198 					if ( bqParent && bqChild && !bqChild.getCustomData( 'blockquote_moveout' ) )
    199 					{
    200 						moveOutNodes.push( bqChild );
    201 						CKEDITOR.dom.element.setMarker( database, bqChild, 'blockquote_moveout', true );
    202 					}
    203 				}
    204
    205 				CKEDITOR.dom.element.clearAllMarkers( database );
    206
    207 				var movedNodes = [],
    208 					processedBlockquoteBlocks = [];
    209
    210 				database = {};
    211 				while ( moveOutNodes.length > 0 )
    212 				{
    213 					var node = moveOutNodes.shift();
    214 					bqBlock = node.getParent();
    215
    216 					// If the node is located at the beginning or the end, just take it out
    217 					// without splitting. Otherwise, split the blockquote node and move the
    218 					// paragraph in between the two blockquote nodes.
    219 					if ( !node.getPrevious() )
    220 						node.remove().insertBefore( bqBlock );
    221 					else if ( !node.getNext() )
    222 						node.remove().insertAfter( bqBlock );
    223 					else
    224 					{
    225 						node.breakParent( node.getParent() );
    226 						processedBlockquoteBlocks.push( node.getNext() );
    227 					}
    228
    229 					// Remember the blockquote node so we can clear it later (if it becomes empty).
    230 					if ( !bqBlock.getCustomData( 'blockquote_processed' ) )
    231 					{
    232 						processedBlockquoteBlocks.push( bqBlock );
    233 						CKEDITOR.dom.element.setMarker( database, bqBlock, 'blockquote_processed', true );
    234 					}
    235
    236 					movedNodes.push( node );
    237 				}
    238
    239 				CKEDITOR.dom.element.clearAllMarkers( database );
    240
    241 				// Clear blockquote nodes that have become empty.
    242 				for ( i = processedBlockquoteBlocks.length - 1 ; i >= 0 ; i-- )
    243 				{
    244 					bqBlock = processedBlockquoteBlocks[i];
    245 					if ( noBlockLeft( bqBlock ) )
    246 						bqBlock.remove();
    247 				}
    248
    249 				if ( editor.config.enterMode == CKEDITOR.ENTER_BR )
    250 				{
    251 					var firstTime = true;
    252 					while ( movedNodes.length )
    253 					{
    254 						node = movedNodes.shift();
    255
    256 						if ( node.getName() == 'div' )
    257 						{
    258 							docFrag = new CKEDITOR.dom.documentFragment( editor.document );
    259 							var needBeginBr = firstTime && node.getPrevious() &&
    260 									!( node.getPrevious().type == CKEDITOR.NODE_ELEMENT && node.getPrevious().isBlockBoundary() );
    261 							if ( needBeginBr )
    262 								docFrag.append( editor.document.createElement( 'br' ) );
    263
    264 							var needEndBr = node.getNext() &&
    265 								!( node.getNext().type == CKEDITOR.NODE_ELEMENT && node.getNext().isBlockBoundary() );
    266 							while ( node.getFirst() )
    267 								node.getFirst().remove().appendTo( docFrag );
    268
    269 							if ( needEndBr )
    270 								docFrag.append( editor.document.createElement( 'br' ) );
    271
    272 							docFrag.replace( node );
    273 							firstTime = false;
    274 						}
    275 					}
    276 				}
    277 			}
    278
    279 			selection.selectBookmarks( bookmarks );
    280 			editor.focus();
    281 		}
    282 	};
    283
    284 	CKEDITOR.plugins.add( 'blockquote',
    285 	{
    286 		init : function( editor )
    287 		{
    288 			editor.addCommand( 'blockquote', commandObject );
    289
    290 			editor.ui.addButton( 'Blockquote',
    291 				{
    292 					label : editor.lang.blockquote,
    293 					command : 'blockquote'
    294 				} );
    295
    296 			editor.on( 'selectionChange', onSelectionChange );
    297 		},
    298
    299 		requires : [ 'domiterator' ]
    300 	} );
    301 })();
    302