web 前端 @ 功用 JS 完成剖析及其道理

近来为完成一个新功用弄的焦头烂额 @xxx 的完成,在完成后写下些心得,供以后会跳入这坑的同志们参考。

起首,当让是斟酌运用范围,因为项目仅仅须要斟酌在 WEBKIT 环境下运用,所以能够不必斟酌 IE 这也使得代码少了许多的 if(){}else{} 推断。在Mozilla 开发者收集上发明 selectionrange 这两个关于选区对象和光标对象,连系 Caret(一个用于推断当前光标位置的JS插件)后,一个大抵的雏形就显现出来。

也许就长如许:

《web 前端 @ 功用 JS 完成剖析及其道理》

先整顿思绪,捋一捋完成步骤。

大抵思绪以下:

  1. 键入 @ 后将挑选框显现出来
  2. 将核心定位在弹出框中的搜刮框中
  3. 点击挑选框中的选项时,返回输入框
  4. 输入框中显现 @xxx
  5. 将光标定位在 @xxx 以后
  6. 删除 @xxx 时须要全部 @xxx 一同删除

因为项目运用了 angular 来构建,所以给的 demo 也是用 angular 来搭建的,然则不管用什么框架,主意有了,那末统统就好办了。

selectionrange 对象的详细运用请参考 MDN 上的相干文章:

  1. selection
  2. range
  3. DEMO页

重要触及的几个要领:

  1. getSelection(window.getSelectio):猎取光标地点的地区(一个div或是一个textarea);
  2. selection.getRangeAt:猎取光标地点地区中光标选区的信息;
  3. range.setStart:设置光标选区的肇端位置;
  4. range.setEnd:设置光标选区的完毕位置;
  5. range.deleteContents:将光标选区选中的内容删除;
  6. range.insertNode:在光标选区中增加内容;
  7. selection.extend:将选区的核心移动到一个特定的位置;
  8. selection.collapseToEnd:将当前的选区摺叠到最末端的一个点。

html 构造

<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 中的内容来过滤选人。

完成 @ 挑选

相干代码以下:

$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 插件猎取到光标位置,将选人框在 @ 标记的下方显现出来,并同时完成了步骤中的第二步:将核心放在搜刮框中。

选人完成

重要触及步骤为:3、4、5

当鼠标点击备选项时须要按递次举行 3、4、5 步骤,所以需将 3、4、53 个步骤放在一同。
相干代码以下:

$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 = '&nbsp;';

    // 将天生内容打包放在 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();
        })
    }
}

这里预计挺多人会有疑问,为啥要在天生的标签背面加一个空格,而且这个空格要经由过程 &nbsp; 如许的体式格局完成。

起首,先诠释第一个题目:为啥要在标签后加一个空格?

假如不加空格的话,以后在输入笔墨会增加在我们天生的标签中,也就是说假如不加空格来间隔我们天生的标签,我们在文本框里所做的操纵就是在我们天生的标签中举行。而加了个空格就为了防止该题目的发作,使得文本编辑在准确的编辑框中举行。

第二个题目:为啥不能直接加空格 ' ' ,而是经由过程 &nbsp; ,不得不说这是个过个伤心的现实,照样碰到了兼容性的题目,在 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 环境确实是能够输入空格的,一看原来是 &nbsp;&nbsp; 不能经由过程 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);

    }
};

代码的逻辑都写在解释里了,这里就不多说了。

如许就完成 @ 这一功用了。

    原文作者:aco
    原文地址: https://segmentfault.com/a/1190000007846897
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