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