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 (function()
  7 {
  8 	// This function is to be called under a "walker" instance scope.
  9 	function iterate( rtl, breakOnFalse )
 10 	{
 11 		var range = this.range;
 12 
 13 		// Return null if we have reached the end.
 14 		if ( this._.end )
 15 			return null;
 16 
 17 		// This is the first call. Initialize it.
 18 		if ( !this._.start )
 19 		{
 20 			this._.start = 1;
 21 
 22 			// A collapsed range must return null at first call.
 23 			if ( range.collapsed )
 24 			{
 25 				this.end();
 26 				return null;
 27 			}
 28 
 29 			// Move outside of text node edges.
 30 			range.optimize();
 31 		}
 32 
 33 		var node,
 34 			startCt = range.startContainer,
 35 			endCt = range.endContainer,
 36 			startOffset = range.startOffset,
 37 			endOffset = range.endOffset,
 38 			guard,
 39 			userGuard = this.guard,
 40 			type = this.type,
 41 			getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
 42 
 43 		// Create the LTR guard function, if necessary.
 44 		if ( !rtl && !this._.guardLTR )
 45 		{
 46 			// The node that stops walker from moving up.
 47 			var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ?
 48 						   endCt :
 49 						   endCt.getParent();
 50 
 51 			// The node that stops the walker from going to next.
 52 			var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ?
 53 							 endCt.getChild( endOffset ) :
 54 							 endCt.getNext();
 55 
 56 			this._.guardLTR = function( node, movingOut )
 57 			{
 58 				return ( ( !movingOut || !limitLTR.equals( node ) )
 59 					&& ( !blockerLTR || !node.equals( blockerLTR ) )
 60 					&& ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) );
 61 			};
 62 		}
 63 
 64 		// Create the RTL guard function, if necessary.
 65 		if ( rtl && !this._.guardRTL )
 66 		{
 67 			// The node that stops walker from moving up.
 68 			var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ?
 69 						   startCt :
 70 						   startCt.getParent();
 71 
 72 			// The node that stops the walker from going to next.
 73 			var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ?
 74 						 startOffset ?
 75 						 startCt.getChild( startOffset - 1 ) : null :
 76 						 startCt.getPrevious();
 77 
 78 			this._.guardRTL = function( node, movingOut )
 79 			{
 80 				return ( ( !movingOut || !limitRTL.equals( node ) )
 81 					&& ( !blockerRTL || !node.equals( blockerRTL ) )
 82 					&& ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || node.getName() != 'body' ) );
 83 			};
 84 		}
 85 
 86 		// Define which guard function to use.
 87 		var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
 88 
 89 		// Make the user defined guard function participate in the process,
 90 		// otherwise simply use the boundary guard.
 91 		if ( userGuard )
 92 		{
 93 			guard = function( node, movingOut )
 94 			{
 95 				if ( stopGuard( node, movingOut ) === false )
 96 					return false;
 97 
 98 				return userGuard( node, movingOut );
 99 			};
100 		}
101 		else
102 			guard = stopGuard;
103 
104 		if ( this.current )
105 			node = this.current[ getSourceNodeFn ]( false, type, guard );
106 		else
107 		{
108 			// Get the first node to be returned.
109 			if ( rtl )
110 			{
111 				node = endCt;
112 
113 				if ( node.type == CKEDITOR.NODE_ELEMENT )
114 				{
115 					if ( endOffset > 0 )
116 						node = node.getChild( endOffset - 1 );
117 					else
118 						node = ( guard ( node, true ) === false ) ?
119 							null : node.getPreviousSourceNode( true, type, guard );
120 				}
121 			}
122 			else
123 			{
124 				node = startCt;
125 
126 				if ( node.type == CKEDITOR.NODE_ELEMENT )
127 				{
128 					if ( ! ( node = node.getChild( startOffset ) ) )
129 						node = ( guard ( startCt, true ) === false ) ?
130 							null : startCt.getNextSourceNode( true, type, guard ) ;
131 				}
132 			}
133 
134 			if ( node && guard( node ) === false )
135 				node = null;
136 		}
137 
138 		while ( node && !this._.end )
139 		{
140 			this.current = node;
141 
142 			if ( !this.evaluator || this.evaluator( node ) !== false )
143 			{
144 				if ( !breakOnFalse )
145 					return node;
146 			}
147 			else if ( breakOnFalse && this.evaluator )
148 				return false;
149 
150 			node = node[ getSourceNodeFn ]( false, type, guard );
151 		}
152 
153 		this.end();
154 		return this.current = null;
155 	}
156 
157 	function iterateToLast( rtl )
158 	{
159 		var node, last = null;
160 
161 		while ( ( node = iterate.call( this, rtl ) ) )
162 			last = node;
163 
164 		return last;
165 	}
166 
167 	CKEDITOR.dom.walker = CKEDITOR.tools.createClass(
168 	{
169 		/**
170 		 * Utility class to "walk" the DOM inside a range boundaries. If
171 		 * necessary, partially included nodes (text nodes) are broken to
172 		 * reflect the boundaries limits, so DOM and range changes may happen.
173 		 * Outside changes to the range may break the walker.
174 		 *
175 		 * The walker may return nodes that are not totaly included into the
176 		 * range boundaires. Let's take the following range representation,
177 		 * where the square brackets indicate the boundaries:
178 		 *
179 		 * [<p>Some <b>sample] text</b>
180 		 *
181 		 * While walking forward into the above range, the following nodes are
182 		 * returned: <p>, "Some ", <b> and "sample". Going
183 		 * backwards instead we have: "sample" and "Some ". So note that the
184 		 * walker always returns nodes when "entering" them, but not when
185 		 * "leaving" them. The guard function is instead called both when
186 		 * entering and leaving nodes.
187 		 *
188 		 * @constructor
189 		 * @param {CKEDITOR.dom.range} range The range within which walk.
190 		 */
191 		$ : function( range )
192 		{
193 			this.range = range;
194 
195 			/**
196 			 * A function executed for every matched node, to check whether
197 			 * it's to be considered into the walk or not. If not provided, all
198 			 * matched nodes are considered good.
199 			 * If the function returns "false" the node is ignored.
200 			 * @name CKEDITOR.dom.walker.prototype.evaluator
201 			 * @property
202 			 * @type Function
203 			 */
204 			// this.evaluator = null;
205 
206 			/**
207 			 * A function executed for every node the walk pass by to check
208 			 * whether the walk is to be finished. It's called when both
209 			 * entering and exiting nodes, as well as for the matched nodes.
210 			 * If this function returns "false", the walking ends and no more
211 			 * nodes are evaluated.
212 			 * @name CKEDITOR.dom.walker.prototype.guard
213 			 * @property
214 			 * @type Function
215 			 */
216 			// this.guard = null;
217 
218 			/** @private */
219 			this._ = {};
220 		},
221 
222 //		statics :
223 //		{
224 //			/* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
225 //			 * @param {CKEDITOR.dom.node} startNode The node from wich the walk
226 //			 *		will start.
227 //			 * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
228 //			 *		in the walk. No more nodes are retrieved after touching or
229 //			 *		passing it. If not provided, the walker stops at the
230 //			 *		<body> closing boundary.
231 //			 * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
232 //			 *		provided nodes.
233 //			 */
234 //			createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
235 //			{
236 //				var range = new CKEDITOR.dom.range();
237 //				if ( startNode )
238 //					range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
239 //				else
240 //					range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
241 //
242 //				if ( endNode )
243 //					range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
244 //				else
245 //					range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
246 //
247 //				return new CKEDITOR.dom.walker( range );
248 //			}
249 //		},
250 //
251 		proto :
252 		{
253 			/**
254 			 * Stop walking. No more nodes are retrieved if this function gets
255 			 * called.
256 			 */
257 			end : function()
258 			{
259 				this._.end = 1;
260 			},
261 
262 			/**
263 			 * Retrieves the next node (at right).
264 			 * @returns {CKEDITOR.dom.node} The next node or null if no more
265 			 *		nodes are available.
266 			 */
267 			next : function()
268 			{
269 				return iterate.call( this );
270 			},
271 
272 			/**
273 			 * Retrieves the previous node (at left).
274 			 * @returns {CKEDITOR.dom.node} The previous node or null if no more
275 			 *		nodes are available.
276 			 */
277 			previous : function()
278 			{
279 				return iterate.call( this, 1 );
280 			},
281 
282 			/**
283 			 * Check all nodes at right, executing the evaluation fuction.
284 			 * @returns {Boolean} "false" if the evaluator function returned
285 			 *		"false" for any of the matched nodes. Otherwise "true".
286 			 */
287 			checkForward : function()
288 			{
289 				return iterate.call( this, 0, 1 ) !== false;
290 			},
291 
292 			/**
293 			 * Check all nodes at left, executing the evaluation fuction.
294 			 * @returns {Boolean} "false" if the evaluator function returned
295 			 *		"false" for any of the matched nodes. Otherwise "true".
296 			 */
297 			checkBackward : function()
298 			{
299 				return iterate.call( this, 1, 1 ) !== false;
300 			},
301 
302 			/**
303 			 * Executes a full walk forward (to the right), until no more nodes
304 			 * are available, returning the last valid node.
305 			 * @returns {CKEDITOR.dom.node} The last node at the right or null
306 			 *		if no valid nodes are available.
307 			 */
308 			lastForward : function()
309 			{
310 				return iterateToLast.call( this );
311 			},
312 
313 			/**
314 			 * Executes a full walk backwards (to the left), until no more nodes
315 			 * are available, returning the last valid node.
316 			 * @returns {CKEDITOR.dom.node} The last node at the left or null
317 			 *		if no valid nodes are available.
318 			 */
319 			lastBackward : function()
320 			{
321 				return iterateToLast.call( this, 1 );
322 			},
323 
324 			reset : function()
325 			{
326 				delete this.current;
327 				this._ = {};
328 			}
329 
330 		}
331 	});
332 
333 	/*
334 	 * Anything whose display computed style is block, list-item, table,
335 	 * table-row-group, table-header-group, table-footer-group, table-row,
336 	 * table-column-group, table-column, table-cell, table-caption, or whose node
337 	 * name is hr, br (when enterMode is br only) is a block boundary.
338 	 */
339 	var blockBoundaryDisplayMatch =
340 	{
341 		block : 1,
342 		'list-item' : 1,
343 		table : 1,
344 		'table-row-group' : 1,
345 		'table-header-group' : 1,
346 		'table-footer-group' : 1,
347 		'table-row' : 1,
348 		'table-column-group' : 1,
349 		'table-column' : 1,
350 		'table-cell' : 1,
351 		'table-caption' : 1
352 	};
353 
354 	CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames )
355 	{
356 		var nodeNameMatches = customNodeNames ?
357 			CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$block, customNodeNames || {} ) :
358 			CKEDITOR.dtd.$block;
359 
360 		// Don't consider floated formatting as block boundary, fall back to dtd check in that case. (#6297)
361 		return this.getComputedStyle( 'float' ) == 'none' && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ]
362 				|| nodeNameMatches[ this.getName() ];
363 	};
364 
365 	CKEDITOR.dom.walker.blockBoundary = function( customNodeNames )
366 	{
367 		return function( node , type )
368 		{
369 			return ! ( node.type == CKEDITOR.NODE_ELEMENT
370 						&& node.isBlockBoundary( customNodeNames ) );
371 		};
372 	};
373 
374 	CKEDITOR.dom.walker.listItemBoundary = function()
375 	{
376 			return this.blockBoundary( { br : 1 } );
377 	};
378 
379 	/**
380 	 * Whether the to-be-evaluated node is a bookmark node OR bookmark node
381 	 * inner contents.
382 	 * @param {Boolean} contentOnly Whether only test againt the text content of
383 	 * bookmark node instead of the element itself(default).
384 	 * @param {Boolean} isReject Whether should return 'false' for the bookmark
385 	 * node instead of 'true'(default).
386 	 */
387 	CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject )
388 	{
389 		function isBookmarkNode( node )
390 		{
391 			return ( node && node.getName
392 					&& node.getName() == 'span'
393 					&& node.data( 'cke-bookmark' ) );
394 		}
395 
396 		return function( node )
397 		{
398 			var isBookmark, parent;
399 			// Is bookmark inner text node?
400 			isBookmark = ( node && !node.getName && ( parent = node.getParent() )
401 						&& isBookmarkNode( parent ) );
402 			// Is bookmark node?
403 			isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
404 			return !! ( isReject ^ isBookmark );
405 		};
406 	};
407 
408 	/**
409 	 * Whether the node is a text node containing only whitespaces characters.
410 	 * @param isReject
411 	 */
412 	CKEDITOR.dom.walker.whitespaces = function( isReject )
413 	{
414 		return function( node )
415 		{
416 			var isWhitespace;
417 			if ( node && node.type == CKEDITOR.NODE_TEXT )
418 			{
419 				// whitespace, as well as the text cursor filler node we used in Webkit. (#9384)
420 				isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
421 					CKEDITOR.env.webkit && node.getText() == '\u200b';
422 			}
423 
424 			return !! ( isReject ^ isWhitespace );
425 		};
426 	};
427 
428 	/**
429 	 * Whether the node is invisible in wysiwyg mode.
430 	 * @param isReject
431 	 */
432 	CKEDITOR.dom.walker.invisible = function( isReject )
433 	{
434 		var whitespace = CKEDITOR.dom.walker.whitespaces();
435 		return function( node )
436 		{
437 			var invisible;
438 
439 			if ( whitespace( node ) )
440 				invisible = 1;
441 			else
442 			{
443 				// Visibility should be checked on element.
444 				if ( node.type == CKEDITOR.NODE_TEXT )
445 					node = node.getParent();
446 
447 				// Nodes that take no spaces in wysiwyg:
448 				// 1. White-spaces but not including NBSP;
449 				// 2. Empty inline elements, e.g. <b></b> we're checking here
450 				// 'offsetHeight' instead of 'offsetWidth' for properly excluding
451 				// all sorts of empty paragraph, e.g. <br />.
452 				invisible = !node.$.offsetHeight;
453 			}
454 
455 			return !! ( isReject ^ invisible );
456 		};
457 	};
458 
459 	CKEDITOR.dom.walker.nodeType = function( type, isReject )
460 	{
461 		return function( node )
462 		{
463 			return !! ( isReject ^ ( node.type == type ) );
464 		};
465 	};
466 
467 	CKEDITOR.dom.walker.bogus = function( isReject )
468 	{
469 		function nonEmpty( node )
470 		{
471 			return !isWhitespaces( node ) && !isBookmark( node );
472 		}
473 
474 		return function( node )
475 		{
476 			var isBogus = !CKEDITOR.env.ie ? node.is && node.is( 'br' ) :
477 					  node.getText && tailNbspRegex.test( node.getText() );
478 
479 			if ( isBogus )
480 			{
481 				var parent = node.getParent(), next = node.getNext( nonEmpty );
482 				isBogus = parent.isBlockBoundary() &&
483 				          ( !next ||
484 				            next.type == CKEDITOR.NODE_ELEMENT &&
485 				            next.isBlockBoundary() );
486 			}
487 
488 			return !! ( isReject ^ isBogus );
489 		};
490 	};
491 
492 	var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/,
493 		isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
494 		isBookmark = CKEDITOR.dom.walker.bookmark(),
495 		toSkip = function( node )
496 		{
497 			return isBookmark( node )
498 					|| isWhitespaces( node )
499 					|| node.type == CKEDITOR.NODE_ELEMENT
500 					&& node.getName() in CKEDITOR.dtd.$inline
501 					&& !( node.getName() in CKEDITOR.dtd.$empty );
502 		};
503 
504 	// Check if there's a filler node at the end of an element, and return it.
505 	CKEDITOR.dom.element.prototype.getBogus = function()
506 	{
507 		// Bogus are not always at the end, e.g. <p><a>text<br /></a></p> (#7070).
508 		var tail = this;
509 		do { tail = tail.getPreviousSourceNode(); }
510 		while ( toSkip( tail ) )
511 
512 		if ( tail && ( !CKEDITOR.env.ie ? tail.is && tail.is( 'br' )
513 				: tail.getText && tailNbspRegex.test( tail.getText() ) ) )
514 		{
515 			return tail;
516 		}
517 		return false;
518 	};
519 
520 })();
521