web前端实现@提醒功能

web前端实现@提醒功能

@xxx功能很多地方用到,比如微博、微信、qq这些通信的都有用到,那么这个功能是如何实现的?

在开始实现功能前,先整理一下思路:

  • 输入@后将列表选择框显示出来
  • 点击选择框中的选项时,返回输入框
  • 在输入框中显示@xxx
  • 将光标放置@xxx之后
  • 删除@xxx时需要整个@xxx一起删除
  • 需要考虑兼容性问题

主要涉及的方法技术点:

  1. 获取Selection对象,Selection对象表示页面中的文本选区,window.getSelection()获取
  2. 获取光标在文本选区的光标信息Range对象(表示包含节点和部分文本节点的文档片段),Selection.getRangeAt(0)获取、document.createRange()获取、通过构造函数range()获取
  3. 获取光标的位置作为列表选择框的位置,通过jquery.caret的$(ele).caret('offset')获取相对窗口的位置
  4. 保存当前的光标的信息,包括range、offset、selection对象
  5. 选提醒人后,需通过获取之前保留的光标信息,通过range.startContainer获取光标前的文本
  6. range.setStart()、range.setEnd()设置光标的选区起始位置,进行选中@字符
  7. range.deleteContents删除光标选区的内容删除
  8. range.insertNode在光标选区中添加内容
  9. selection.extend将选区的焦点移动到一个特定的位置
  10. selection.collapseToEnd将当前的选区折叠到最尾的一个点

Angular框架实现

<!DOCTYPE html>
<html ng-app='@DEMO'>

<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="http://cdn.bootcss.com/angular.js/1.6.1/angular.js"></script>
    <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.js"></script>
    <script src="http://cdn.bootcss.com/Caret.js/0.3.1/jquery.caret.min.js"></script>
    <style type="text/css">
        * {
            box-sizing: border-box;
            padding: 0;
            margin: 0;
        }

        .demo {
            width: 80%;
            height: 300px;
            border: 2px solid #ccc;
            border-radius: 20px;
            padding: 20px;
            outline: none;
        }

        .demo-wrap {
            position: relative;
        }

        .select-person {
            position: absolute;
            width: 160px;
            border: 1px solid #dcdcdc;
            border-radius: 4px;
        }

        .select-person input {
            width: 100%;
            height: 40px;
            border: none;
            outline: none;
            padding: 0 10px;
        }

        .row {
            display: flex;
            height: 40px;
            border-top: 1px solid #dcdcdc;
            align-items: center;
            cursor: pointer;
        }

        .row:hover {
            background: #ccc;
        }

        .row .col-1 {
            width: 40px;
            flex: 0 0 40px;
        }

        .row .col-2 {
            width: 40px;
            flex: auto;
        }

        .img-wrap {
            width: 30px;
            height: 30px;
            border-radius: 15px;
            background: #4caf50;
            margin: auto;
            font-size: 12px;
            color: #fff;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .at-text {
            color: #4caf50;
        }
    </style>
</head>

<body>
<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-if="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">
                        <span ng-bind="'所有'"></span>
                    </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">
                        <span ng-bind="item.fullName.slice(-2)"></span>
                    </div>
                </div>
                <div class="col-2" ng-bind="item.fullName"></div>
            </li>
        </ul>
    </div>
</div>
</body>
<script type="text/javascript">
    angular.module('@DEMO', [])
        .controller('Controller', ['$scope', '$timeout', function($scope, $timeout) {

            $scope.atList = [{
                fullName: '张三'
            }, {
                fullName: '李四'
            }, {
                fullName: '王五'
            }, {
                fullName: '战前孙'
            }]

            $scope.keyIn = function(e) {
                var selection = getSelection();
                var ele = document.getElementById('demo');
                if (e.code == 'Digit2' && e.shiftKey) {
                    console.log(selection);
                    // 保存光标信息
                    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);

                }
            };

            $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();
            };

            $scope.missFocus = function() {
                setTimeout(function() {
                    $scope.showSelect = false;
                }, 100);
            }
        }]);
</script>
</html>

js封装实现

