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