JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.0
[ckeditor.git] / _source / core / dom / walker.js
diff --git a/_source/core/dom/walker.js b/_source/core/dom/walker.js
new file mode 100644 (file)
index 0000000..4617a81
--- /dev/null
@@ -0,0 +1,411 @@
+/*\r
+Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.html or http://ckeditor.com/license\r
+*/\r
+\r
+(function()\r
+{\r
+       // This function is to be called under a "walker" instance scope.\r
+       function iterate( rtl, breakOnFalse )\r
+       {\r
+               // Return null if we have reached the end.\r
+               if ( this._.end )\r
+                       return null;\r
+\r
+               var node,\r
+                       range = this.range,\r
+                       guard,\r
+                       userGuard = this.guard,\r
+                       type = this.type,\r
+                       getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );\r
+\r
+               // This is the first call. Initialize it.\r
+               if ( !this._.start )\r
+               {\r
+                       this._.start = 1;\r
+\r
+                       // Trim text nodes and optmize the range boundaries. DOM changes\r
+                       // may happen at this point.\r
+                       range.trim();\r
+\r
+                       // A collapsed range must return null at first call.\r
+                       if ( range.collapsed )\r
+                       {\r
+                               this.end();\r
+                               return null;\r
+                       }\r
+               }\r
+\r
+               // Create the LTR guard function, if necessary.\r
+               if ( !rtl && !this._.guardLTR )\r
+               {\r
+                       // Gets the node that stops the walker when going LTR.\r
+                       var limitLTR = range.endContainer,\r
+                               blockerLTR = limitLTR.getChild( range.endOffset );\r
+\r
+                       this._.guardLTR = function( node, movingOut )\r
+                       {\r
+                               return ( ( !movingOut || !limitLTR.equals( node ) )\r
+                                       && ( !blockerLTR || !node.equals( blockerLTR ) )\r
+                                       && ( node.type != CKEDITOR.NODE_ELEMENT || node.getName() != 'body' ) );\r
+                       };\r
+               }\r
+\r
+               // Create the RTL guard function, if necessary.\r
+               if ( rtl && !this._.guardRTL )\r
+               {\r
+                       // Gets the node that stops the walker when going LTR.\r
+                       var limitRTL = range.startContainer,\r
+                               blockerRTL = ( range.startOffset > 0 ) && limitRTL.getChild( range.startOffset - 1 );\r
+\r
+                       this._.guardRTL = function( node, movingOut )\r
+                       {\r
+                               return ( ( !movingOut || !limitRTL.equals( node ) )\r
+                                       && ( !blockerRTL || !node.equals( blockerRTL ) )\r
+                                       && ( node.type != CKEDITOR.NODE_ELEMENT || node.getName() != 'body' ) );\r
+                       };\r
+               }\r
+\r
+               // Define which guard function to use.\r
+               var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;\r
+\r
+               // Make the user defined guard function participate in the process,\r
+               // otherwise simply use the boundary guard.\r
+               if ( userGuard )\r
+               {\r
+                       guard = function( node, movingOut )\r
+                       {\r
+                               if ( stopGuard( node, movingOut ) === false )\r
+                                       return false;\r
+\r
+                               return userGuard( node );\r
+                       };\r
+               }\r
+               else\r
+                       guard = stopGuard;\r
+\r
+               if ( this.current )\r
+                       node = this.current[ getSourceNodeFn ]( false, type, guard );\r
+               else\r
+               {\r
+                       // Get the first node to be returned.\r
+\r
+                       if ( rtl )\r
+                       {\r
+                               node = range.endContainer;\r
+\r
+                               if ( range.endOffset > 0 )\r
+                               {\r
+                                       node = node.getChild( range.endOffset - 1 );\r
+                                       if ( guard( node ) === false )\r
+                                               node = null;\r
+                               }\r
+                               else\r
+                                       node = ( guard ( node ) === false ) ?\r
+                                               null : node.getPreviousSourceNode( true, type, guard );\r
+                       }\r
+                       else\r
+                       {\r
+                               node = range.startContainer;\r
+                               node = node.getChild( range.startOffset );\r
+\r
+                               if ( node )\r
+                               {\r
+                                       if ( guard( node ) === false )\r
+                                               node = null;\r
+                               }\r
+                               else\r
+                                       node = ( guard ( range.startContainer ) === false ) ?\r
+                                               null : range.startContainer.getNextSourceNode( true, type, guard ) ;\r
+                       }\r
+               }\r
+\r
+               while ( node && !this._.end )\r
+               {\r
+                       this.current = node;\r
+\r
+                       if ( !this.evaluator || this.evaluator( node ) !== false )\r
+                       {\r
+                               if ( !breakOnFalse )\r
+                                       return node;\r
+                       }\r
+                       else if ( breakOnFalse && this.evaluator )\r
+                               return false;\r
+\r
+                       node = node[ getSourceNodeFn ]( false, type, guard );\r
+               }\r
+\r
+               this.end();\r
+               return this.current = null;\r
+       }\r
+\r
+       function iterateToLast( rtl )\r
+       {\r
+               var node, last = null;\r
+\r
+               while ( ( node = iterate.call( this, rtl ) ) )\r
+                       last = node;\r
+\r
+               return last;\r
+       }\r
+\r
+       CKEDITOR.dom.walker = CKEDITOR.tools.createClass(\r
+       {\r
+               /**\r
+                * Utility class to "walk" the DOM inside a range boundaries. If\r
+                * necessary, partially included nodes (text nodes) are broken to\r
+                * reflect the boundaries limits, so DOM and range changes may happen.\r
+                * Outside changes to the range may break the walker.\r
+                *\r
+                * The walker may return nodes that are not totaly included into the\r
+                * range boundaires. Let's take the following range representation,\r
+                * where the square brackets indicate the boundaries:\r
+                *\r
+                * [<p>Some <b>sample] text</b>\r
+                *\r
+                * While walking forward into the above range, the following nodes are\r
+                * returned: <p>, "Some ", <b> and "sample". Going\r
+                * backwards instead we have: "sample" and "Some ". So note that the\r
+                * walker always returns nodes when "entering" them, but not when\r
+                * "leaving" them. The guard function is instead called both when\r
+                * entering and leaving nodes.\r
+                *\r
+                * @constructor\r
+                * @param {CKEDITOR.dom.range} range The range within which walk.\r
+                */\r
+               $ : function( range )\r
+               {\r
+                       this.range = range;\r
+\r
+                       /**\r
+                        * A function executed for every matched node, to check whether\r
+                        * it's to be considered into the walk or not. If not provided, all\r
+                        * matched nodes are considered good.\r
+                        * If the function returns "false" the node is ignored.\r
+                        * @name CKEDITOR.dom.walker.prototype.evaluator\r
+                        * @property\r
+                        * @type Function\r
+                        */\r
+                       // this.evaluator = null;\r
+\r
+                       /**\r
+                        * A function executed for every node the walk pass by to check\r
+                        * whether the walk is to be finished. It's called when both\r
+                        * entering and exiting nodes, as well as for the matched nodes.\r
+                        * If this function returns "false", the walking ends and no more\r
+                        * nodes are evaluated.\r
+                        * @name CKEDITOR.dom.walker.prototype.guard\r
+                        * @property\r
+                        * @type Function\r
+                        */\r
+                       // this.guard = null;\r
+\r
+                       /** @private */\r
+                       this._ = {};\r
+               },\r
+\r
+//             statics :\r
+//             {\r
+//                     /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.\r
+//                      * @param {CKEDITOR.dom.node} startNode The node from wich the walk\r
+//                      *              will start.\r
+//                      * @param {CKEDITOR.dom.node} [endNode] The last node to be considered\r
+//                      *              in the walk. No more nodes are retrieved after touching or\r
+//                      *              passing it. If not provided, the walker stops at the\r
+//                      *              <body> closing boundary.\r
+//                      * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the\r
+//                      *              provided nodes.\r
+//                      */\r
+//                     createOnNodes : function( startNode, endNode, startInclusive, endInclusive )\r
+//                     {\r
+//                             var range = new CKEDITOR.dom.range();\r
+//                             if ( startNode )\r
+//                                     range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;\r
+//                             else\r
+//                                     range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;\r
+//\r
+//                             if ( endNode )\r
+//                                     range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;\r
+//                             else\r
+//                                     range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;\r
+//\r
+//                             return new CKEDITOR.dom.walker( range );\r
+//                     }\r
+//             },\r
+//\r
+               proto :\r
+               {\r
+                       /**\r
+                        * Stop walking. No more nodes are retrieved if this function gets\r
+                        * called.\r
+                        */\r
+                       end : function()\r
+                       {\r
+                               this._.end = 1;\r
+                       },\r
+\r
+                       /**\r
+                        * Retrieves the next node (at right).\r
+                        * @returns {CKEDITOR.dom.node} The next node or null if no more\r
+                        *              nodes are available.\r
+                        */\r
+                       next : function()\r
+                       {\r
+                               return iterate.call( this );\r
+                       },\r
+\r
+                       /**\r
+                        * Retrieves the previous node (at left).\r
+                        * @returns {CKEDITOR.dom.node} The previous node or null if no more\r
+                        *              nodes are available.\r
+                        */\r
+                       previous : function()\r
+                       {\r
+                               return iterate.call( this, true );\r
+                       },\r
+\r
+                       /**\r
+                        * Check all nodes at right, executing the evaluation fuction.\r
+                        * @returns {Boolean} "false" if the evaluator function returned\r
+                        *              "false" for any of the matched nodes. Otherwise "true".\r
+                        */\r
+                       checkForward : function()\r
+                       {\r
+                               return iterate.call( this, false, true ) !== false;\r
+                       },\r
+\r
+                       /**\r
+                        * Check all nodes at left, executing the evaluation fuction.\r
+                        * @returns {Boolean} "false" if the evaluator function returned\r
+                        *              "false" for any of the matched nodes. Otherwise "true".\r
+                        */\r
+                       checkBackward : function()\r
+                       {\r
+                               return iterate.call( this, true, true ) !== false;\r
+                       },\r
+\r
+                       /**\r
+                        * Executes a full walk forward (to the right), until no more nodes\r
+                        * are available, returning the last valid node.\r
+                        * @returns {CKEDITOR.dom.node} The last node at the right or null\r
+                        *              if no valid nodes are available.\r
+                        */\r
+                       lastForward : function()\r
+                       {\r
+                               return iterateToLast.call( this );\r
+                       },\r
+\r
+                       /**\r
+                        * Executes a full walk backwards (to the left), until no more nodes\r
+                        * are available, returning the last valid node.\r
+                        * @returns {CKEDITOR.dom.node} The last node at the left or null\r
+                        *              if no valid nodes are available.\r
+                        */\r
+                       lastBackward : function()\r
+                       {\r
+                               return iterateToLast.call( this, true );\r
+                       },\r
+\r
+                       reset : function()\r
+                       {\r
+                               delete this.current;\r
+                               this._ = {};\r
+                       }\r
+\r
+               }\r
+       });\r
+\r
+       /*\r
+        * Anything whose display computed style is block, list-item, table,\r
+        * table-row-group, table-header-group, table-footer-group, table-row,\r
+        * table-column-group, table-column, table-cell, table-caption, or whose node\r
+        * name is hr, br (when enterMode is br only) is a block boundary.\r
+        */\r
+       var blockBoundaryDisplayMatch =\r
+       {\r
+               block : 1,\r
+               'list-item' : 1,\r
+               table : 1,\r
+               'table-row-group' : 1,\r
+               'table-header-group' : 1,\r
+               'table-footer-group' : 1,\r
+               'table-row' : 1,\r
+               'table-column-group' : 1,\r
+               'table-column' : 1,\r
+               'table-cell' : 1,\r
+               'table-caption' : 1\r
+       },\r
+       blockBoundaryNodeNameMatch = { hr : 1 };\r
+\r
+       CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames )\r
+       {\r
+               var nodeNameMatches = CKEDITOR.tools.extend( {},\r
+                                                                                                       blockBoundaryNodeNameMatch, customNodeNames || {} );\r
+\r
+               return blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] ||\r
+                       nodeNameMatches[ this.getName() ];\r
+       };\r
+\r
+       CKEDITOR.dom.walker.blockBoundary = function( customNodeNames )\r
+       {\r
+               return function( node , type )\r
+               {\r
+                       return ! ( node.type == CKEDITOR.NODE_ELEMENT\r
+                                               && node.isBlockBoundary( customNodeNames ) );\r
+               };\r
+       };\r
+\r
+       CKEDITOR.dom.walker.listItemBoundary = function()\r
+       {\r
+                       return this.blockBoundary( { br : 1 } );\r
+       };\r
+       /**\r
+        * Whether the node is a bookmark node's inner text node.\r
+        */\r
+       CKEDITOR.dom.walker.bookmarkContents = function( node )\r
+       {\r
+       },\r
+\r
+       /**\r
+        * Whether the to-be-evaluated node is a bookmark node OR bookmark node\r
+        * inner contents.\r
+        * @param {Boolean} contentOnly Whether only test againt the text content of\r
+        * bookmark node instead of the element itself(default).\r
+        * @param {Boolean} isReject Whether should return 'false' for the bookmark\r
+        * node instead of 'true'(default).\r
+        */\r
+       CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject )\r
+       {\r
+               function isBookmarkNode( node )\r
+               {\r
+                       return ( node && node.getName\r
+                                       && node.getName() == 'span'\r
+                                       && node.hasAttribute('_fck_bookmark') );\r
+               }\r
+\r
+               return function( node )\r
+               {\r
+                       var isBookmark, parent;\r
+                       // Is bookmark inner text node?\r
+                       isBookmark = ( node && !node.getName && ( parent = node.getParent() )\r
+                                               && isBookmarkNode( parent ) );\r
+                       // Is bookmark node?\r
+                       isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );\r
+                       return isReject ^ isBookmark;\r
+               };\r
+       };\r
+\r
+       /**\r
+        * Whether the node contains only white-spaces characters.\r
+        * @param isReject\r
+        */\r
+       CKEDITOR.dom.walker.whitespaces = function( isReject )\r
+       {\r
+               return function( node )\r
+               {\r
+                       var isWhitespace = node && ( node.type == CKEDITOR.NODE_TEXT )\r
+                                                       && !CKEDITOR.tools.trim( node.getText() );\r
+                       return isReject ^ isWhitespace;\r
+               };\r
+       };\r
+})();\r