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