天坑之路:用js给选中文字添加样式
前言
本例基于react,但是实际上就是用原生js做的。兼容性做到了IE9,但是按照这个思路做是可以做到IE8甚至更低的。
需求与最初的思路
当我拿到这个需求的时候以为很简单,就是可以给页面上的文章做记号,比如添加个下划线,或者背景涂色做成荧光笔的样子。
因为只需要兼容IE9,所以window.getSelection是支持的。(IE8及以下有其它的获取选中的方法)
那么思路就是选中文本,点击添加下划线后,通过 window.getSelection.getRangeAt(0) 拿到选中的文本对象,获取到文本后,通过文本对象的 surroundContents 方法来将文本替换为带有class的元素。
初步的实现
思路很简单,代码同样也很简单。
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代码:
/**
* 用元素替换被选中的文本
*/
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结构一般都没有这么简单,比如对于以下情况:
<p>
<p>道可道,非常道。</p>
<p>名可名,非常名</p>
</p>
如果在页面上我选中的操作如下:

那么上面的代码实现就会出现BUG,对于这种跨元素选中的情况,想当然的用元素去替换文本是没用的。
如果你想得更多,比如跨多个元素选中,以及选中元素为更为复杂的html结构,你就会发现这是一个多大的坑。
html结构有多复杂,这个坑就有多深。
思路的僵局与写轮眼
其实天坑也不是完全没有路走,在跨多个元素选中的过程中,我想给选中的内容加样式,那么就需要获取到所有选中的文本节点,并且批量替换成元素。
但是 window.getSelection.getRangeAt(0) 获取到的range对象只能获取到最开始选中的节点和最后选中的节点的。
那么接下来通过选中的最开始的节点和最后的节点获取到所有的文本节点。
思路就是这么个思路,但是实现起来是很复杂的。
面临深坑,肯定不可能硬刚。
毕竟我已经不是当年头铁的愣头青了,做项目是有时间和精力成本的,如果要填掉这个坑,那么加班是不可避免的,最重要的是在有限时间内填的这个坑可能还有各种BUG和兼容性问题。
对待这种天坑,一般就给需求来个做不了三连了。
但是这把我想赢,毕竟这个东西看起来确实简单。
在外人的眼里,如此之简单,分分钟搞定的事情。这都做不了,我还怎么在前端的圈子里继续划水?
所以我要动用程序员的入门技——写轮眼。

然而百度、谷歌无效,根本没有这个解决方案,有的全是些我最初的简单实现。

但是回忆我们以前见到的各种网页应用与场景,很容易就能想到上面的这种操作我们是见过的。
那就是从远古IE时代就已经出现的各种富文本编辑器组件。
目标确认,百度的ueditor,这波我要赢。

