本例基于react,可是实际上就是用原生js作的。兼容性作到了IE9,可是按照这个思路作是能够作到IE8甚至更低的。html
当我拿到这个需求的时候觉得很简单,就是能够给页面上的文章作记号,好比添加个下划线,或者背景涂色作成荧光笔的样子。前端
由于只须要兼容IE9,因此window.getSelection是支持的。(IE8及如下有其它的获取选中的方法)node
那么思路就是选中文本,点击添加下划线后,经过 window.getSelection.getRangeAt(0) 拿到选中的文本对象,获取到文本后,经过文本对象的 surroundContents 方法来将文本替换为带有class的元素。react
思路很简单,代码一样也很简单。git
CSS代码:程序员
.custom-underline{ border-bottom: 1px solid #f00; font-style: normal; } .nite-writer-pen{ background-color: lightgreen; border-radius: 5px; box-shadow: 0 0 10px lightgreen; font-style: normal; }
JS代码:github
/** * 用元素替换被选中的文本 */ var replaceSelectedStrByEle = function(className){ var selecter = window.getSelection(); var selectStr = selecter.toString(); if (selectStr.trim != "") { var rang = selecter.getRangeAt(0); var ele = document.createElement("i"); ele.className = className; ele.textContent = selectStr rang.surroundContents(ele); } } replaceSelectedStrByEle('nite-writer-pen');
上面的思路实在是过于简单,若是是一个很简单的元素,那么这种作法是没有问题的。浏览器
可是咱们的文章的html结构通常都没有这么简单,好比对于如下状况:app
<p> <p>道可道,很是道。</p> <p>名可名,很是名</p> </p>
若是在页面上我选中的操做以下:dom
那么上面的代码实现就会出现BUG,对于这种跨元素选中的状况,想固然的用元素去替换文本是没用的。
若是你想得更多,好比跨多个元素选中,以及选中元素为更为复杂的html结构,你就会发现这是一个多大的坑。
html结构有多复杂,这个坑就有多深。
其实天坑也不是彻底没有路走,在跨多个元素选中的过程当中,我想给选中的内容加样式,那么就须要获取到全部选中的文本节点,而且批量替换成元素。
可是 window.getSelection.getRangeAt(0) 获取到的range对象只能获取到最开始选中的节点和最后选中的节点的。
那么接下来经过选中的最开始的节点和最后的节点获取到全部的文本节点。
思路就是这么个思路,可是实现起来是很复杂的。
面临深坑,确定不可能硬刚。
毕竟我已经不是当年头铁的愣头青了,作项目是有时间和精力成本的,若是要填掉这个坑,那么加班是不可避免的,最重要的是在有限时间内填的这个坑可能还有各类BUG和兼容性问题。
对待这种天坑,通常就给需求来个作不了三连了。
可是这把我想赢,毕竟这个东西看起来确实简单。
在外人的眼里,如此之简单,分分钟搞定的事情。这都作不了,我还怎么在前端的圈子里继续划水?
因此我要动用程序员的入门技——写轮眼。
然而百度、谷歌无效,根本没有这个解决方案,有的全是些我最初的简单实现。
可是回忆咱们之前见到的各类网页应用与场景,很容易就能想到上面的这种操做咱们是见过的。
那就是从远古IE时代就已经出现的各类富文本编辑器组件。
目标确认,百度的ueditor,这波我要赢。
上github两三下拿到ueditor源码,开始读源码分析代码。
中间过程再也不多说,精简代码,除去一些不须要的代码和兼容性处理后,拿到了五个文件:
即便精简后,代码也很多,大概两三千行。不过其中还有不少注释,压缩后体积并不大。
因为代码比较多,这里就不所有展现了,文章最后会给出github的地址。
这里只给出最后的使用代码:
/** * 添加下划线 */ addUnderline = () => { this.replaceSelectedStrByEle(styles['custom-underline']) } /** * 启用荧光笔 */ enableNiteWriterPen = () => { this.replaceSelectedStrByEle(styles['nite-writer-pen']) } /** * 用元素替换被选中的文本 */ replaceSelectedStrByEle = (className) => { var getRange = () => { var me = window; var range = new Range(me.document); var sel = window.getSelection(); if (sel && sel.rangeCount) { var firstRange = sel.getRangeAt(0); var lastRange = sel.getRangeAt(sel.rangeCount - 1); range.setStart(firstRange.startContainer, firstRange.startOffset) .setEnd(lastRange.endContainer, lastRange.endOffset); } return range } var range = getRange(); range.applyInlineStyle('i', { class: className }); range.select(); }
使用起来仍是比较简单的。
若是咱们选中的是已经被包裹在i元素中的一段文本,那么调用后会发现并无加上class属性。
这是由于富文本编辑器和咱们的需求不同,经过操做想把选中文本变为i元素,而文本外面原本就是i元素了,天然不会进行剩下的操做。
在填充元素的最后会有一个mergeToParent的操做,他会在填充元素的标签和其父级元素的标签同样后将元素替换为文本。
if (parent.tagName == node.tagName || parent.tagName == "A") { //... }
那么这里咱们要修改源码加上一个判断
if ((parent.tagName == node.tagName && parent.className == node.className) || parent.tagName == "A") { //... }
至于其它的逻辑保持不变便可。
这里getRange拿到的对象range在选中内容并替换样式后依然可使用。
能够调用
range.removeInlineStyle('i')
移除以前添加的样式。
也就是说这里若是使用一个命令模式之类的,是能够实现回退操做的。
不过这里仍是有一个坑,就是removeInlineStyle会移除掉选中内容中全部的i元素,因而我修改了
Range.js中removeInlineStyle这个方法,多加了一个className参数,每次去掉i元素时都会判断是否参数等于className。
而后咱们调用时就是
range.removeInlineStyle('i',styles['nite-writer-pen'])
做为一个暗藏天坑的小需求,搞定以后其实还挺有成就感的。
粗略阅读了源码后才发现若是本身作会有多坑,基本上没个三五天下不来,而且在多掉了N根头发后仍然会发现处处都是考虑不周和各类BUG。
那么最后贴上代码的github地址:github地址 。
如文中有谬误,或者您有更有趣的玩法,还望不吝赐教。