2019/8/6


问题描述


简单描述

今天做了一个需求,就是使用textarea进行文本输入时,需要去后台请求字典项来自动完成一些预留的统一本文。刚开始感觉还蛮容易的,但是做到后面坑越来越大,因此记录一下详细的过程。(一个菜鸡后端程序猿,被逼着写前端,巨难受)

需要注意的点
  1. 由于业务需要,前端的textarea对应的数据是一个数组,即textarea有多项。【如果是只有一个对象的话会很容易】
  2. 前端监听字段变化有很多种办法,比如使用watch监听数组变化、computer计算属性、监听键盘事件,甚至使用jquery来监听焦点及文本变化等等。
  3. 需要自动完成提示框能够根据当前文本所在位置来进行显示,类似于QQ的@功能及各种输入法的功能。
  4. 可以考虑使用键盘上下键来进行选择,按回车键选择自动回复的内容,这样可以很大程度的方便用户。
成品展示

在这里插入图片描述

参考文献

如何实现textarea中输入@在当前文本的右下方出现一个div,里面选择人名

问题构思


  •  为textarea设置文本变化监听事件,并获取到当前textarea的数组下标。
  •  在数据库中新增自动回复的数据,使用like 'xxx%'进行匹配。(业务需求,只考虑开头匹配,中间文本不支持)。
  •  根据输入的文本,向后台请求自动完成字段。(由于有多个,因此返回值为一个List数组)
  •  将请求到的数组,使用v-for渲染在需要展示的ul组件上。
  •  获取当前textarea中光标所在的位置,便于对齐ul组件
  •  根据选中的li,将textarea对应数组的内容替换成当前选中li的文本。
    扩展选项
  •  监听键盘上下键,使得按上下键可以上下选择自动回复的文本。
  •  监听键盘回车键,按下回车也可以达到和点击li一样的效果。

【粗体标注的是本文的重点内容,删除线不在本文的介绍范围内】

解决方案及步骤


监听textarea文本变化

由于业务需要,要获取到当前输入的文本值, textarea对应的数组下标以及触发当前文本变化的DOM。 因此,watch和computer方法不太适用(也可能是我没使用好,但他们两都获取不到数组下标以及DOM)。最终我采用了监听键盘事件的方法。尽管功能实现了,但仍旧感觉有所欠缺。。。
使用vue监听textarea键盘事件代码如下:

<textarea style="margin-top: 10px" class="layui-textarea" v-for="(it, dex) in newApprove" :value="it" 
v-model="newApprove[dex]" @keyup="autoCompletion(it, dex, $event)"></textarea>

其中newApprove为一个字符串数组:newApprove: [""],
autoCompletion用于监听文本的变化。方法第一个参数为当前文本、第二个为数组下标,第三个为事件。

atuoCompletion方法如下:

autoCompletion: function (text, index, e) {
    // 监听上键与下键,拦截默认事件并阻止冒泡
    if(e.keyCode === 38 || e.keyCode === 40) { 
        e.stopPropagation();
        e.preventDefault();
        return false; 
    }   
    var list = $("#list");
    // 键盘变化时,隐藏ul
    list.hide();
    clearTimeout(this.timer);  //清除延迟执行
     //设置延迟500毫秒执行
    this.timer = setTimeout(()=>{
        // 保存当前数组下标
        vm.newApproveIndex = index;
        if(text.length > 0) {
            // 请求快捷回复内容, 使用回调来处理成功返回的数据
            getAutoCompletionData(text, function (list) {
                //后端返回的数据,数据类型为List
                vm.autoCompletionData = list;
                // 触发显示选项框事件, 并绑定单击事件
                showAutoUI(text, e);
            });
        }
    }, 500);
},

渲染获取到的数据

下面的html是当前业务用到的所有html,其中layui是一个前端开源框架,有需要的可以百度下载,不需要的删除即可。

<!-- 这里是一个大的div -->
<div class="layui-field-box box">
    <textarea style="margin-top: 10px" class="layui-textarea" v-for="(it, dex) in newApprove" :value="it" 
v-model="newApprove[dex]" @keyup="autoCompletion(it, dex, $event)"></textarea>
    <!-- 使用ul-li渲染获取到的自动回复内容 -->
    <ul id="list" >
        <li id="list_li" v-for="(item, index) in autoCompletionData" :tabindex=index 
@click="selectedAutoState(item.optName)">{{item.optName}}                 </li>    
    </ul> 
    <!-- 用于点击事件,每点击一次增加一个textarea -->
    <div style="text-align: right;">
        <i v-if="data.xmlJson.isManyApproval" class="layui-icon layui-icon-add-circle" style="font-size: 30px; color: 
#1E9FFF;" @click="addApproveDetail()"></i>
    </div>
</div>

获取当前光标位置

获取光标位置需要在监听文本事件,即在showAutoUI(text, e); 方法内部。
以下是showAutoUI方法的代码。第一个参数为变化之后的文本,第二个为当前的点击事件。

function showAutoUI(text, e) {
    var curTarget = e.target;
    var list = $("#list");
    // 获取当前光标所处位置    
    var position = getPosition(curTarget);   
    // 光标处于文本最后    
    if(position === text.length){
        // 获取当前DOM的所有style
        var iStyle = window.getComputedStyle(curTarget);        
        //把双字节的替换成两个单字节的然后再获得长度        
        var len = (text || '').replace(/[^\x00-\xff]/g,"01").length / 2;        
        var fz = parseFloat(iStyle.fontSize);        
        var wd = parseFloat(iStyle.width);        
        var lh = parseFloat(iStyle.lineHeight);        
        list.css("left", fz * ( len % wd) > wd ? wd : fz * (len % wd) + curTarget.offsetLeft + 5 + "px");        
        list.css("top", Math.ceil(len / wd) * lh +  curTarget.offsetTop + 5 +"px");
        list.find("li,.select").each(function({ 
            $(this).removeClass("select")        
        });
        list.show();
    }
}

getPosition即获取光标方法,其中参数为使当前文本改动的DOM,即textarea中拥有光标的那一个。getPosition方法代码如下:

//输入框获取光标
function getPosition(element) {    
    var cursorPos = 0;    
    if (document.selection) {//IE        
        var selectRange = document.selection.createRange();                    selectRange.moveStart('character', -element.value.length);
        cursorPos = selectRange.text.length;
    } else if (element.selectionStart || element.selectionStart == '0'{       
        cursorPos = element.selectionStart;   
    }    
    return cursorPos;
}

未完待续~

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

You got to put the past behind you before you can move on.