本文采用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 @人功能的资料请关注其它相关文章!
© 版权声明
THE END
暂无评论内容