转载请注明: TheViper http://www.cnblogs.com/TheViper
改进版:更简单的 编辑器从光标处插入图片(失去焦点后仍然可以在原位置插入)
为什么会有这个需求?
当我们需要一个文本输入框(编辑器),它的功能定位介于专门的富文本编辑器和裸(原生)文本框之间。这时,如果用专门富文本编辑器,如kindeditor,ueditor,显的很大材小用,而且这两个的体积都不小,而体积小的富文本编辑器又是针对现代浏览器的。
贴吧发帖和知乎发问题的编辑器就是典型的这种需求
问题的出现
下面是这个问题的呈现,ie8下,知乎编辑器中插入图片
首先将光标移到已经输入文字的任意位置,然后让光标在编辑器中失去焦点。上传图片,最后看到图片并没有在光标最后在编辑器中的位置。
如果没有失去焦点,或者浏览器是现代浏览器,则不存在这个问题。
网上的解决方案
网上有很多方案,但至少到现在,我没有看到一个成功的。一般问题都出在编辑器失去焦点后,不能“智能”的在光标原位置插入图片。
最接近的方案就是很简单的win.document.execCommand("insertImage", '',data);,但这个只能针对现代浏览器。
我的成功解决方案
先看下效果
firefox
ie8
注意到在kindeditor,ueditor中,都很好的解决了这个问题,那最简单的方法就是从源码中扒出那些代码。ueditor的源码复杂点,多点,就从kindeditor中扒。kindeditor的源码也就5000多行,还没jquery多.
kindeditor里面的思路是每次mouseup,keyup的时候,就重新锁定selection,range.
range的startContainer和endContainer一样,startOffset和endOffset也一样,也就是说创建的range实际上一个里面什么都没有的range,没有选中内容的range当然就是光标了。
self.afterChange(function(e) { cmd.selection(); }); ........ afterChange : function(fn) { var self = this, doc = self.doc, body = doc.body; K(doc).mouseup(fn); return self; }
这个就是最精华的地方,其他都没什么好说的,无非就是对selection,range的封装。
扒下来的代码
1 define('editor', function() { 2 function _extend(child, parent, proto) { 3 if (!proto) { 4 proto = parent; 5 parent = null; 6 } 7 var childProto; 8 if (parent) { 9 var fn = function () {}; 10 fn.prototype = parent.prototype; 11 childProto = new fn(); 12 _each(proto, function(key, val) { 13 childProto[key] = val; 14 }); 15 } else { 16 childProto = proto; 17 } 18 childProto.constructor = child; 19 child.prototype = childProto; 20 child.parent = parent ? parent.prototype : null; 21 } 22 function _iframeDoc(iframe) { 23 return iframe.contentDocument || iframe.contentWindow.document; 24 } 25 var _IERANGE = !window.getSelection; 26 function _updateCollapsed(range) { 27 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); 28 return range; 29 } 30 function _moveToElementText(range, el) { 31 try { 32 range.moveToElementText(el); 33 } catch(e) {} 34 } 35 function _getStartEnd(rng, isStart) { 36 var doc = rng.parentElement().ownerDocument, 37 pointRange = rng.duplicate(); 38 pointRange.collapse(isStart); 39 var parent = pointRange.parentElement(), 40 nodes = parent.childNodes; 41 var startNode = doc, startPos = 0, cmp = -1; 42 var testRange = rng.duplicate(); 43 _moveToElementText(testRange, parent); 44 for (var i = 0, len = nodes.length; i < len; i++) { 45 var node = nodes[i]; 46 cmp = testRange.compareEndPoints('StartToStart', pointRange); 47 if (cmp === 0) { 48 return {node: node.parentNode, offset: i}; 49 } 50 if (node.nodeType == 3) { 51 testRange.moveStart('character', node.nodeValue.length); 52 startPos += node.nodeValue.length; 53 } 54 if (cmp < 0) { 55 startNode = node; 56 } 57 } 58 testRange = rng.duplicate(); 59 _moveToElementText(testRange, parent); 60 testRange.setEndPoint('StartToEnd', pointRange); 61 startPos -= testRange.text.replace(/\r\n|\n|\r/g, '').length; 62 if (cmp > 0 && startNode.nodeType == 3) { 63 var prevNode = startNode.previousSibling; 64 while (prevNode && prevNode.nodeType == 3) { 65 startPos -= prevNode.nodeValue.length; 66 prevNode = prevNode.previousSibling; 67 } 68 } 69 return {node: startNode, offset: startPos}; 70 } 71 function _toRange(rng) { 72 var doc, range; 73 if (_IERANGE) { 74 if (rng.item) { 75 doc = _getDoc(rng.item(0)); 76 range = new KRange(doc); 77 range.selectNode(rng.item(0)); 78 return range; 79 } 80 doc = rng.parentElement().ownerDocument; 81 var start = _getStartEnd(rng, true), 82 end = _getStartEnd(rng, false); 83 range = new KRange(doc); 84 range.setStart(start.node, start.offset); 85 range.setEnd(end.node, end.offset); 86 return range; 87 } 88 var startContainer = rng.startContainer; 89 doc = startContainer.ownerDocument || startContainer; 90 range = new KRange(doc); 91 range.setStart(startContainer, rng.startOffset); 92 range.setEnd(rng.endContainer, rng.endOffset); 93 return range; 94 } 95 function KRange(doc) { 96 this.init(doc); 97 } 98 _extend(KRange,{ 99 init : function(doc) { 100 var self = this; 101 self.startContainer = doc; 102 self.startOffset = 0; 103 self.endContainer = doc; 104 self.endOffset = 0; 105 self.collapsed = true; 106 self.doc = doc; 107 }, 108 setStart : function(node, offset) { 109 var self = this, doc = self.doc; 110 self.startContainer = node; 111 self.startOffset = offset; 112 if (self.endContainer === doc) { 113 self.endContainer = node; 114 self.endOffset = offset; 115 } 116 return _updateCollapsed(this); 117 }, 118 setEnd : function(node, offset) { 119 var self = this, doc = self.doc; 120 self.endContainer = node; 121 self.endOffset = offset; 122 if (self.startContainer === doc) { 123 self.startContainer = node; 124 self.startOffset = offset; 125 } 126 return _updateCollapsed(this); 127 }, 128 setStartBefore : function(node) { 129 return this.setStart(node.parentNode || this.doc, 0); 130 }, 131 setEndAfter : function(node) { 132 return this.setEnd(node.parentNode ||1); 133 }, 134 selectNode : function(node) { 135 return this.setStartBefore(node).setEndAfter(node); 136 }, 137 selectNodeContents : function(node) { 138 return this.setStart(node, 0).setEnd(node, 0); 139 }, 140 collapse : function(toStart) { 141 if (toStart) { 142 return this.setEnd(this.startContainer, this.startOffset); 143 } 144 return this.setStart(this.endContainer, this.endOffset); 145 }, 146 cloneRange : function() { 147 return new KRange(this.doc).setStart(this.startContainer, this.startOffset).setEnd(this.endContainer, this.endOffset); 148 }, 149 toString : function() { 150 var rng = this.get(), str = _IERANGE ? rng.text : rng.toString(); 151 return str.replace(/\r\n|\n|\r/g, ''); 152 }, 153 insertNode : function(node) { 154 var self = this, 155 sc = self.startContainer, so = self.startOffset, 156 ec = self.endContainer, eo = self.endOffset, 157 firstChild, lastChild, c, nodeCount = 1; 158 if (sc.nodeType == 1) { 159 c = sc.childNodes[so]; 160 if (c) { 161 sc.insertBefore(node, c); 162 if (sc === ec) { 163 eo += nodeCount; 164 } 165 } else { 166 sc.appendChild(node); 167 } 168 } else if (sc.nodeType == 3) { 169 if (so === 0) { 170 sc.parentNode.insertBefore(node, sc); 171 if (sc.parentNode === ec) { 172 eo += nodeCount; 173 } 174 } else if (so >= sc.nodeValue.length) { 175 if (sc.nextSibling) { 176 sc.parentNode.insertBefore(node, sc.nextSibling); 177 } else { 178 sc.parentNode.appendChild(node); 179 } 180 } else { 181 if (so > 0) { 182 c = sc.splitText(so); 183 } else { 184 c = sc; 185 } 186 sc.parentNode.insertBefore(node, c); 187 if (sc === ec) { 188 ec = c; 189 eo -= so; 190 } 191 } 192 } 193 if (firstChild) { 194 self.setStartBefore(firstChild).setEndAfter(lastChild); 195 } else { 196 self.selectNode(node); 197 } 198 return self.setEnd(ec, eo); 199 } 200 }); 201 function _getDoc(node) { 202 if (!node) { 203 return document; 204 } 205 return node.ownerDocument || node.document || node; 206 } 207 function _getWin(node) { 208 if (!node) { 209 return window; 210 } 211 var doc = _getDoc(node); 212 return doc.parentWindow || doc.defaultView; 213 } 214 function _getSel(doc) { 215 var win = _getWin(doc); 216 return _IERANGE ? doc.selection : win.getSelection(); 217 } 218 function _getRng(doc) { 219 var sel = _getSel(doc), rng; 220 try { 221 if (sel.rangeCount > 0) { 222 rng = sel.getRangeAt(0); 223 } else { 224 rng = sel.createRange(); 225 } 226 } catch(e) {} 227 if (_IERANGE && (!rng || (!rng.item && rng.parentElement().ownerDocument !== doc))) { 228 return null; 229 } 230 return rng; 231 } 232 function KCmd(range) { 233 this.init(range); 234 } 235 _extend(KCmd,{ 236 init : function(range) { 237 var self = this, doc = range.doc; 238 self.doc = doc; 239 self.win = _getWin(doc); 240 self.sel = _getSel(doc); 241 self.range = range; 242 }, 243 selection : function(forceReset) { 244 var self = this, doc = self.doc, rng = _getRng(doc); 245 self.sel = _getSel(doc); 246 if (rng) { 247 self.range = _range(rng); 248 return self; 249 } 250 return self; 251 }, 252 inserthtml : function(val, quickMode) { 253 var self = this, range = self.range; 254 if (val === '') { 255 return self; 256 } 257 function insertHtml(range, val) { 258 var doc = range.doc, 259 frag = doc.createDocumentFragment(); 260 function parseHTML(htmlStr, fragment) { 261 var div = document.createElement("div"), reSingleTag = /^<(\w+)\s*\/?>$/; 262 htmlStr += ''; 263 if (reSingleTag.test(htmlStr)) { 264 return [ document.createElement(RegExp.$1) ]; 265 } 266 var tagWrap = { 267 option : [ "select" ], 268 optgroup : [ "select" ], 269 tbody : [ "table" ], 270 thead : [ "table" ], 271 tfoot : [ "table" ], 272 tr : [ "table", "tbody" ], 273 td : [ "table", "tbody", "tr" ], 274 th : [ "table", "thead", "tr" ], 275 legend : [ "fieldset" ], 276 caption : [ "table" ], 277 colgroup : [ "table" ], 278 col : [ "table", "colgroup" ], 279 li : [ "ul" ], 280 link : [ "div" ] 281 }; 282 for ( var param in tagWrap) { 283 var tw = tagWrap[param]; 284 switch (param) { 285 case "option": 286 tw.pre = '<select multiple="multiple">'; 287 break; 288 case "link": 289 tw.pre = 'fixbug<div>'; 290 break; 291 default: 292 tw.pre = "<" + tw.join("><") + ">"; 293 } 294 tw.post = "</" + tw.reverse().join("></") + ">"; 295 } 296 var reMultiTag = /<\s*([\w\:]+)/, match = htmlStr.match(reMultiTag), tag = match ? match[1] 297 .toLowerCase() 298 : ""; 299 if (match && tagWrap[tag]) { 300 var wrap = tagWrap[tag]; 301 div.innerHTML = wrap.pre + htmlStr + wrap.post; 302 n = wrap.length; 303 while (--n >= 0) 304 div = div.lastChild; 305 } else { 306 div.innerHTML = htmlStr; 307 } 308 if (/^\s/.test(htmlStr)) 309 div.insertBefore(document 310 .createTextNode(htmlStr.match(/^\s*/)[0]), 311 div.firstChild); 312 if (fragment) { 313 var firstChild; 314 while ((firstChild = div.firstChild)) { 315 fragment.appendChild(firstChild); 316 } 317 return fragment; 318 } 319 return div.children; 320 }; 321 var oFrag = document.createDocumentFragment(); 322 frag.appendChild(parseHTML(val,oFrag)); 323 range.insertNode(frag); 324 } 325 insertHtml(range, val); 326 return self; 327 } 328 }); 329 function _range(mixed) { 330 if (!mixed.nodeName) { 331 return mixed.constructor === KRange ? mixed : _toRange(mixed); 332 } 333 return new KRange(mixed); 334 } 335 function _cmd(mixed) { 336 if (mixed.nodeName) { 337 var doc = _getDoc(mixed); 338 mixed = _range(doc).selectNodeContents(doc.body).collapse(false); 339 } 340 return new KCmd(mixed); 341 } 342 return _cmd; 343 });
使用的话,
var t=$('editor_iframe').contentDocument || $('editor_iframe').contentWindow.document,cmd=_cmd(doc); bind(t,'mouseup keyup',function(e){
if(e.keyCode==13)
$('editor_iframe').focus();//在ie中如果回车键换行,编辑器会失去焦点,不会在原位置插入 cmd.selection(); }); bind($('editor_image'),'click',function(e){ cmd.inserthtml("<img src='http://localhost/my_editor/1.jpg'>"); });
_cmd(doc).inserthtml("<img src='http://localhost/my_editor/1.jpg'>");就行了。
doc=win.document,doc是iframe window的document.
代码其实很少。
最后说下,这种方案是基于iframe的,而贴吧,知乎的编辑器都是基于div contenteditable=true的,这个用上面的方法也行的通。