This article uses vue, and adds mouse click events and some small page optimizations Basic structure Create a sandBox.vue file to write the basic structure of the function <div class="content"> <!--Text box--> <div class="editor" ref="divRef" contenteditable @keyup="handkeKeyUp" @keydown="handleKeyDown" ></div> <!--Options--> <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: '', // Get the node user: '', // The content of the selected item endIndex: '', // The last cursor position queryString: '', // Search value showDialog: false, // Whether to display the pop-up window position: { x: 0, y: 0 }//Popup window display position} }, methods: { // Get the cursor position getCursorIndex () { const selection = window.getSelection() return selection.focusOffset //Select the offset of focusNode at the beginning}, // Get the node getRangeNode () { const selection = window.getSelection() return selection.focusNode // selected end node}, // The location where the pop-up window appears getRangeRect () { const selection = window.getSelection() const range = selection.getRangeAt(0) // is a generic object for managing selection ranges const rect = range.getClientRects()[0] // Select some text and get the range of the selected text const LINE_HEIGHT = 30 return { x: rect.x, y: rect.y + LINE_HEIGHT } }, // Whether to display @ 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 }, // Get @user 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 }, // Create a label 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) }, // Insert @ tag 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) // Add 0 wide characters const atButton = this.createAtButton(user) parentNode.removeChild(node) // Insert in the text box if (nextNode) { parentNode.insertBefore(previousTextNode, nextNode) parentNode.insertBefore(atButton, nextNode) parentNode.insertBefore(nextTextNode, nextNode) } else { parentNode.appendChild(previousTextNode) parentNode.appendChild(atButton) parentNode.appendChild(nextTextNode) } // Reset the cursor position const range = new Range() const selection = window.getSelection() range.setStart(nextTextNode, 0) range.setEnd(nextTextNode, 0) selection.removeAllRanges() selection.addRange(range) } }, //Keyboard up eventhandkeKeyUp () { 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 } }, //Keyboard press event handleKeyDown (e) { if (this.showDialog) { if (e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'Enter') { e.preventDefault() } } }, // Hide the selection box after inserting the tag handlePickUser (user) { this.replaceAtUser(user) this.user = user this.showDialog = false }, //Hide the selection box handleHide () { this.showDialog = false }, // Display the selection box 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> If a click event is added, the node and cursor position must be obtained in the [Keyboard Up Event] and saved to the data //Keyboard up eventhandkeKeyUp () { if (this.showAt()) { const node = this.getRangeNode() // Get the node const endIndex = this.getCursorIndex() // Get the cursor position this.node = node this.endIndex = endIndex this.position = this.getRangeRect() this.queryString = this.getAtUser() || '' this.showDialog = true } else { this.showDialog = false } }, Create a new component and edit the pop-up options <template> <div class="wrapper" :style="{position:'fixed',top:position.y +'px',left:position.x+'px'}"> <div v-if="!mockList.length" class="empty">No search results</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 } //Keyboard pressed => ↓ if (e.code === 'ArrowDown') { if (this.index >= this.mockList.length - 1) { this.index = 0 } else { this.index = this.index + 1 } } //Keyboard pressed => ↑ if (e.code === 'ArrowUp') { if (this.index <= 0) { this.index = this.mockList.length - 1 } else { this.index = this.index - 1 } } //Keyboard pressed => Enterif (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> The above is the details of how to implement the @人 function through Vue. For more information about Vue @人 function, please pay attention to other related articles on 123WORDPRESS.COM! You may also be interested in:
|
<<: Bootstrap 3.0 study notes CSS related supplement
>>: Linux system file sharing samba configuration tutorial
Table of contents The concept of affairs The stat...
1. Install Docker 1. I installed Centos7 in the v...
First, let me explain the version of MySQL: mysql...
Table of contents 1. Security issues with Docker ...
Achieve results Implementation ideas The melting ...
Swap memory mainly means that when the physical m...
MySQL is divided into installation version and fr...
This article shares the specific code of vue+echa...
How much do you know about HTML? If you are learni...
First look at the effect: Code: 1.html <div cl...
Environmental Description: There is a running MyS...
Preface The requirement implemented in this artic...
When using a cloud server, we sometimes connect t...
1. Packaging Vue project Enter the following name...
FTP and SFTP are widely used as file transfer pro...