function AtMembers(iframs){
    this.lastSelection = null;
    this.sPersonPosi = null;
    this.iframeObj = iframs;
    this.clientWidth = $(window).width();
    this.clientHeight = $(window).height();
    this.init = function () {
        this.getDomObj();
        this.events();
    }
}

AtMembers.prototype.getDomObj = function () {
    this.childWindow = this.iframeObj.contentWindow;
    $(this.iframeObj).contents().find("body").attr('id', 'dynamicText');
    this.textareaObj = this.childWindow.document.getElementById('dynamicText');
    this.memberWrap = $('#memberListAt');
}

AtMembers.prototype.events = function () {
    var self = this;

    //输入框的键盘事件
    this.textareaObj.onkeydown = function (event) {
        var selection = self.childWindow.getSelection();
        var e = event ? event : window.event;
        if ((e.code == 'Digit2' || e.which === 50) && e.shiftKey) {
            // 保存光标信息
            self.lastSelection = {
                range: self.childWindow.document.selection ? self.childWindow.document.selection.createRange() : selection.getRangeAt(0),
                offset: selection.focusOffset,
                selection: selection
            };
            // 设置弹出框位置
            var offset = $(self.textareaObj).caret('offset', {iframe: self.iframeObj});

            offset.left += $(self.iframeObj).offset().left;
            offset.top += $(self.iframeObj).offset().top;
            position = $(self.textareaObj).caret('position', {iframe: self.iframeObj});


            self.sPersonPosi = {
                left: self.clientWidth -300 < offset.left ? (self.clientWidth -300)+'px' : offset.left + 'px',
                top: self.clientHeight -450 < offset.top ? (self.clientHeight -450)+'px' : (offset.top + 30) + 'px'
            };
            self.memberWrap.css(self.sPersonPosi).css({display: 'block'});
        } 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 != "member-at-text"){
                removeNode = range.startContainer.previousElementSibling;
            }

            if (range.startContainer.parentElement.className == "member-at-text"){
                removeNode = range.startContainer.parentElement;
            }
            if (removeNode && removeNode.className === "member-at-text"){
                self.textareaObj.removeChild(removeNode);
            }
        }else{
            self.memberWrap.css({display: 'none'});
        }
    }

    //保存
    this.memberWrap.on('click', 'li', function () {
        self.person = {};
        self.person = {
            id: $(this).attr('data-id'),
            name: $(this).find('span').text()
        };

        self.memberWrap.css({display: 'none'});
        $(self.textareaObj).focus();

        // 获取之前保留先来的信息。
        // 需要修改 keyIn 的代码,保存选区以及光标信息,用于获取在光标焦点离开前,光标的位置
        var selection = self.lastSelection.selection;
        var range = self.lastSelection.range;
        var textNode = range.startContainer;

        // 删除 @ 符号。
        range.setStart(textNode, range.endOffset);
        range.setEnd(textNode, range.endOffset + 1);
        range.deleteContents();

        // 生成需要显示的内容,包括一个 span 和一个空格。
        var spanNode1 = document.createElement('a');
        var spanNode2 = document.createElement('span');
        spanNode1.className = 'member-at-text';
        spanNode1.setAttribute('data-id', self.person.id);
        spanNode1.setAttribute('href', '/user/center/'+self.person.id);
        spanNode1.setAttribute('target', '_blank');
        spanNode1.innerHTML = '@' + self.person.name;
        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();

    });

    $(window).resize(function () {
        self.clientWidth = $(window).width();
        self.clientHeight = $(window).height();
    });
}

Seletion的文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Selection

Range的文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Range

caret的github:http://ichord.github.com/Caret.js/

                    https://www.npmjs.com/package/jquery.caret

caret实现获取光标的位置信息(依赖jquery)

  • 获取相对父元素的偏移量

$('#inputor').caret('position');

  • 获取相对文档的偏移量

$('#inputor').caret('offset');

  • 获取指定位置的光标坐标

var fixPos = 20;

$('#inputor').caret('position', fixPos  );

$('#inputor').caret('offset', fixPos );

  • 在iframe的文本编辑

var iframeObj  = $('#iframeId')[0];

$('#inputor').caret('offset',{ iframe: iframeObj });


  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值