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