上周产品经理提出一个需求:
在编辑框中,用户双击或直接选中左括号(右引号)或左引号(有引号),对其包含的所有字符(含括号或引号字符在内)进行高亮;
听到这个需求的时候,我的心理活动是这样的:“卧槽,这什么需求,从来没见过。前端做得了吗?”。 脸上笑嘻嘻,心里妈卖批,假装淡定的对产品经理说,没那么快做,排到3月份的计划去吧~
双手挠头,开始头脑风暴
首先想到的是,搜索时,在搜索结果中对关键词的进行高亮。这个思路很简单,就是在搜索结果的文本中匹配关键词,用个span标签包裹,并给span标签加个样式即可。这个,好像跟需求相去甚远啊!
百度了半天也没找到相关的案例,没事,我们还有时间,可以自己来造个轮子。
正确思路:
选择(select)事件
- 从事件入手。高亮是文本被选中时触发,文本框有个DOM事件——select,表示文本框文本被选中时触发的事件。
如果取得选中的文本:
- 两个属性:selectionStart、selectionEnd。 这两个属性中保存的是基于 0 的数值,表示所选择 文本的范围(即文本选区开头和结尾的偏移量)。
- 要获得选中的文本,可以使用如下代码:
target.value.substring(selectionStart, selectionEnd); // target是文本对象
- IE9+、Firefox、Safari、Chrome 和 Opera 都支持这两个属性。IE8 及之前版本不支持这两个属性, 而是提供了另一种方案,这里不做介绍
如何选择部分文本
- 所有文本框都有一个 setSelectionRange()方法。这个方法接收两个参数:要选择的第一个字符的索引和要选择的最后一个字符之后的字符的索引。
实现
知道了怎么获取选中的文本和如何选中部分文本,接下来就是去实现了。
- 括号跟引号的匹配逻辑是不同的,括号要考虑嵌套问题,而引号不用,因为引号的嵌套没有语义。
- 左括号跟右括号的匹配逻辑也是不同的,如果用户选中的是左括号,则要从前向后匹配。如果是右括号,则要从后往前匹配。
- 引号类似,只是不需要考虑嵌套关系
有了思路,实现就不难了,代码很简单,相信大家应该看一下就懂了,我就不在啰嗦了。
源码:
/**
* 选中括号、引号,对其包含的所有字符进行高亮显示
* @param target 目标对象
*/
function keywordHighlight(target) {
var selectionStart = target.selectionStart, // 选中的文本的起始位置
selectionEnd = target.selectionEnd, // 选中的文本的结束位置
text = target.value, // 输入框中的文本内容
selectValue = target.value.substring(selectionStart, selectionEnd); // 选中的文本
var tmpValue, // 待匹配的文本
tmpArr = [], // 用于存放切割后的待匹配文本的数组
stack = [], // 栈
endIndex; // 结束位置在tmpArr中的下标
// selectValue === '(',从前向后匹配
if (selectValue === '(') {
tmpValue = text.slice(selectionStart);
tmpArr = tmpValue.split("");
for (var i = 0, len = tmpArr.length; i < len; i++) {
if (tmpArr[i] === '(') {
stack.push(tmpArr[i]);
} else if (tmpArr[i] === ')') {
stack.pop();
if (stack.length === 0) {
endIndex = i + 1;
break;
}
}
}
if (endIndex) {
target.setSelectionRange(selectionStart, selectionStart + endIndex);
}
}
// selectValue === ')',从后向前匹配
if (selectValue.trim() === ')') {
tmpValue = text.slice(0, selectionEnd);
tmpArr = tmpValue.split("");
for (var j = tmpArr.length - 1; j >= 0; j--) {
if (tmpArr[j] === ')') {
stack.push(tmpArr[j]);
} else if (tmpArr[j] === '(') {
stack.pop();
if (stack.length === 0) {
endIndex = j;
break;
}
}
}
if (endIndex || endIndex === 0) {
target.setSelectionRange(endIndex, selectionEnd);
}
}
if (selectValue.trim() === '"') {
// 获取引号出现次数
var count = (text.slice(0, selectionEnd).split('"')).length-1;
if (count % 2 !== 0) { // 引号出现次数为奇数,从前向后匹配
// slice获取匹配文本时length+1,获取匹配下标时length+1,故最后 +1+1
endIndex = text.slice(selectionStart + 1,text.length).indexOf('"') + 1 + 1;
if (endIndex) {
target.setSelectionRange(selectionStart, selectionStart + endIndex);
}
} else { // 引号出现次数为偶数,从后向前匹配
tmpValue = text.slice(0, selectionEnd).trim();
endIndex = tmpValue.slice(0,tmpValue.length-1).lastIndexOf('"');
if (endIndex || endIndex === 0) {
target.setSelectionRange(endIndex, selectionEnd);
}
}
}
};
参考文献:《javascript高级程序设计》第三版