资讯专栏INFORMATION COLUMN

web 前端 @ 功能 JS 实现分析及其原理

jsyzchen / 799人阅读

摘要:最近为实现一个新功能弄的焦头烂额的实现,在实现后写下些心得,供以后会跳入这坑的同志们参考。选人实现主要涉及步骤为。需要修改的代码,保存选区以及光标信息,用于获取在光标焦点离开前,光标的位置删除符号。这样就完成这一功能了。

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

首先,当让是考虑使用范围,由于项目仅仅需要考虑在 WEBKIT 环境下使用,所以可以不用考虑 IE 这也使得代码少了很多的 if(){}else{} 判断。在Mozilla 开发者网络上发现 selectionrange 这两个关于选区对象和光标对象,结合 Caret(一个用于判断当前光标位置的JS插件)后,一个大致的雏形就浮现出来。

大概就长这样:

先整理思路,捋一捋实现步骤。

大致思路如下:

键入 @ 后将选择框显示出来

将焦点定位在弹出框中的搜索框中

点击选择框中的选项时,返回输入框

输入框中显示 @xxx

将光标定位在 @xxx 之后

删除 @xxx 时需要整个 @xxx 一起删除

由于项目使用了 angular 来构建,所以给的 demo 也是用 angular 来搭建的,但是不论用什么框架,想法有了,那么一切就好办了。

selectionrange 对象的具体使用请参考 MDN 上的相关文章:

selection

range

DEMO页

主要涉及的几个方法:

getSelection(window.getSelectio):获取光标所在的区域(一个div或是一个textarea);

selection.getRangeAt:获取光标所在区域中光标选区的信息;

range.setStart:设置光标选区的起始位置;

range.setEnd:设置光标选区的结束位置;

range.deleteContents:将光标选区选中的内容删除;

range.insertNode:在光标选区中添加内容;

selection.extend:将选区的焦点移动到一个特定的位置;

selection.collapseToEnd:将当前的选区折叠到最末尾的一个点。

html 结构
  • 所有人

样式相关的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 = " ";

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

    }
};

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

这样就完成 @ 这一功能了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86537.html

相关文章

  • 前端基础

    摘要:谈起闭包,它可是两个核心技术之一异步基于打造前端持续集成开发环境本文将以一个标准的项目为例,完全抛弃传统的前端项目开发部署方式,基于容器技术打造一个精简的前端持续集成的开发环境。 这一次,彻底弄懂 JavaScript 执行机制 本文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我。 不论你是javascript新手还是老鸟,不论是面试求职,还是日...

    graf 评论0 收藏0
  • 写一本关于 React.js 的小书

    摘要:因为工作中一直在使用,也一直以来想总结一下自己关于的一些知识经验。于是把一些想法慢慢整理书写下来,做成一本开源免费专业简单的入门级别的小书,提供给社区。本书的后续可能会做成视频版本,敬请期待。本作品采用署名禁止演绎国际许可协议进行许可 React.js 小书 本文作者:胡子大哈本文原文:React.js 小书 转载请注明出处,保留原文链接以及作者信息 在线阅读:http://huzi...

    Scorpion 评论0 收藏0
  • ELSE 技术周刊(2017.11.13期)

    摘要:腾讯空间超分辨率技术为用户节省流量,处理效果和速度超谷歌技术在的标准下,处理速度在提升了,处理效果也有明显提升。此外,也是业界首次实现移动端使用深度神经网络进行超分辨率,并保证图片能够实时进行处理。值得一提的是的对应指标也在名单里。 团队分享 魔幻语言 JavaScript 系列之 call、bind 以及上下文 从一行代码来看看 JavaScript 是一门多么魔幻的语言,顺便谈谈 ...

    caohaoyu 评论0 收藏0
  • 基础 - 收藏集 - 掘金

    摘要:的语言的动态性意味着我们可以使用以上种数据类型表示变换过渡动画实现案例前端掘金以下所有效果的实现方式均为个人见解,如有不对的地方还请一一指出。 读 zepto 源码之工具函数 - 掘金Zepto 提供了丰富的工具函数,下面来一一解读。 源码版本 本文阅读的源码为 zepto1.2.0 $.extend $.extend 方法可以用来扩展目标对象的属性。目标对象的同名属性会被源对象的属性...

    wuaiqiu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<