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 					database = {};
210 				while ( moveOutNodes.length > 0 )
211 				{
212 					var node = moveOutNodes.shift(),
213 						bqBlock = node.getParent();
214
215 					// If the node is located at the beginning or the end, just take it out
216 					// without splitting. Otherwise, split the blockquote node and move the
217 					// paragraph in between the two blockquote nodes.
218 					if ( !node.getPrevious() )
219 						node.remove().insertBefore( bqBlock );
220 					else if ( !node.getNext() )
221 						node.remove().insertAfter( bqBlock );
222 					else
223 					{
224 						node.breakParent( node.getParent() );
225 						processedBlockquoteBlocks.push( node.getNext() );
226 					}
227
228 					// Remember the blockquote node so we can clear it later (if it becomes empty).
229 					if ( !bqBlock.getCustomData( 'blockquote_processed' ) )
230 					{
231 						processedBlockquoteBlocks.push( bqBlock );
232 						CKEDITOR.dom.element.setMarker( database, bqBlock, 'blockquote_processed', true );
233 					}
234
235 					movedNodes.push( node );
236 				}
237
238 				CKEDITOR.dom.element.clearAllMarkers( database );
239
240 				// Clear blockquote nodes that have become empty.
241 				for ( var i = processedBlockquoteBlocks.length - 1 ; i >= 0 ; i-- )
242 				{
243 					var bqBlock = processedBlockquoteBlocks[i];
244 					if ( noBlockLeft( bqBlock ) )
245 						bqBlock.remove();
246 				}
247
248 				if ( editor.config.enterMode == CKEDITOR.ENTER_BR )
249 				{
250 					var firstTime = true;
251 					while ( movedNodes.length )
252 					{
253 						var node = movedNodes.shift();
254
255 						if ( node.getName() == 'div' )
256 						{
257 							var docFrag = new CKEDITOR.dom.documentFragment( editor.document ),
258 								needBeginBr = firstTime && node.getPrevious() &&
259 									!( node.getPrevious().type == CKEDITOR.NODE_ELEMENT && node.getPrevious().isBlockBoundary() );
260 							if ( needBeginBr )
261 								docFrag.append( editor.document.createElement( 'br' ) );
262
263 							var needEngBr = node.getNext() &&
264 								!( node.getNext().type == CKEDITOR.NODE_ELEMENT && node.getNext().isBlockBoundary() );
265 							while ( node.getFirst() )
266 								node.getFirst().remove().appendTo( docFrag );
267
268 							if ( needEndBr )
269 								docFrag.append( editor.document.createElement( 'br' ) );
270
271 							docFrag.replace( node );
272 							firstTime = false;
273 						}
274 					}
275 				}
276 			}
277
278 			selection.selectBookmarks( bookmarks );
279 			editor.focus();
280 		}
281 	};
282
283 	CKEDITOR.plugins.add( 'blockquote',
284 	{
285 		init : function( editor )
286 		{
287 			editor.addCommand( 'blockquote', commandObject );
288
289 			editor.ui.addButton( 'Blockquote',
290 				{
291 					label : editor.lang.blockquote,
292 					command : 'blockquote'
293 				} );
294
295 			editor.on( 'selectionChange', onSelectionChange );
296 		},
297
298 		requires : [ 'domiterator' ]
299 	} );
300 })();
301