本文采用vue,同时增加鼠标点击事件和一些页面小优化
基本结构
新建一个sandbox.vue文件编写功能的基本结构
<div class="content"> <!--文本框--> <div class="editor" ref="divref" contenteditable @keyup="handkekeyup" @keydown="handlekeydown" ></div> <!--选项--> <atdialog v-if="showdialog" :visible="showdialog" :position="position" :querystring="querystring" @onpickuser="handlepickuser" @onhide="handlehide" @onshow="handleshow" ></atdialog> </div> <script> import atdialog from '../components/atdialog' export default { name: 'sandbox', components: { atdialog }, data () { return { node: '', // 获取到节点 user: '', // 选中项的内容 endindex: '', // 光标最后停留位置 querystring: '', // 搜索值 showdialog: false, // 是否显示弹窗 position: { x: 0, y: 0 }// 弹窗显示位置 } }, methods: { // 获取光标位置 getcursorindex () { const selection = window.getselection() return selection.focusoffset // 选择开始处 focusnode 的偏移量 }, // 获取节点 getrangenode () { const selection = window.getselection() return selection.focusnode // 选择的结束节点 }, // 弹窗出现的位置 getrangerect () { const selection = window.getselection() const range = selection.getrangeat(0) // 是用于管理选择范围的通用对象 const rect = range.getclientrects()[0] // 择一些文本并将获得所选文本的范围 const line_height = 30 return { x: rect.x, y: rect.y + line_height } }, // 是否展示 @ showat () { const node = this.getrangenode() if (!node || node.nodetype !== node.text_node) return false const content = node.textcontent || '' const regx = /@([^@\s]*)$/ const match = regx.exec(content.slice(0, this.getcursorindex())) return match && match.length === 2 }, // 获取 @ 用户 getatuser () { const content = this.getrangenode().textcontent || '' const regx = /@([^@\s]*)$/ const match = regx.exec(content.slice(0, this.getcursorindex())) if (match && match.length === 2) { return match[1] } return undefined }, // 创建标签 createatbutton (user) { const btn = document.createelement('span') btn.style.display = 'inline-block' btn.dataset.user = json.stringify(user) btn.classname = 'at-button' btn.contenteditable = 'false' btn.textcontent = `@${user.name}` const wrapper = document.createelement('span') wrapper.style.display = 'inline-block' wrapper.contenteditable = 'false' const spaceelem = document.createelement('span') spaceelem.style.whitespace = 'pre' spaceelem.textcontent = '\u200b' spaceelem.contenteditable = 'false' const clonedspaceelem = spaceelem.clonenode(true) wrapper.appendchild(spaceelem) wrapper.appendchild(btn) wrapper.appendchild(clonedspaceelem) return wrapper }, replacestring (raw, replacer) { return raw.replace(/@([^@\s]*)$/, replacer) }, // 插入@标签 replaceatuser (user) { const node = this.node if (node && user) { const content = node.textcontent || '' const endindex = this.endindex const preslice = this.replacestring(content.slice(0, endindex), '') const restslice = content.slice(endindex) const parentnode = node.parentnode const nextnode = node.nextsibling const previoustextnode = new text(preslice) const nexttextnode = new text('\u200b' + restslice) // 添加 0 宽字符 const atbutton = this.createatbutton(user) parentnode.removechild(node) // 插在文本框中 if (nextnode) { parentnode.insertbefore(previoustextnode, nextnode) parentnode.insertbefore(atbutton, nextnode) parentnode.insertbefore(nexttextnode, nextnode) } else { parentnode.appendchild(previoustextnode) parentnode.appendchild(atbutton) parentnode.appendchild(nexttextnode) } // 重置光标的位置 const range = new range() const selection = window.getselection() range.setstart(nexttextnode, 0) range.setend(nexttextnode, 0) selection.removeallranges() selection.addrange(range) } }, // 键盘抬起事件 handkekeyup () { if (this.showat()) { const node = this.getrangenode() const endindex = this.getcursorindex() this.node = node this.endindex = endindex this.position = this.getrangerect() this.querystring = this.getatuser() || '' this.showdialog = true } else { this.showdialog = false } }, // 键盘按下事件 handlekeydown (e) { if (this.showdialog) { if (e.code === 'arrowup' || e.code === 'arrowdown' || e.code === 'enter') { e.preventdefault() } } }, // 插入标签后隐藏选择框 handlepickuser (user) { this.replaceatuser(user) this.user = user this.showdialog = false }, // 隐藏选择框 handlehide () { this.showdialog = false }, // 显示选择框 handleshow () { this.showdialog = true } } } </script> <style scoped lang="scss"> .content { font-family: sans-serif; h1{ text-align: center; } } .editor { margin: 0 auto; width: 600px; height: 150px; background: #fff; border: 1px solid blue; border-radius: 5px; text-align: left; padding: 10px; overflow: auto; line-height: 30px; &:focus { outline: none; } } </style>
如果添加了点击事件,节点和光标位置获取,需要在【键盘抬起事件】中获取,并保存到data
// 键盘抬起事件 handkekeyup () { if (this.showat()) { const node = this.getrangenode() // 获取节点 const endindex = this.getcursorindex() // 获取光标位置 this.node = node this.endindex = endindex this.position = this.getrangerect() this.querystring = this.getatuser() || '' this.showdialog = true } else { this.showdialog = false } },
新建一个组件,编辑弹窗选项
<template> <div class="wrapper" :style="{position:'fixed',top:position.y +'px',left:position.x+'px'}"> <div v-if="!mocklist.length" class="empty">无搜索结果</div> <div v-for="(item,i) in mocklist" :key="item.id" class="item" :class="{'active': i === index}" ref="usersref" @click="clickat($event,item)" @mouseenter="hoverat(i)" > <div class="name">{{item.name}}</div> </div> </div> </template> <script> const mockdata = [ { name: 'html', id: 'html' }, { name: 'css', id: 'css' }, { name: 'java', id: 'java' }, { name: 'javascript', id: 'javascript' } ] export default { name: 'atdialog', props: { visible: boolean, position: object, querystring: string }, data () { return { users: [], index: -1, mocklist: mockdata } }, watch: { querystring (val) { val ? this.mocklist = mockdata.filter(({ name }) => name.startswith(val)) : this.mocklist = mockdata.slice(0) } }, mounted () { document.addeventlistener('keyup', this.keydownhandler) }, destroyed () { document.removeeventlistener('keyup', this.keydownhandler) }, methods: { keydownhandler (e) { if (e.code === 'escape') { this.$emit('onhide') return } // 键盘按下 => ↓ if (e.code === 'arrowdown') { if (this.index >= this.mocklist.length - 1) { this.index = 0 } else { this.index = this.index + 1 } } // 键盘按下 => ↑ if (e.code === 'arrowup') { if (this.index <= 0) { this.index = this.mocklist.length - 1 } else { this.index = this.index - 1 } } // 键盘按下 => 回车 if (e.code === 'enter') { if (this.mocklist.length) { const user = { name: this.mocklist[this.index].name, id: this.mocklist[this.index].id } this.$emit('onpickuser', user) this.index = -1 } } }, clickat (e, item) { const user = { name: item.name, id: item.id } this.$emit('onpickuser', user) this.index = -1 }, hoverat (index) { this.index = index } } } </script> <style scoped lang="scss"> .wrapper { width: 238px; border: 1px solid #e4e7ed; border-radius: 4px; background-color: #fff; box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); box-sizing: border-box; padding: 6px 0; } .empty{ font-size: 14px; padding: 0 20px; color: #999; } .item { font-size: 14px; padding: 0 20px; line-height: 34px; cursor: pointer; color: #606266; &.active { background: #f5f7fa; color: blue; .id { color: blue; } } &:first-child { border-radius: 5px 5px 0 0; } &:last-child { border-radius: 0 0 5px 5px; } .id { font-size: 12px; color: rgb(83, 81, 81); } } </style>
以上就是如何通过vue实现@人的功能的详细内容,更多关于vue @人功能的资料请关注www.887551.com其它相关文章!
黄山市民网:https://www.huangshanshimin.com/