记一次造轮子---@组件

336 阅读1分钟

需求

企业微信自建应用-APP端需要实现@提及他人的功能,没有合适的第三方组件,只能自己造轮子了

具体实现

<view contenteditable v-html="val" @keydown="handleKeyDown" @input="inputChange"></view>

contenteditable 可以指定一个可编辑元素,这其实也算是实现一个简化版的富文本编辑器了

判断@字符输入

// 获取当前光标
getCurrentRange() {
    const selection = window.getSelection()
    let range = null
    if (selection && selection.rangeCount > 0) {
        range = selection.getRangeAt(0)
    }
    if (range) {
        const r = range.cloneRange()
        range.collapse(true)
        return range
    }
    return
},
// 判断当前输入的是否是@
inputAtValid(event) {
    const range = getCurrentRange()
    if (range) {
        this.range = range // 保存后续兼容IOS需(伪)实时更新range
        const rangeIndex = range.endOffset
        const text = range.endContainer.innerHTML || range.endContainer.textContent
        if (text.substr(rangeIndex - 1, 1) === '@') {
            return true
        }
    }
    return false
},

添加@人

@要求不可编辑,即

<span contenteditable="false" class="at-name">@张三<span>

上面的HTML直接拼接到输入的值后面,光标会有问题,这时候我们可以创建一个节点在光标处插入,同时把光标移到插入元素的后面

先删除原来输入的@字符

// 去除光标前输入的@
clearAtChart() {
    const range = this.range
    applyRange(range)
    if (range) {
        let offset = (range.endOffset - 1) < 0 ? 0 : range.endOffset - 1
        range.setStart(range.endContainer, offset);
        range.setEnd(range.endContainer, range.endOffset);
        range.deleteContents();
    }
},

光标后插入HTML

insertTextAtSelection(text) {
    const range = this.range
    if (window.getSelection) {
        if (range) {
            var el = document.createElement("span");
            el.innerHTML = text;
            const frag = el.firstChild
            const t = document.createTextNode('')
            range.insertNode(t)
            range.insertNode(frag);
            range.setEndAfter(frag)
            range.collapse(false) // 参数在IE下必传
            applyRange(range)
            this.saveCurrentRange() // 实时保存光标
        }
    }
},

删除

删除的时候需要判断是否是@人,是的话需要整块删除

// 监听键盘事件判断是否是删除
// ...

// 删除触发时处理
handleDelete() {
    const range = this.getCurrentRange()
    if (range) {
        if (range.endOffset >= 1) {
            let a =
                range.endContainer.childNodes[range.endOffset] ||
                range.endContainer.childNodes[range.endOffset - 1]
            if (!a || (a.nodeType === Node.TEXT_NODE && !/^\s?$/.test(a.wholeText))) {
                return
            } else if (a.nodeType === Node.TEXT_NODE) {
                if (a.previousElementSibling) a = a.previousElementSibling
                var ch = [].slice.call(a.childNodes)
            } else {
               var ch = [].slice.call(a.childNodes)
            }
            ch = [].reverse.call(ch)
            ch.unshift(a)
            let last;
            [].some.call(ch, (c) => {
                // 判断最后一个元素是否是@人,这个可以添加一个自定义的属性判断
                if (...) {
                    last = c
                    return true
                }
            })
            if (last) {
                window.event.preventDefault()
                window.event.stopPropagation()
                const r = this.getCurrentRange()
                if (r) {
                    r.setStartBefore(last)
                    r.deleteContents()
                    r.collapse(false)
                    applyRange(r)
                    this.handleInput()
                }
            }
            return
        }
    }
}

注意---光标问题

刚开始功能完成后还信心满满地提测,当时开发的时候是用安卓机自测的,没想到到了IOS上,光标各种问题。要么没了,要么在输入框最后面不动,解决方案是实时保存光标信息,在此基础上去做一些操作

参考

vue-at
Range

最后

这是第一次尝试写文章,说实话感觉有点像流水账哈哈|ू・ω・` )