vue實現(xiàn)At人文本輸入框示例詳解
知識前置
基于vue手把手教你實現(xiàn)一個擁有@人功能的文本編輯器(其實就是微信群聊的輸入框)
Selection
對象,表示用戶選擇的文本范圍或插入符號的當前
developer.mozilla.org/zh-CN/docs/…
contenteditable
是一個枚舉屬性,表示元素是否可被用戶編輯。
developer.mozilla.org/zh-CN/docs/…
需求分析
- 文本框能夠輸入文本(太簡單了)
- 能夠at人
實現(xiàn)
創(chuàng)建能夠輸入文本的文本框
在這里主要利用 contenteditable
屬性,讓創(chuàng)建的 div
能夠編輯
利用input事件監(jiān)聽數(shù)據(jù)變化,將數(shù)據(jù)同步出去
<!--main.vue!--> <template> <div> <Editor v-model="value"/> </div> </template>
<!--editor.vue!--> <template> <div> <div class="editor" contenteditable="true" @input="input" /> </div> </template> <script> export default { computed: { editor() { return this.$refs.editor || {} } }, methods: { input(e) { this.$emit('input', this.getEditorHtml()) }, getEditorHtml() { return this.editor.innerHTML || '' } } } </script> <style lang="less" scoped> .editor{ overflow-y: auto; background: #F4F6FB; border-radius: 4px; border: 1px solid transparent; min-height: 40px; max-height:200px; padding: 14px 9px; line-height: 20px; &:empty{ &::before{ content:'輸入你想對他/她說的話,然后@她!'; color: #999; } } &:focus{ outline: none; border-color: #3656C6; border-radius: 4px; } } </style>
效果如下圖所示
這個時候我們就實現(xiàn)了一個能夠綁定數(shù)據(jù)的文本輸入框,第一個需求完美實現(xiàn),接下來實現(xiàn)第二個需求(開始折磨)
添加at功能
這里的需求主要分四步走
- 當用戶輸入@字符時,彈出用戶選擇列表
- 當用戶點擊@的人時,收回@列表
- 將@的人嵌入到文本框中
- 刪除@的人時,要直接整塊刪除
首先我們先實現(xiàn)一個用戶選擇的列表,這里主要涉及到的都是界面的編輯和動畫的設置,不展開描述,直接上效果圖**(完整代碼會在文末給出)**
接著我們要改造input函數(shù),檢測當用戶輸入為@
符號時,彈出選擇框
input(e) { if (e.data === '@') { // 彈出用戶選擇框 this.$refs.UserList.show() // 失去焦點,退出手機的軟體鍵盤 this.editor.blur() } this.$emit('input', this.getEditorHtml()) },
當用戶點擊要@的人時,關(guān)閉選擇列表,同時將@人的人插入到文本框中
userItemClick(item) { const dom = this.createAtDom(item) this.$refs.editor.innerHTML = this.$refs.editor.innerHTML + dom.outerHTML this.$refs.UserList.close() }, createAtDom(item) { const dom = document.createElement('span') dom.classList.add('active-text') // 這里的contenteditable屬性設置為false,刪除時可以整塊刪除 dom.setAttribute('contenteditable', 'false') // 將id存儲在dom元素的標簽上,便于后續(xù)數(shù)據(jù)處理 dom.setAttribute('data-id', item.id) dom.innerHTML = ` @${item.name} ` return dom },
效果入下圖所示
相信有不少朋友已經(jīng)發(fā)現(xiàn)問題了,這種方式只能怪將@
的人添加到文本的最末尾,但如果我編輯文本的時候,光標的位置不是在文本的最后,而是在文本之間的某個位置,那此時我們這么添加@
的人就會有點反直覺。
所以我們在彈出選擇列表的時候,要把當前光標所處的位置標記下來,插入時,就插入到對應的位置上。所以此時就要拋出我們本文最重要的一個對象
Selection
對象
我們要利用 Selection
對象的 anchorOffset 屬性去獲取當前焦點的位置,此時我們改造input函數(shù),添加 saveIndex
方法,在彈出文本框失焦之前,保存當前焦點的位置 。
//改造input函數(shù) input(e) { if (e.data === '@') { // 保存焦點位置 this.saveIndex() // 彈出用戶選擇框 this.$refs.UserList.show() // 失去焦點,退出手機的軟體鍵盤 this.editor.blur() } this.$emit('input', this.getEditorHtml()) }, // 添加saveIndex方法 async saveIndex() { // 獲取selection對象 const selection = getSelection() // 保存當前焦點的位置 this.selectionIndex = selection.anchorOffset },
// 改造userItemClick函數(shù) userItemClick(item) { const dom = this.createAtDom(item) this.addData(item) this.$refs.UserList.close() }, // 添加dom節(jié)點到指定位置 addData(item){ const html = this.editor.innerHTML const leftInnerHtml = html.substring(0, this.selectionIndex - 1) const dom = this.createAtDom(item) const rightInnerHtml = html.substring(this.selectionIndex, html.length) this.editor.innerHTML = leftInnerHtml + dom.outerHTML + rightInnerHtml }
這個時候我們就可以把@
的人添加到我們之前光標的位置了,效果如下如所示
但在某天,你突發(fā)奇想,想同時對很多個女神發(fā)出邀請,這個時候你發(fā)現(xiàn),@
多人的時候,出現(xiàn)問題了
我們插入的@
人的節(jié)點被硬生生拆成了字符串,這很明顯跟我們的預期有差別呀,這個時候我們應該分析一下我們編輯時的dom結(jié)構(gòu),如下圖所示
為了便于理解我畫了個簡單的圖
我們在插入dom節(jié)點
之前,文本框的所有內(nèi)容都是屬于editor節(jié)點
下唯一一個textNode節(jié)點,插入dom節(jié)點
之后,editor節(jié)點
新增了一個子節(jié)點,而 Selection.anchorOffset 這個屬性獲取到的焦點位置,實際上是相對于當前所處node節(jié)點
而言的(←理解這個概念,非常重要)
也就是說
我們第一次插入dom節(jié)點
,焦點位置是相對于當前節(jié)點,也就是editor節(jié)點
下的唯一一個textNode節(jié)點計算
第二次插入dom節(jié)點
,焦點位置是相對于當前節(jié)點,也就是當前textNode節(jié)點
計算
后續(xù)插入的dom節(jié)點
,焦點位置計算方式同上
所以當我們有如下需求的時候
Selection.anchorOffset
的返回值是5,而我們的addData
方法,實際上是從editor.innerHtml
的第一個位置開始算,第五個位置剛好插到了span節(jié)點
的里面,所以就出現(xiàn)了上文亂碼的問題。
所以我們解決的方案,就是在保存焦點位置的時候,同時保存當前編輯的那個textNode節(jié)點
,那我們怎么找到當前正在編輯的那個textNode節(jié)點
呢?
Selection
對象提供了一個方法 Selection.containsNode()
mdn文檔是這么描述的:判斷指定的節(jié)點是否包含在 Selection 中 (是否被選中)
在我們這個場景中,通俗點講就是,我這個節(jié)點到底是不是編輯的節(jié)點?是你就返回true
,不是就false
所以我們可以在彈出用戶選擇框之前,遍歷一下editor節(jié)點
的子節(jié)點,找出我們當前編輯的那個textNode節(jié)點
// 改造一下saveIndex async saveIndex() { const selection = getSelection() this.selectionIndex = selection.anchorOffset const nodeList = this.editor.childNodes // 保存當前編輯的dom節(jié)點 for (const [index, value] of nodeList.entries()) { // 這里第二個參數(shù)要配置成true,沒配置有其他的一些小bug,這里不展開講,詳細可以看文檔 if (selection.containsNode(value, true)) { this.dom = value this.domIndex = index } } },
現(xiàn)在當前編輯的節(jié)點和編輯的位置都已經(jīng)保存下來了,剩下的就是把@
人的節(jié)點插入到我們編輯的那個textNode
節(jié)點里面就完成了。
// 改造一下addData方法 addData(item) { const html = this.dom.textContent const leftText = html.substring(0, this.selectionIndex - 1) const dom = this.createAtDom(item) const rightText = html.substring(this.selectionIndex, html.length) this.dom.textContent = leftText + dom.outerHTML + rightText },
然而,當我們再次運行代碼調(diào)試的時候,出現(xiàn)了我們預期外的結(jié)果
是我們代碼有問題嗎?說是其實不算是,說不是,其實也算是(廢話)
其實是因為我們編輯的是textNode節(jié)點
,而textNode節(jié)點
就算包含了dom結(jié)構(gòu)
,他也是把結(jié)構(gòu)當成文本輸出到頁面上,所以在這里
- 我們應該創(chuàng)建一個新的結(jié)構(gòu),也就是我們的文檔片段DocumentFragment
- 然后把我們的節(jié)點結(jié)構(gòu)插入到
DocumentFragment
中 - 接著利用Node.insertBefore()方法,把
DocumentFragment
插入到原來編輯的textNode節(jié)點
之前,再用Node.removeChild()方法把原來編輯的textNode節(jié)點
刪除 - 這樣就可以實現(xiàn)正常的插入
為了方便理解,可以看一下流程圖
addData(item) { const text = document.createDocumentFragment() const span = document.createElement('span') const html = this.dom.textContent // 左邊的節(jié)點 const textLeft = document.createTextNode(html.substring(0, this.selectionIndex - 1) + '') // 這里如果textLeft是個空的文本節(jié)點,會導致@用戶無法刪除,這里添加一個判斷,如果是空,則插入一個空的span節(jié)點 text.appendChild(textLeft.textContent ? textLeft : span) // 加入@人的節(jié)點 text.appendChild(this.createAtDom(item)) // 右邊的節(jié)點 const textRight = document.createTextNode(html.substring(this.selectionIndex, html.length)) textRight.textContent && text.appendChild(textRight) this.editor.insertBefore(text, this.dom) this.editor.removeChild(this.dom) },
當我們處理到這里時,就可以多次at想要at的人,效果如圖
后續(xù)我們要將數(shù)據(jù)提取出來,可以根據(jù)v-model綁定的value進行解析,把插在標簽里的數(shù)據(jù)提取出來,也可以根據(jù)自己的業(yè)務插入一些數(shù)據(jù),這里不是重點,也不展開講
后記
基本上文本編輯器的核心邏輯到這里就講完了,但是這個demo在做的過程中,有好幾個地方做了優(yōu)化,特別是針對移動端軟體鍵盤的進入和離開,還有焦點的對焦和失焦,都做了一些處理,但是在文章里頭沒有展開講
想要詳細了解的大佬們可以到我github倉庫下載源碼 github.com/adouni1996/…
以上就是vue實現(xiàn)At人文本輸入框示例詳解的詳細內(nèi)容,更多關(guān)于vue At人文本輸入框的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ant design的table組件實現(xiàn)全選功能以及自定義分頁
這篇文章主要介紹了ant design的table組件實現(xiàn)全選功能以及自定義分頁,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11vue實現(xiàn)圖片路徑轉(zhuǎn)二進制文件流(binary)
這篇文章主要介紹了vue實現(xiàn)圖片路徑轉(zhuǎn)二進制文件流(binary),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06vue實例配置對象中el、template、render的用法
這篇文章主要介紹了vue實例配置對象中el、template、render的用法,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11