最近为实现一个新功能弄的焦头烂额 @xxx
的实现,在实现后写下些心得,供之后会跳入这坑的同志们参考。html
首先,当让是考虑使用范围,因为项目仅仅须要考虑在 WEBKIT
环境下使用,因此能够不用考虑 IE
这也使得代码少了不少的 if(){}else{}
判断。在Mozilla 开发者网络上发现 selection
和 range
这两个关于选区对象和光标对象,结合 Caret(一个用于判断当前光标位置的JS插件)后,一个大体的雏形就浮现出来。node
大概就长这样:git
先整理思路,捋一捋实现步骤。github
大体思路以下:web
@
后将选择框显示出来@xxx
@xxx
以后@xxx
时须要整个 @xxx
一块儿删除因为项目使用了 angular
来构建,因此给的 demo
也是用 angular
来搭建的,可是不论用什么框架,想法有了,那么一切就好办了。chrome
selection
和 range
对象的具体使用请参考 MDN
上的相关文章:网络
主要涉及的几个方法:app
<div class="demo-wrap" ng-controller="Controller"> <!-- 文本输入框 --> <div class="demo" id="demo" contenteditable="true" ng-keydown="keyIn($event)"></div> <!-- 带有输入框的选人框 --> <div class="select-person" id="selectPerson" ng-show="showSelect" ng-style="sPersonPosi"> <input type="text" id="searchPersonInput" ng-model="personSearchText" ng-blur="missFocus()"> <ul class="person-wrap"> <li class="row" ng-click="sPersonDone({fullName:'全部人'})"> <div class="col-1"> <div class="img-wrap"> <portrait src="" text="'全部'"></portrait> </div> </div> <div class="col-2">全部人</div> </li> <li class="row" ng-click="sPersonDone(item)" ng-repeat="item in atList | filter :{fullName: personSearchText}"> <div class="col-1"> <div class="img-wrap"> <portrait src="item.img" text="item.fullName.slice(-2)"></portrait> </div> </div> <div class="col-2" ng-bind="item.fullName"></div> </li> </ul> </div> </div>
样式相关的CSS
代码就不放上来了,简要分析下页面结构,一个 contenteditable="true"
的输入框和一个 id="selectPerson"
的选人框。框架
contenteditable="true"
主要是由于想在输入框中插入标签,将 @xxx
内容显示出不一样的颜色(这就须要将 @xxx
放在一个标签中),绑定 keyIn
的键盘输入事件,用于检索用户输入 @
和 backspace
,并作出相应的动做;showSelect
来控制是否显示,遍历显示须要显示的选人,以及使用 input
中的内容来过滤选人。相关代码以下:spa
$scope.keyIn = function(e) { var selection = getSelection(); var ele = $('#demo'); if (e.code == 'Digit2' && e.shiftKey) { $scope.showSelect = true; var offset = ele.caret('offset'); $scope.sPersonPosi = { left: offset.left - 10 + 'px', top: offset.top + 20 + 'px' }; // 让选人框中的搜索框获取焦点 $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } }
实现起来挺简单,代码也不复杂,利用 caret
插件获取到光标位置,将选人框在 @
符号的下方显示出来,并同时实现了步骤中的第二步:将焦点放在搜索框中。
主要涉及步骤为:三、四、5
。
当鼠标点击备选项时须要按顺序进行 三、四、5
步骤,因此需将 三、四、5
这 3
个步骤放在一块儿。
相关代码以下:
$scope.sPersonDone = function(person) { // 成功选人后,关闭选择框,让输入框获取焦点。 $scope.showSelect = false; var ele = $('#demo')[0]; ele.focus(); // 获取以前保留先来的信息。 // 须要修改 keyIn 的代码,保存选区以及光标信息,用于获取在光标焦点离开前,光标的位置 var selection = lastSelection.selection; var range = lastSelection.range; var textNode = range.startContainer; // 删除 @ 符号。 range.setStart(textNode, range.endOffset); range.setEnd(textNode, range.endOffset + 1); range.deleteContents(); // 生成须要显示的内容,包括一个 span 和一个空格。 var spanNode1 = document.createElement('span'); var spanNode2 = document.createElement('span'); spanNode1.className = 'at-text'; spanNode1.innerHTML = '@' + person.fullName; spanNode2.innerHTML = ' '; // 将生成内容打包放在 Fragment 中,并获取生成内容的最后一个节点,也就是空格。 var frag = document.createDocumentFragment(), node, lastNode; frag.appendChild(spanNode1); while ((node = spanNode2.firstChild)) { lastNode = frag.appendChild(node); } // 将 Fragment 中的内容放入 range 中,并将光标放在空格以后。 range.insertNode(frag); selection.extend(lastNode, 1); selection.collapseToEnd(); };
咱们须要的效果是在 @
选人后,将整理好的 @xxx
包装成一个标签,放在原先 @
的位置,因此咱们须要对原先的 $scope.keyIn
方法进行改造,保留原先的光标信息,方便在上面的方法中使用。
改造后的 $scope.keyIn
方法以下:
$scope.keyIn = function(e) { var selection = getSelection(); var ele = $('#demo'); if (e.code == 'Digit2' && e.shiftKey) { $scope.showSelect = true; // 保存光标信息 lastSelection = { range: selection.getRangeAt(0), offset: selection.focusOffset, selection: selection }; $scope.showSelect = true; // 设置弹出框位置 var offset = ele.caret('offset'); $scope.sPersonPosi = { left: offset.left - 10 + 'px', top: offset.top + 20 + 'px' }; $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } }
这里估计挺多人会有疑问,为啥要在生成的标签后面加一个空格,并且这个空格要经过
这样的方式实现。
首先,先解释第一个问题:为啥要在标签后加一个空格?
若是不加空格的话,以后在输入文字会添加在咱们生成的标签中,也就是说若是不加空格来隔断咱们生成的标签,咱们在文本框里所作的操做就是在咱们生成的标签中进行。而加了个空格就为了不该问题的发生,使得文本编辑在正确的编辑框中进行。
第二个问题:为啥不能直接加空格 ' '
,而是经过
,不得不说这是个过个悲伤的事实,仍是碰到了兼容性的问题,在 chrome
下运行好好的代码,在 node-webkit
中就会各类报错。缘由在不断的 defug
后发现了: node-webkit
中,将一个 ' '
添加到 contenteditable="true"
的 div
中会没有啊,坑爹啊有木有!!!呈上以前的代码来祭奠下。
var spanNode1 = document.createElement('span'); var node = document.createTextNode(' '); spanNode1.className = 'at-text'; spanNode1.innerHTML = '@' + person.fullName; var frag = document.createDocumentFragment(); frag.appendChild(spanNode1); frag.appendChild(node); range.insertNode(frag); selection.extend(node, 1);
结果一上 node-webkit
环境各类报错。真是坑了个大爹。缘由是光标定位不许,指定位置超出实际位置,可是 node-webkit
环境确实是能够输入空格的,一看原来是
而
不能经过 createTextNode
来建立,因此就有了以前的哪一个曲线救国的策略了。
终于捋到最后一个步骤了,删除时,须要将一整个标签一块儿删除。因为须要监听键盘的输入,因此就可与以前 keyIn
的代码写在一块儿。
最终的 keyIn
代码为:
$scope.keyIn = function(e) { var selection = getSelection(); var ele = document.getElementById('demo'); if (e.code == 'Digit2' && e.shiftKey) { // 保存光标信息 lastSelection = { range: selection.getRangeAt(0), offset: selection.focusOffset, selection: selection }; $scope.showSelect = true; // 设置弹出框位置 var offset = $(ele).caret('offset'); $scope.sPersonPosi = { left: offset.left + 'px', top: offset.top + 30 + 'px' }; $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } else if (e.code == 'Backspace') { // 删除逻辑 // 1 :因为在建立时默认会在 @xxx 后添加一个空格, // 因此当得知光标位于 @xxx 以后的一个第一个字符后并按下删除按钮时, // 应该将光标前的 @xxx 给删除 // 2 :当光标位于 @xxx 中间时,按下删除按钮时应该将整个 @xxx 给删除。 var range = selection.getRangeAt(0); var removeNode = null; if (range.startOffset <= 1 && range.startContainer.parentElement.className != "at-text") removeNode = range.startContainer.previousElementSibling; if (range.startContainer.parentElement.className == "at-text") removeNode = range.startContainer.parentElement; if (removeNode) ele.removeChild(removeNode); } };
代码的逻辑都写在注释里了,这里就很少说了。
这样就完成 @
这一功能了。