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