1 /**
  2  * ie selection fix.
  3  * modified from ckeditor core
  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/selectionFix", function (S, Editor) {
 11 
 12     var TRUE = true,
 13         FALSE = false,
 14         NULL = null,
 15         UA = S.UA,
 16         Event = S.Event,
 17         DOM = S.DOM,
 18         Node = S.Node,
 19         KES = Editor.SELECTION;
 20 
 21     /**
 22      * 2012-01-11 借鉴 tinymce
 23      * 解决:ie 没有滚动条时,点击窗口空白区域,光标不能正确定位
 24      */
 25     function fixCursorForIE(editor) {
 26         var started,
 27             win = editor.get("window")[0],
 28             doc = editor.get("document")[0],
 29             startRng;
 30 
 31         // Return range from point or NULL if it failed
 32         function rngFromPoint(x, y) {
 33             var rng = doc.body.createTextRange();
 34 
 35             try {
 36                 rng['moveToPoint'](x, y);
 37             } catch (ex) {
 38                 // IE sometimes throws and exception, so lets just ignore it
 39                 rng = NULL;
 40             }
 41 
 42             return rng;
 43         }
 44 
 45         // Removes listeners
 46         function endSelection() {
 47             var rng = doc.selection.createRange();
 48 
 49             // If the range is collapsed then use the last start range
 50             if (startRng &&
 51                 !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
 52                 startRng.select();
 53             }
 54             Event.remove(doc, 'mouseup', endSelection);
 55             Event.remove(doc, 'mousemove', selectionChange);
 56             startRng = started = 0;
 57         }
 58 
 59         // Fires while the selection is changing
 60         function selectionChange(e) {
 61             var pointRng;
 62 
 63             // Check if the button is down or not
 64             if (e.button) {
 65                 // Create range from mouse position
 66                 pointRng = rngFromPoint(e.pageX, e.pageY);
 67 
 68                 if (pointRng) {
 69                     // Check if pointRange is before/after selection then change the endPoint
 70                     if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
 71                         pointRng.setEndPoint('StartToStart', startRng);
 72                     else
 73                         pointRng.setEndPoint('EndToEnd', startRng);
 74 
 75                     pointRng.select();
 76                 }
 77             } else {
 78                 endSelection();
 79             }
 80         }
 81 
 82         // ie 点击空白处光标不能定位到末尾
 83         // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
 84         Event.on(doc, "mousedown contextmenu", function (e) {
 85             var html = doc.documentElement;
 86             if (e.target === html) {
 87                 if (started) {
 88                     endSelection();
 89                 }
 90                 // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
 91                 if (html.scrollHeight > html.clientHeight) {
 92                     return;
 93                 }
 94                 // S.log("fix ie cursor");
 95                 started = 1;
 96                 // Setup start position
 97                 startRng = rngFromPoint(e.pageX, e.pageY);
 98                 if (startRng) {
 99                     // Listen for selection change events
100                     Event.on(doc, 'mouseup', endSelection);
101                     Event.on(doc, 'mousemove', selectionChange);
102 
103                     win.focus();
104                     startRng.select();
105                 }
106             }
107         });
108     }
109 
110 
111     function fixSelectionForIEWhenDocReady(editor) {
112         var doc = editor.get("document")[0],
113             body = new Node(doc.body),
114             html = new Node(doc.documentElement);
115         //ie 焦点管理不行 (ie9 也不行) ,编辑器 iframe 失去焦点,选择区域/光标位置也丢失了
116         //ie中事件都是同步,focus();xx(); 会立即触发事件处理函数,然后再运行xx();
117 
118         // In IE6/7 the blinking cursor appears, but contents are
119         // not editable. (#5634)
120         // 终于和ck同步了,我也发现了这个bug,ck3.3.2解决
121         if (//ie8 的 7 兼容模式
122             Editor.Utils.ieEngine < 8) {
123             // The 'click' event is not fired when clicking the
124             // scrollbars, so we can use it to check whether
125             // the empty space following <body> has been clicked.
126             html.on('click', function (evt) {
127                 var t = new Node(evt.target);
128                 if (t.nodeName() === "html") {
129                     editor.getSelection().getNative().createRange().select();
130                 }
131             });
132         }
133 
134 
135         // Other browsers don't loose the selection if the
136         // editor document loose the focus. In IE, we don't
137         // have support for it, so we reproduce it here, other
138         // than firing the selection change event.
139 
140         var savedRange,
141             saveEnabled,
142         // 2010-10-08 import from ckeditor 3.4.1
143         // 点击(mousedown-focus-mouseup),不保留原有的 selection
144             restoreEnabled = TRUE;
145 
146         // Listening on document element ensures that
147         // scrollbar is included. (#5280)
148         // or body.on('mousedown')
149         html.on('mousedown', function () {
150             // Lock restore selection now, as we have
151             // a followed 'click' event which introduce
152             // new selection. (#5735)
153             //点击时不要恢复了,点击就意味着原来的选择区域作废
154             restoreEnabled = FALSE;
155         });
156 
157         html.on('mouseup', function () {
158             restoreEnabled = TRUE;
159         });
160 
161         //事件顺序
162         // 1.body mousedown
163         // 2.html mousedown
164         // body  blur
165         // window blur
166         // 3.body focusin
167         // 4.body focus
168         // 5.window focus
169         // 6.body mouseup
170         // 7.body mousedown
171         // 8.body click
172         // 9.html click
173         // 10.doc click
174 
175         // "onfocusin" is fired before "onfocus". It makes it
176         // possible to restore the selection before click
177         // events get executed.
178         body.on('focusin', function (evt) {
179             var t = new Node(evt.target);
180             // If there are elements with layout they fire this event but
181             // it must be ignored to allow edit its contents #4682
182             if (t.nodeName() != 'body')
183                 return;
184 
185             // If we have saved a range, restore it at this
186             // point.
187             if (savedRange) {
188                 // Well not break because of this.
189                 try {
190                     // S.log("body focusin");
191                     // 如果不是 mousedown 引起的 focus
192                     if (restoreEnabled) {
193                         savedRange.select();
194                     }
195                 }
196                 catch (e) {
197                 }
198 
199                 savedRange = NULL;
200             }
201         });
202 
203         body.on('focus', function () {
204             // S.log("body focus");
205             // Enable selections to be saved.
206             saveEnabled = TRUE;
207             saveSelection();
208         });
209 
210         body.on('beforedeactivate', function (evt) {
211             // Ignore this event if it's caused by focus switch between
212             // internal editable control type elements, e.g. layouted paragraph. (#4682)
213             if (evt.relatedTarget)
214                 return;
215 
216             // S.log("beforedeactivate");
217             // Disable selections from being saved.
218             saveEnabled = FALSE;
219             restoreEnabled = TRUE;
220         });
221 
222         // IE before version 8 will leave cursor blinking inside the document after
223         // editor blurred unless we clean up the selection. (#4716)
224 // http://yiminghe.github.com/lite-ext/playground/iframe_selection_ie/demo.html
225 // 需要第一个 hack
226 //            editor.on('blur', function () {
227 //                // 把选择区域与光标清除
228 //                // Try/Catch to avoid errors if the editor is hidden. (#6375)
229 //                // S.log("blur");
230 //                try {
231 //                    var el = document.documentElement || document.body;
232 //                    var top = el.scrollTop, left = el.scrollLeft;
233 //                    doc && doc.selection.empty();
234 //                    //in case if window scroll to editor
235 //                    el.scrollTop = top;
236 //                    el.scrollLeft = left;
237 //                } catch (e) {
238 //                }
239 //            });
240 
241         // IE fires the "selectionchange" event when clicking
242         // inside a selection. We don't want to capture that.
243         body.on('mousedown', function () {
244             // S.log("body mousedown");
245             saveEnabled = FALSE;
246         });
247         body.on('mouseup', function () {
248             // S.log("body mouseup");
249             saveEnabled = TRUE;
250             setTimeout(function () {
251                 saveSelection(TRUE);
252             }, 0);
253         });
254 
255         /**
256          *
257          * @param {Boolean=} testIt
258          */
259         function saveSelection(testIt) {
260             // S.log("saveSelection");
261             if (saveEnabled) {
262                 var sel = editor.getSelection(),
263                     type = sel && sel.getType(),
264                     nativeSel = sel && doc.selection;
265 
266                 // There is a very specific case, when clicking
267                 // inside a text selection. In that case, the
268                 // selection collapses at the clicking point,
269                 // but the selection object remains in an
270                 // unknown state, making createRange return a
271                 // range at the very start of the document. In
272                 // such situation we have to test the range, to
273                 // be sure it's valid.
274                 // 右键时,若前一个操作选中,则该次一直为None
275                 if (testIt && nativeSel && type == KES.SELECTION_NONE) {
276                     // The "InsertImage" command can be used to
277                     // test whether the selection is good or not.
278                     // If not, it's enough to give some time to
279                     // IE to put things in order for us.
280                     if (!doc['queryCommandEnabled']('InsertImage')) {
281                         setTimeout(function () {
282                             //S.log("retry");
283                             saveSelection(TRUE);
284                         }, 50);
285                         return;
286                     }
287                 }
288 
289                 // Avoid saving selection from within text input. (#5747)
290                 var parentTag;
291                 if (nativeSel && nativeSel.type && nativeSel.type != 'Control'
292                     && ( parentTag = nativeSel.createRange() )
293                     && ( parentTag = parentTag.parentElement() )
294                     && ( parentTag = parentTag.nodeName )
295                     && parentTag.toLowerCase() in { input:1, textarea:1 }) {
296                     return;
297                 }
298                 savedRange = nativeSel && sel.getRanges()[ 0 ];
299                 // S.log("monitor ing...");
300                 // 同时检测,不同则 editor 触发 selectionChange
301                 editor.checkSelectionChange();
302             }
303         }
304 
305         body.on('keydown', function () {
306             saveEnabled = FALSE;
307         });
308         body.on('keyup', function () {
309             saveEnabled = TRUE;
310             setTimeout(function () {
311                 saveSelection();
312             }, 0);
313         });
314     }
315 
316     function fireSelectionChangeForNonIE(editor) {
317         var doc = editor.get("document")[0];
318         // In other browsers, we make the selection change
319         // check based on other events, like clicks or keys
320         // press.
321         function monitor() {
322             // S.log("fireSelectionChangeForNonIE in selection/index");
323             editor.checkSelectionChange();
324         }
325 
326         Event.on(doc, 'mouseup keyup', monitor);
327     }
328 
329     /**
330      * 监控选择区域变化
331      * @param editor
332      */
333     function monitorSelectionChange(editor) {
334         // Matching an empty paragraph at the end of document.
335         // 注释也要排除掉
336         var emptyParagraphRegexp =
337             /\s*<(p|div|address|h\d|center)[^>]*>\s*(?:<br[^>]*>| |\u00A0| |(<!--[\s\S]*?-->))?\s*(:?<\/\1>)?(?=\s*$|<\/body>)/gi;
338 
339 
340         function isBlankParagraph(block) {
341             return block._4e_outerHtml().match(emptyParagraphRegexp);
342         }
343 
344         var isNotWhitespace = Editor.Walker.whitespaces(TRUE),
345             isNotBookmark = Editor.Walker.bookmark(FALSE, TRUE);
346         //除去注释和空格的下一个有效元素
347         var nextValidEl = function (node) {
348             return isNotWhitespace(node) && node.nodeType != 8
349         };
350 
351         // 光标可以不能放在里面
352         function cannotCursorPlaced(element) {
353             var dtd = Editor.XHTML_DTD;
354             return element._4e_isBlockBoundary() && dtd.$empty[ element.nodeName() ];
355         }
356 
357         function isNotEmpty(node) {
358             return isNotWhitespace(node) && isNotBookmark(node);
359         }
360 
361         /**
362          * 如果选择了body下面的直接inline元素,则新建p
363          */
364         editor.on("selectionChange", function (ev) {
365             // S.log("monitor selectionChange in selection/index.js");
366             var path = ev.path,
367                 body = new Node(editor.get("document")[0].body),
368                 selection = ev.selection,
369                 range = selection && selection.getRanges()[0],
370                 blockLimit = path.blockLimit;
371 
372             // Fix gecko link bug, when a link is placed at the end of block elements there is
373             // no way to move the caret behind the link. This fix adds a bogus br element after the link
374             // kissy-editor #12
375             if (UA['gecko']) {
376                 var pathBlock = path.block || path.blockLimit,
377                     lastNode = pathBlock && pathBlock.last(isNotEmpty);
378                 if (pathBlock
379                     // style as block
380                     && pathBlock._4e_isBlockBoundary()
381                     // lastNode is not block
382                     && !( lastNode && lastNode[0].nodeType == 1 && lastNode._4e_isBlockBoundary() )
383                     // not pre
384                     && pathBlock.nodeName() != 'pre'
385                     // does not have bogus
386                     && !pathBlock._4e_getBogus()) {
387                     pathBlock._4e_appendBogus();
388                 }
389             }
390 
391             if (!range ||
392                 !range.collapsed ||
393                 path.block) {
394                 return;
395             }
396 
397             // 裸的光标出现在 body 里面
398             if (blockLimit.nodeName() == "body") {
399                 var fixedBlock = range.fixBlock(TRUE, "p");
400                 if (fixedBlock &&
401                     // https://dev.ckeditor.com/ticket/8550
402                     // 新加的 p 在 body 最后,那么不要删除
403                     // <table><td/></table>^ => <table><td/></table><p>^</p>
404                     fixedBlock[0] != body[0].lastChild) {
405                     // firefox选择区域变化时自动添加空行,不要出现裸的text
406                     if (isBlankParagraph(fixedBlock)) {
407                         var element = fixedBlock.next(nextValidEl, 1);
408                         if (element &&
409                             element[0].nodeType == DOM.ELEMENT_NODE &&
410                             !cannotCursorPlaced[ element ]) {
411                             range.moveToElementEditablePosition(element);
412                             fixedBlock._4e_remove();
413                         } else {
414                             element = fixedBlock.prev(nextValidEl, 1);
415                             if (element &&
416                                 element[0].nodeType == DOM.ELEMENT_NODE &&
417                                 !cannotCursorPlaced[element]) {
418                                 range.moveToElementEditablePosition(element,
419                                     // 空行的话还是要移到开头的
420                                     isBlankParagraph(element) ? FALSE : TRUE);
421                                 fixedBlock._4e_remove();
422                             } else {
423                                 // 否则的话,就在文章中间添加空行了!
424                             }
425                         }
426                     }
427                 }
428                 range.select();
429                 // 选择区域变了,通知其他插件更新状态
430                 editor.notifySelectionChange();
431             }
432 
433             /**
434              *  当 table pre div 是 body 最后一个元素时,鼠标没法移到后面添加内容了
435              *  解决:增加新的 p
436              */
437             var doc = editor.get("document")[0],
438                 lastRange = new Editor.Range(doc),
439                 lastPath, editBlock;
440             // 最后的编辑地方
441             lastRange
442                 .moveToElementEditablePosition(body,
443                 TRUE);
444             lastPath = new Editor.ElementPath(lastRange.startContainer);
445             // 不位于 <body><p>^</p></body>
446             if (lastPath.blockLimit.nodeName() !== 'body') {
447                 editBlock = new Node(doc.createElement('p')).appendTo(body);
448                 if (!UA['ie']) {
449                     editBlock._4e_appendBogus();
450                 }
451             }
452         });
453     }
454 
455     return {
456         init:function (editor) {
457             editor.docReady(function () {
458                 // S.log("editor docReady for fix selection");
459                 if (UA.ie) {
460                     fixCursorForIE(editor);
461                     fixSelectionForIEWhenDocReady(editor);
462                 } else {
463                     fireSelectionChangeForNonIE(editor);
464                 }
465             });
466             // 1. 选择区域变化时各个浏览器的奇怪修复
467             // 2. 触发 selectionChange 事件
468             monitorSelectionChange(editor);
469         }
470     };
471 }, {
472     requires:['./base', './selection']
473 });
474