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 DOM iterator, which iterates over list items, lines and paragraphs.
  8  */
  9
 10 CKEDITOR.plugins.add( 'domiterator' );
 11
 12 (function()
 13 {
 14 	// Functions ported over from v2.
 15 	function getTouchedStartNode( range )
 16 	{
 17 		var container = range.startContainer;
 18
 19 		if ( range.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
 20 			return container;
 21
 22 		return container.getChildCount() > range.startOffset ? container.getChild( range.startOffset ) : container;
 23 	}
 24
 25 	function getTouchedEndNode( range )
 26 	{
 27 		var container = range.endContainer;
 28
 29 		if ( range.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
 30 			return container;
 31
 32 		return container.getChildCount() > range.endOffset ? container.getChild( range.endOffset ) : container;
 33 	}
 34
 35 	function getNextSourceNode( currentNode, startFromSibling, nodeType, stopSearchNode )
 36 	{
 37 		if ( !currentNode )
 38 			return null;
 39
 40 		var node;
 41
 42 		if ( !startFromSibling && currentNode.getFirst && currentNode.getFirst() )
 43 			node = currentNode.getFirst();
 44 		else
 45 		{
 46 			if ( stopSearchNode && currentNode.equals( stopSearchNode ) )
 47 				return null;
 48
 49 			node = currentNode.getNext();
 50
 51 			if ( !node && ( !stopSearchNode || !stopSearchNode.equals( currentNode.parentNode ) ) )
 52 				return getNextSourceNode( currentNode.getParent(), true, nodeType, stopSearchNode );
 53 		}
 54
 55 		if ( nodeType && node && node.type != nodeType )
 56 			return getNextSourceNode( node, false, nodeType, stopSearchNode );
 57
 58 		return node;
 59 	}
 60
 61 	var iterator = function( range )
 62 	{
 63 		if ( arguments.length < 1 )
 64 			return;
 65
 66 		this.range = range;
 67 		this.forceBrBreak = false;
 68 		this.enforceRealBlocks = false;
 69
 70 		this._ || ( this._ = {} );
 71 	},
 72 		beginWhitespaceRegex = /^[\r\n\t ]+$/;
 73
 74
 75 	iterator.prototype = {
 76 		getNextParagraph : function( blockTag )
 77 		{
 78 			// The block element to be returned.
 79 			var block;
 80
 81 			// The range object used to identify the paragraph contents.
 82 			var range;
 83
 84 			// Indicats that the current element in the loop is the last one.
 85 			var isLast;
 86
 87 			// Instructs to cleanup remaining BRs.
 88 			var removePreviousBr, removeLastBr;
 89
 90 			// This is the first iteration. Let's initialize it.
 91 			if ( !this._.lastNode )
 92 			{
 93 				range = this.range.clone();
 94 				range.enlarge( this.forceBrBreak ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
 95
 96 				this._.nextNode = getTouchedStartNode( range );
 97 				this._.lastNode = getTouchedEndNode( range );
 98
 99 				// Let's reuse this variable.
100 				range = null;
101 			}
102
103 			var currentNode = this._.nextNode,
104 				lastNode = this._.lastNode;
105
106 			this._.nextNode = null;
107
108 			while ( currentNode )
109 			{
110 				// closeRange indicates that a paragraph boundary has been found,
111 				// so the range can be closed.
112 				var closeRange = false;
113
114 				// includeNode indicates that the current node is good to be part
115 				// of the range. By default, any non-element node is ok for it.
116 				var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
117 					continueFromSibling = false;
118
119 				// If it is an element node, let's check if it can be part of the
120 				// range.
121 				if ( !includeNode )
122 				{
123 					var nodeName = currentNode.getName();
124
125 					if ( currentNode.isBlockBoundary( this.forceBrBreak && { br : 1 } ) )
126 					{
127 						// <br> boundaries must be part of the range. It will
128 						// happen only if ForceBrBreak.
129 						if ( nodeName == 'br' )
130 							includeNode = true;
131 						else if ( !range && currentNode.getChildCount() == 0 && nodeName != 'hr' )
132 						{
133 							// If we have found an empty block, and haven't started
134 							// the range yet, it means we must return this block.
135 							block = currentNode;
136 							isLast = currentNode.equals( lastNode );
137 							break;
138 						}
139
140 						// The range must finish right before the boundary,
141 						// including possibly skipped empty spaces. (#1603)
142 						if ( range )
143 						{
144 							range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
145
146 							// The found boundary must be set as the next one at this
147 							// point. (#1717)
148 							if ( nodeName != 'br' )
149 								this._.nextNode = getNextSourceNode( currentNode, true, null, lastNode ) || currentNode;
150 						}
151
152 						closeRange = true;
153 					}
154 					else
155 					{
156 						// If we have child nodes, let's check them.
157 						if ( currentNode.getFirst() )
158 						{
159 							// If we don't have a range yet, let's start it.
160 							if ( !range )
161 							{
162 								range = new CKEDITOR.dom.range( this.range.document );
163 								range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
164 							}
165
166 							currentNode = currentNode.getFirst();
167 							continue;
168 						}
169 						includeNode = true;
170 					}
171 				}
172 				else if ( currentNode.type == CKEDITOR.NODE_TEXT )
173 				{
174 					// Ignore normal whitespaces (i.e. not including   or
175 					// other unicode whitespaces) before/after a block node.
176 					if ( beginWhitespaceRegex.test( currentNode.getText() ) )
177 						includeNode = false;
178 				}
179
180 				// The current node is good to be part of the range and we are
181 				// starting a new range, initialize it first.
182 				if ( includeNode && !range )
183 				{
184 					range = new CKEDITOR.dom.range( this.range.document );
185 					range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
186 				}
187
188 				// The last node has been found.
189 				isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
190
191 				// If we are in an element boundary, let's check if it is time
192 				// to close the range, otherwise we include the parent within it.
193 				if ( range && !closeRange )
194 				{
195 					while ( !currentNode.getNext() && !isLast )
196 					{
197 						var parentNode = currentNode.getParent();
198
199 						if ( parentNode.isBlockBoundary( this.forceBrBreak && { br : 1 } ) )
200 						{
201 							closeRange = true;
202 							isLast = isLast || ( parentNode.equals( lastNode) );
203 							break;
204 						}
205
206 						currentNode = parentNode;
207 						includeNode = true;
208 						isLast = ( currentNode.equals( lastNode ) );
209 						continueFromSibling = true;
210 					}
211 				}
212
213 				// Now finally include the node.
214 				if ( includeNode )
215 					range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
216
217 				// We have found a block boundary. Let's close the range and move out of the
218 				// loop.
219 				if ( ( closeRange || isLast ) && range )
220 				{
221 					var boundaryNodes = range.getBoundaryNodes(),
222 						startPath = new CKEDITOR.dom.elementPath( range.startContainer ),
223 						endPath = new CKEDITOR.dom.elementPath( range.endContainer );
224 					if ( boundaryNodes.startNode.equals( boundaryNodes.endNode )
225 							&& boundaryNodes.startNode.getParent().equals( startPath.blockLimit )
226 							&& boundaryNodes.startNode.type == CKEDITOR.NODE_ELEMENT && boundaryNodes.startNode.getAttribute( '_fck_bookmark' ) )
227 						range = null;
228 					else
229 						break;
230 				}
231
232 				if ( isLast )
233 					break;
234
235 				currentNode = getNextSourceNode( currentNode, continueFromSibling, null, lastNode );
236 			}
237
238 			// Now, based on the processed range, look for (or create) the block to be returned.
239 			if ( !block )
240 			{
241 				// If no range has been found, this is the end.
242 				if ( !range )
243 				{
244 					this._.nextNode = null;
245 					return null;
246 				}
247
248 				var startPath = new CKEDITOR.dom.elementPath( range.startContainer ),
249 					startBlockLimit = startPath.blockLimit,
250 					checkLimits = { div : 1, th : 1, td : 1 };
251 				block = startPath.block;
252
253 				if ( !block
254 						&& !this.enforceRealBlocks
255 						&& checkLimits[ startBlockLimit.getName() ]
256 						&& range.checkStartOfBlock()
257 						&& range.checkEndOfBlock() )
258 					block = startBlockLimit;
259 				else if ( !block || ( this.enforceRealBlocks && block.getName() == 'li' ) )
260 				{
261 					// Create the fixed block.
262 					block = this.range.document.createElement( blockTag || 'p' );
263
264 					// Move the contents of the temporary range to the fixed block.
265 					range.extractContents().appendTo( block );
266 					block.trim();
267
268 					// Insert the fixed block into the DOM.
269 					range.insertNode( block );
270
271 					removePreviousBr = removeLastBr = true;
272 				}
273 				else if ( block.getName() != 'li' )
274 				{
275 					// If the range doesn't includes the entire contents of the
276 					// block, we must split it, isolating the range in a dedicated
277 					// block.
278 					if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() )
279 					{
280 						// The resulting block will be a clone of the current one.
281 						block = block.clone( false );
282
283 						// Extract the range contents, moving it to the new block.
284 						range.extractContents().appendTo( block );
285 						block.trim();
286
287 						// Split the block. At this point, the range will be in the
288 						// right position for our intents.
289 						var splitInfo = range.splitBlock();
290
291 						removePreviousBr = !splitInfo.wasStartOfBlock;
292 						removeLastBr = !splitInfo.wasEndOfBlock;
293
294 						// Insert the new block into the DOM.
295 						range.insertNode( block );
296 					}
297 				}
298 				else if ( !isLast )
299 				{
300 					// LIs are returned as is, with all their children (due to the
301 					// nested lists). But, the next node is the node right after
302 					// the current range, which could be an <li> child (nested
303 					// lists) or the next sibling <li>.
304
305 					this._.nextNode = ( block.equals( lastNode ) ? null :
306 							getNextSourceNode( range.getBoundaryNodes().endNode, true, null, lastNode ) );
307 				}
308 			}
309
310 			if ( removePreviousBr )
311 			{
312 				var previousSibling = block.getPrevious();
313 				if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT )
314 				{
315 					if ( previousSibling.getName() == 'br' )
316 						previousSibling.remove();
317 					else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
318 						previousSibling.getLast().remove();
319 				}
320 			}
321
322 			if ( removeLastBr )
323 			{
324 				var lastChild = block.getLast();
325 				if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' )
326 					lastChild.remove();
327 			}
328
329 			// Get a reference for the next element. This is important because the
330 			// above block can be removed or changed, so we can rely on it for the
331 			// next interation.
332 			if ( !this._.nextNode )
333 				this._.nextNode = ( isLast || block.equals( lastNode ) ) ? null : getNextSourceNode( block, true, null, lastNode );
334
335 			return block;
336 		}
337 	};
338
339 	CKEDITOR.dom.range.prototype.createIterator = function()
340 	{
341 		return new iterator( this );
342 	};
343 })();
344