分析ueditor与复制
上github两三下拿到ueditor源码,开始读源码分析代码。
中间过程不再多说,精简代码,除去一些不需要的代码和兼容性处理后,拿到了五个文件:
- browser.js (浏览器版本判断,用于做兼容性处理)
- domUtils.js (dom操作)
- dtd.js (节点的类型与元素判断)
- Range.js (封装的选中范围对象)
- utils.js (工具类)
即使精简后,代码也不少,大概两三千行。不过其中还有很多注释,压缩后体积并不大。
由于代码比较多,这里就不全部展示了,文章最后会给出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元素的处理做的一些修改
如果我们选中的是已经被包裹在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地址:项目地址 。
如文中有谬误,或者您有更有趣的玩法,还望不吝赐教。
天坑之路:用js给选中文字添加样式的更多相关文章
- js 控制选中文字
//脚本获取网页中选中文字 var word = document.selection.createRange().text; //获取选中文字所在的句子 var range = documen ...
- 不让复制是不可能的----js获取选中文字
在360百科.知乎上经常会遇见禁止复制文本的情形,这能挡住一部分人复制,却挡不住程序员的复制. HTML都给我了,难道一小段文本我都拿不下来吗? F12打开控制台,然后选中文本,在控制台下粘贴以下代码 ...
- js实现选中文字 分享功能
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- 用css改变鼠标选中文字的样式
打开一个页面,选中一段文字,会出现系统默认的蓝色背景和白色文字,如下图: 那么 这种选中效果我们怎么去自定制呢,比如我想让文字选中的时候背景是绿色,文字是白色 我们在css文件中插入如下代码: ::- ...
- js实现分页列表添加样式
<script> var dUrl=window.location.href; var cUrl=(dUrl.substring(0, dUrl.indexOf('list_'))); v ...
- 原生js移除或添加样式
样式效果如下,点击商品详情 添加样式active 代码 <!doctype html> <html lang="en"> <head> < ...
- js 选中文字
选中文字,文字背景是蓝色 当前点击的元素: var e = e || event; var tag = e.target || e.srcElement; 选中文字:window.getSelecti ...
- [js高手之路]Node.js+jade+mongoose实战todolist(分页,ajax编辑,删除)
该系列文章索引: [js高手之路]node js系列课程-创建简易web服务器与文件读写 [js高手之路]node js系列课程-图解express+supervisor+ejs用法 [js高手之路] ...
- marquee实现文字移动效果;js+div实现文字无缝移动效果
1.marquee实现文字移动: <marquee width="220px;" scrollamount="5" onmouseover="t ...
随机推荐
- Django APP打包重用
引言 有时候,我们需要将自己写的app分发(dist)给同事,分享给朋友,或者在互联网上发布,这都需要打包.分发我们的app. Django的子系统重用是基于app级别的.也就是一个项目可以包含多个互 ...
- Troubleshooting SQL Server RESOURCE_SEMAPHORE Waittype Memory Issues
前言: 本文是对博客https://www.mssqltips.com/sqlservertip/2827/troubleshooting-sql-server-resourcesemaphore-w ...
- 爬虫入门实例:利用requests库爬取笔趣小说网
w3cschool上的来练练手,爬取笔趣看小说http://www.biqukan.com/, 爬取<凡人修仙传仙界篇>的所有章节 1.利用requests访问目标网址,使用了get方法 ...
- 智能合约 solidity 开发的环境基本搭建
以太坊Dapp开发快速入门 以太坊为开源社区,虽然设计东西都很优秀,但是组件十分的杂乱,因此下面首先简单介绍下以太坊的一些常用组件以及各种工具介绍 Geth Geth是由以太坊基金会提供的官方客户端软 ...
- Windows10家庭版连接远程桌面出现credssp加密oracle修正问题
我发现我的win10不能连接别人的远程桌面,问题如下: 1.windows10家庭版需要修改注册表,家庭版没有组织策略: 2.win+R打开快速启动命令行输入"regedit": ...
- LeetCode算法题-Palindrome Linked List(Java实现)
这是悦乐书的第196次更新,第202篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第58题(顺位题号是234).给出一个单链表,确定它是否是回文.例如: 输入:1-> ...
- nginx基本配置与参数说明
user nobody; #启动进程,通常设置成和cpu的数量相等 worker_processes 1; #全局错误日志及PID文件 #error_log logs/error.log; # ...
- 官网下载Git方法
最近去官网下载Git,奇慢,下到一半直接挂掉,挂VPN也是一样 https://git-scm.com/ 今天学到一个方法,下载速度可以达到2m/s,那就是复制下载地址,用迅雷下载,可能是迅雷有P2 ...
- Java面试知识点之虚拟机篇(一)
前言:Java虚拟机的重要性不言而喻,不管是在实际工作中,还是面试中. 1.JVM架构 要点: 主要了解Java虚拟机运行时数据区:程序计数器.Java虚拟机栈.本地方法栈.Java堆和方法区. 参考 ...
- Loj#6183. 看无可看
Loj#6183. 看无可看 题目描述 首先用特征根求出通项公式\(A_n=p\cdot 3^n+q\cdot(-1)^n\).通过给定的\(f_0,f_1\)可以解出\(p,q\). 然后我们要求的 ...