JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
191d758003ae34a8aa3d102176f1b68039b04bfe
[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, true );\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, false, true ) !== 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, true, true ) !== 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, true );\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         blockBoundaryNodeNameMatch = { hr : 1 };\r
339 \r
340         CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames )\r
341         {\r
342                 var nodeNameMatches = CKEDITOR.tools.extend( {},\r
343                                                                                                         blockBoundaryNodeNameMatch, customNodeNames || {} );\r
344 \r
345                 return blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] ||\r
346                         nodeNameMatches[ this.getName() ];\r
347         };\r
348 \r
349         CKEDITOR.dom.walker.blockBoundary = function( customNodeNames )\r
350         {\r
351                 return function( node , type )\r
352                 {\r
353                         return ! ( node.type == CKEDITOR.NODE_ELEMENT\r
354                                                 && node.isBlockBoundary( customNodeNames ) );\r
355                 };\r
356         };\r
357 \r
358         CKEDITOR.dom.walker.listItemBoundary = function()\r
359         {\r
360                         return this.blockBoundary( { br : 1 } );\r
361         };\r
362 \r
363         /**\r
364          * Whether the to-be-evaluated node is a bookmark node OR bookmark node\r
365          * inner contents.\r
366          * @param {Boolean} contentOnly Whether only test againt the text content of\r
367          * bookmark node instead of the element itself(default).\r
368          * @param {Boolean} isReject Whether should return 'false' for the bookmark\r
369          * node instead of 'true'(default).\r
370          */\r
371         CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject )\r
372         {\r
373                 function isBookmarkNode( node )\r
374                 {\r
375                         return ( node && node.getName\r
376                                         && node.getName() == 'span'\r
377                                         && node.hasAttribute('_fck_bookmark') );\r
378                 }\r
379 \r
380                 return function( node )\r
381                 {\r
382                         var isBookmark, parent;\r
383                         // Is bookmark inner text node?\r
384                         isBookmark = ( node && !node.getName && ( parent = node.getParent() )\r
385                                                 && isBookmarkNode( parent ) );\r
386                         // Is bookmark node?\r
387                         isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );\r
388                         return isReject ^ isBookmark;\r
389                 };\r
390         };\r
391 \r
392         /**\r
393          * Whether the node is a text node containing only whitespaces characters.\r
394          * @param isReject\r
395          */\r
396         CKEDITOR.dom.walker.whitespaces = function( isReject )\r
397         {\r
398                 return function( node )\r
399                 {\r
400                         var isWhitespace = node && ( node.type == CKEDITOR.NODE_TEXT )\r
401                                                         && !CKEDITOR.tools.trim( node.getText() );\r
402                         return isReject ^ isWhitespace;\r
403                 };\r
404         };\r
405 \r
406         /**\r
407          * Whether the node is invisible in wysiwyg mode.\r
408          * @param isReject\r
409          */\r
410         CKEDITOR.dom.walker.invisible = function( isReject )\r
411         {\r
412                 var whitespace = CKEDITOR.dom.walker.whitespaces();\r
413                 return function( node )\r
414                 {\r
415                         // Nodes that take no spaces in wysiwyg:\r
416                         // 1. White-spaces but not including NBSP;\r
417                         // 2. Empty inline elements, e.g. <b></b> we're checking here\r
418                         // 'offsetHeight' instead of 'offsetWidth' for properly excluding\r
419                         // all sorts of empty paragraph, e.g. <br />.\r
420                         var isInvisible = whitespace( node ) || node.is && !node.$.offsetHeight;\r
421                         return isReject ^ isInvisible;\r
422                 };\r
423         };\r
424 \r
425         var tailNbspRegex = /^[\t\r\n ]*(?:&nbsp;|\xa0)$/,\r
426                 isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),\r
427                 isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),\r
428                 fillerEvaluator = function( element )\r
429                 {\r
430                         return isNotBookmark( element ) && isNotWhitespaces( element );\r
431                 };\r
432 \r
433         // Check if there's a filler node at the end of an element, and return it.\r
434         CKEDITOR.dom.element.prototype.getBogus = function ()\r
435         {\r
436                 var tail = this.getLast( fillerEvaluator );\r
437                 if ( tail && ( !CKEDITOR.env.ie ? tail.is && tail.is( 'br' )\r
438                                 : tail.getText && tailNbspRegex.test( tail.getText() ) ) )\r
439                 {\r
440                         return tail;\r
441                 }\r
442                 return false;\r
443         };\r
444 \r
445 })();\r