1 /**
  2  * modified from ckeditor for kissy editor ,walker implementation
  3  * refer: http://www.w3.org/TR/DOM-Level-2-Traversal-Range/traversal#TreeWalker
  4  * @author yiminghe@gmail.com
  5  */
  6 /*
  7  Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
  8  For licensing, see LICENSE.html or http://ckeditor.com/license
  9  */
 10 KISSY.add("editor/core/walker", function (S, Editor) {
 11 
 12     var TRUE = true,
 13         FALSE = false,
 14         NULL = null,
 15         UA = S.UA,
 16         DOM = S.DOM,
 17         dtd = Editor.XHTML_DTD,
 18         Node = S.Node;
 19 
 20 
 21     function iterate(rtl, breakOnFalseRetFalse) {
 22         var self = this;
 23         // Return NULL if we have reached the end.
 24         if (self._.end) {
 25             return NULL;
 26         }
 27         var node,
 28             range = self.range,
 29             guard,
 30             userGuard = self.guard,
 31             type = self.type,
 32             getSourceNodeFn = ( rtl ? '_4e_previousSourceNode' :
 33                 '_4e_nextSourceNode' );
 34 
 35         // This is the first call. Initialize it.
 36         if (!self._.start) {
 37             self._.start = 1;
 38 
 39             // Trim text nodes and optimize the range boundaries. DOM changes
 40             // may happen at this point.
 41             range.trim();
 42 
 43             // A collapsed range must return NULL at first call.
 44             if (range.collapsed) {
 45                 self.end();
 46                 return NULL;
 47             }
 48         }
 49 
 50         // Create the LTR guard function, if necessary.
 51         if (!rtl && !self._.guardLTR) {
 52             // Gets the node that stops the walker when going LTR.
 53             var limitLTR = range.endContainer[0],
 54                 blockerLTR = limitLTR.childNodes[range.endOffset];
 55             // 从左到右保证在 range 区间内获取 nextSourceNode
 56             this._.guardLTR = function (node, movingOut) {
 57                 // 从endContainer移出去,失败返回false
 58                 if (movingOut && (limitLTR == node || DOM.nodeName(node) == "body")) {
 59                     return false;
 60                 }
 61                 // 达到边界的下一个节点,注意 null 的情况
 62                 // node 永远不能为 null
 63                 return node != blockerLTR;
 64 
 65             };
 66         }
 67 
 68         // Create the RTL guard function, if necessary.
 69         if (rtl && !self._.guardRTL) {
 70             // Gets the node that stops the walker when going LTR.
 71             var limitRTL = range.startContainer[0],
 72                 blockerRTL = ( range.startOffset > 0 ) &&
 73                     limitRTL.childNodes[range.startOffset - 1] || null;
 74 
 75             self._.guardRTL = function (node, movingOut) {
 76                 // 从endContainer移出去,失败返回false
 77                 if (movingOut && (limitRTL == node || DOM.nodeName(node) == "body")) {
 78                     return false;
 79                 }
 80                 // 达到边界的下一个节点,注意 null 的情况
 81                 // node 永远不能为 null
 82                 return node != blockerRTL;
 83             };
 84         }
 85 
 86         // Define which guard function to use.
 87         var stopGuard = rtl ? self._.guardRTL : self._.guardLTR;
 88 
 89         // Make the user defined guard function participate in the process,
 90         // otherwise simply use the boundary guard.
 91         if (userGuard) {
 92             guard = function (node, movingOut) {
 93                 if (stopGuard(node, movingOut) === FALSE) {
 94                     return FALSE;
 95                 }
 96                 return userGuard(node, movingOut);
 97             };
 98         }
 99         else {
100             guard = stopGuard;
101         }
102 
103         if (self.current) {
104             node = this.current[ getSourceNodeFn ](FALSE, type, guard);
105         } else {
106             // Get the first node to be returned.
107 
108             if (rtl) {
109                 node = range.endContainer;
110                 if (range.endOffset > 0) {
111                     node = new Node(node[0].childNodes[range.endOffset - 1]);
112                     if (guard(node[0]) === FALSE) {
113                         node = NULL;
114                     }
115                 } else {
116                     node = ( guard(node, TRUE) === FALSE ) ?
117                         NULL : node._4e_previousSourceNode(TRUE, type, guard, undefined);
118                 }
119             }
120             else {
121                 node = range.startContainer;
122                 node = new Node(node[0].childNodes[range.startOffset]);
123 
124                 if (node.length) {
125                     if (guard(node[0]) === FALSE) {
126                         node = NULL;
127                     }
128                 } else {
129                     node = ( guard(range.startContainer, TRUE) === FALSE ) ?
130                         NULL : range.startContainer._4e_nextSourceNode(TRUE, type, guard, undefined);
131                 }
132             }
133         }
134 
135         while (node && !self._.end) {
136             self.current = node;
137             if (!self.evaluator || self.evaluator(node[0]) !== FALSE) {
138                 if (!breakOnFalseRetFalse) {
139                     return node;
140                 }
141             } else if (breakOnFalseRetFalse && self.evaluator) {
142                 return FALSE;
143             }
144             node = node[ getSourceNodeFn ](FALSE, type, guard);
145         }
146 
147         self.end();
148         return self.current = NULL;
149     }
150 
151     function iterateToLast(rtl) {
152         var node,
153             last = NULL;
154         while (node = iterate.call(this, rtl)) {
155             last = node;
156         }
157         return last;
158     }
159 
160     /**
161      * @name Walker
162      * @param {Editor.Range} range
163      * @class
164      * Walker for DOM.
165      * @memberOf Editor
166      */
167     function Walker(range) {
168         this.range = range;
169 
170         /**
171          * A function executed for every matched node, to check whether
172          * it's to be considered into the walk or not. If not provided, all
173          * matched nodes are considered good.
174          * If the function returns "FALSE" the node is ignored.
175          * @type Function
176          * @memberOf Editor.Walker#
177          */
178         this.evaluator = NULL;// 当前 range 范围内深度遍历的元素调用
179 
180         /**
181          * A function executed for every node the walk pass by to check
182          * whether the walk is to be finished. It's called when both
183          * entering and exiting nodes, as well as for the matched nodes.
184          * If this function returns "FALSE", the walking ends and no more
185          * nodes are evaluated.
186          * @type Function
187          * @memberOf Editor.Walker#
188          */
189         this.guard = NULL;// 人为缩小当前 range 范围
190 
191 
192         /** @private */
193         this._ = {};
194     }
195 
196 
197     S.augment(Walker,
198         /**
199          * @lends Editor.Walker#
200          */
201         {
202             /**
203              * Stop walking. No more nodes are retrieved if this function gets
204              * called.
205              */
206             end:function () {
207                 this._.end = 1;
208             },
209 
210             /**
211              * Retrieves the next node (at right).
212              * @returns {Boolean} The next node or NULL if no more
213              *        nodes are available.
214              */
215             next:function () {
216                 return iterate.call(this);
217             },
218 
219             /**
220              * Retrieves the previous node (at left).
221              * @returns {Boolean} The previous node or NULL if no more
222              *        nodes are available.
223              */
224             previous:function () {
225                 return iterate.call(this, TRUE);
226             },
227 
228             /**
229              * Check all nodes at right, executing the evaluation function.
230              * @returns {Boolean} "FALSE" if the evaluator function returned
231              *        "FALSE" for any of the matched nodes. Otherwise "TRUE".
232              */
233             checkForward:function () {
234                 return iterate.call(this, FALSE, TRUE) !== FALSE;
235             },
236 
237             /**
238              * Check all nodes at left, executing the evaluation function.
239              * 是不是 (不能后退了)
240              * @returns {Boolean} "FALSE" if the evaluator function returned
241              *        "FALSE" for any of the matched nodes. Otherwise "TRUE".
242              */
243             checkBackward:function () {
244                 // 在当前 range 范围内不会出现 evaluator 返回 false 的情况
245                 return iterate.call(this, TRUE, TRUE) !== FALSE;
246             },
247 
248             /**
249              * Executes a full walk forward (to the right), until no more nodes
250              * are available, returning the last valid node.
251              * @returns {Boolean} The last node at the right or NULL
252              *        if no valid nodes are available.
253              */
254             lastForward:function () {
255                 return iterateToLast.call(this);
256             },
257 
258             /**
259              * Executes a full walk backwards (to the left), until no more nodes
260              * are available, returning the last valid node.
261              * @returns {Boolean} The last node at the left or NULL
262              *        if no valid nodes are available.
263              */
264             lastBackward:function () {
265                 return iterateToLast.call(this, TRUE);
266             },
267 
268             reset:function () {
269                 delete this.current;
270                 this._ = {};
271             },
272 
273             // for unit test
274             _iterator:iterate
275 
276         });
277 
278 
279     S.mix(Walker,
280         /**
281          * @lends Editor.Walker
282          */
283         {
284             /**
285              * Whether the to-be-evaluated node is not a block node and does not match given node name map.
286              * @param {Object} customNodeNames Given node name map.
287              * @return {Function} Function for evaluation.
288              */
289             blockBoundary:function (customNodeNames) {
290                 return function (node) {
291                     return !(node.nodeType == DOM.ELEMENT_NODE &&
292                         DOM._4e_isBlockBoundary(node, customNodeNames) );
293                 };
294             },
295 
296             /**
297              * Whether the to-be-evaluated node is a bookmark node OR bookmark node
298              * inner contents.
299              * @param {Boolean} [contentOnly] Whether only test againt the text content of
300              * bookmark node instead of the element itself(default).
301              * @param {Boolean} [isReject] Whether should return 'FALSE' for the bookmark
302              * node instead of 'TRUE'(default).
303              * @return {Function} Function for evaluation.
304              */
305             bookmark:function (contentOnly, isReject) {
306                 function isBookmarkNode(node) {
307                     return  DOM.nodeName(node) == 'span' &&
308                         DOM.attr(node, '_ke_bookmark');
309                 }
310 
311                 return function (node) {
312                     var isBookmark, parent;
313                     // Is bookmark inner text node?
314                     isBookmark = ( node.nodeType == DOM.TEXT_NODE &&
315                         ( parent = node.parentNode ) &&
316                         isBookmarkNode(parent) );
317                     // Is bookmark node?
318                     isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode(node);
319                     // !! 2012-05-15
320                     // evaluator check ===false, must turn it to boolean false
321                     return !!(isReject ^ isBookmark);
322                 };
323             },
324 
325             /**
326              * Whether the node is a text node containing only whitespaces characters.
327              * @param {Boolean} [isReject]
328              */
329             whitespaces:function (isReject) {
330                 return function (node) {
331                     var isWhitespace = node.nodeType == DOM.TEXT_NODE &&
332                         !S.trim(node.nodeValue);
333                     return !!(isReject ^ isWhitespace);
334                 };
335             },
336             /**
337              * Whether the node is invisible in wysiwyg mode.
338              * @param isReject
339              */
340             invisible:function (isReject) {
341                 var whitespace = Walker.whitespaces();
342                 return function (node) {
343                     // Nodes that take no spaces in wysiwyg:
344                     // 1. White-spaces but not including NBSP;
345                     // 2. Empty inline elements, e.g. <b></b> we're checking here
346                     // 'offsetHeight' instead of 'offsetWidth' for properly excluding
347                     // all sorts of empty paragraph, e.g. <br />.
348                     var isInvisible = whitespace(node) ||
349                         node.nodeType == DOM.ELEMENT_NODE && !node.offsetHeight;
350                     return !!(isReject ^ isInvisible);
351                 };
352             }
353         });
354 
355     var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/,
356         isWhitespaces = Walker.whitespaces(),
357         isBookmark = Walker.bookmark(),
358         toSkip = function (node) {
359             var name = DOM.nodeName(node);
360             return isBookmark(node) ||
361                 isWhitespaces(node) ||
362                 node.nodeType == 1 &&
363                     name in dtd.$inline &&
364                     !( name in dtd.$empty );
365         };
366 
367     // Check if there's a filler node at the end of an element, and return it.
368     function getBogus(tail) {
369         // Bogus are not always at the end, e.g. <p><a>text<br /></a></p>
370         do {
371             tail = tail._4e_previousSourceNode();
372         } while (tail && toSkip(tail[0]));
373 
374         if (tail && ( !UA.ie ? tail.nodeName() == "br"
375             : tail[0].nodeType == 3 && tailNbspRegex.test(tail.text()) )) {
376             return tail[0];
377         }
378         return false;
379     }
380 
381     Editor.Utils.injectDom({
382         _4e_getBogus:function (el) {
383             return getBogus(new Node(el));
384         }
385     });
386 
387     Editor.Walker = Walker;
388 
389     return Walker;
390 }, {
391     requires:['./base', './utils', './dom']
392 });
393