【實(shí)戰(zhàn)】我是如何在輸入框?qū)崿F(xiàn)@ At功能的
作者:InfinityTomorrow 授權(quán)轉(zhuǎn)載
鏈接:https://juejin.cn/post/6982251438332182542
一、前言
最近接手了一個(gè)需求,在評論框中實(shí)現(xiàn) @At通知用戶的功能。這個(gè)可以說是我的知識盲點(diǎn)了,但是其實(shí)很多應(yīng)用都有這類功能了,例如:QQ空間、微博搜索、企業(yè)微信的TAPD...但是一看就不想不做~??(產(chǎn)品經(jīng)理ps:為什么別人可以做你不可以做?)
明確目標(biāo)

二、技術(shù)方案分析
在尋求我們的技術(shù)方案的時(shí)候、我們首先要明確我們想要的功能是什么
你知道自己想要什么,知道要去哪兒、當(dāng)我們把需求、功能、拆解的很細(xì)的時(shí)候可以節(jié)約我們走彎路的時(shí)間(ps:不要問我怎么知道的)
當(dāng)前需求的拆解
按住 shift + @的時(shí)候,彈出通知列表選擇時(shí) @的用戶標(biāo)簽插入當(dāng)前的光標(biāo)位置中生成 @的用戶標(biāo)簽的規(guī)則是:高亮、攜帶用戶ID、一鍵刪除信息、不可以編輯。文本框要隨內(nèi)容自適應(yīng)高度 Android、IOS、Web顯示多端一致。具有擴(kuò)張性,未來評論可能插入圖片文件等....
市面流行方案對比
ps: 方案有很多種方式,適合自己、適合團(tuán)隊(duì)的才是最佳實(shí)踐。沒有完美的方案(ps:只有不聽話的產(chǎn)品經(jīng)理??????) 的產(chǎn)品經(jīng)理??????)
textarea、input(例:新浪微博)流程大概都是(監(jiān)聽keyup, 獲取光標(biāo)位置拆入@的節(jié)點(diǎn)...), 但是...相信我如果你手寫,你不會(huì)快樂的?。?!所以推薦下面的庫給大家、只要稍作改動(dòng)就可以使用啦~~ Tribute.js(推薦, ES6) At.js JQ) contenteditable (例:QQ空間, 掘金)
HTML5新屬性規(guī)定元素內(nèi)容是否可編輯、可以做為編輯器使用,由于時(shí)間原因并沒有深入體會(huì)、感興趣的小伙伴可以看一下以下內(nèi)容 contenteditable-MDN contenteditable實(shí)現(xiàn)編輯器,光標(biāo)、輸入法處理 基于contenteditable技術(shù)實(shí)現(xiàn)@選人功能 富文本 (例:企業(yè)微信TAPD)
支持 文本、富文本、圖片、擁有豐富的配置與強(qiáng)大的API。 因?yàn)榭紤]到擴(kuò)展性與踩坑的深淺、api的豐富程度最終選擇 wangeditor富文本 做為最終的方案。
既然選擇好了方向,那就開沖吧、沖沖沖?。?!
三、準(zhǔn)備工作
本功能是基于wangeditor富文本編輯器來實(shí)現(xiàn)的,本文wangeditor版本4.3.0
npm?i?wangeditor?--save
初始化一下項(xiàng)項(xiàng)目結(jié)構(gòu)~
<template>
????<div?ref="editor">div>
template>
<script>
import?E?from?'wangeditor'
export?default?{
????data()?{
????????return?{
????????????editor:?''
????????}
????},
????mounted()?{
????????this.initEditor()?//?初始化編輯器
????},
????methods:?{
????????initEditor()?{
????????????let?editor?=?new?E(this.$refs.editor)
????????????editor.config.placeholder?=?'寫評論~可手動(dòng)輸入@通知其他人'
????????????editor.config.menus?=?[]?//?顯示菜單按鈕
????????????editor.config.showFullScreen?=?false?//?不顯示全屏按鈕
????????????editor.config.pasteIgnoreImg?=?true?//?如果復(fù)制的內(nèi)容有圖片又有文字,則只粘貼文字,不粘貼圖片。
????????????editor.config.height?=?'100'
????????????editor.config.focus?=?false??//?取消自動(dòng)?focus
????????????editor.create()
????????????this.editor?=?editor
????????????//?銷毀編輯器,定義與銷毀應(yīng)該在同一個(gè)地方,增加閱讀性,方便后期維護(hù)。
????????????this.$once('hook:beforeDestroy',?()?=>?{?
????????????????this.editor.destroy()
????????????????this.editor?=?null
????????????})
????????}
????}
}
script>
拓展知識:
為什么在上文中使用 ”new E(this.$refs.editor)“ 使用ref的方式而不是ID的方式呢?
使用ref的好處是具有良好的可重用性和范圍。因?yàn)閞ef只留在這個(gè)組件中,所以當(dāng)您操作這個(gè)ref時(shí),它不會(huì)干擾其他組件。 如果您使用id,它就有重復(fù)的問題,這就意味著你不可能重用某個(gè)元素。 例:我再生成一個(gè)富文本組件就會(huì)初始化失敗、因?yàn)閕d是唯一的。這就是為什么很多人推薦盡量少用ID的原因。(不要問我為什么知道這個(gè)問題?。?!)。
wangeditor的配置只支持固定高度,如果我們想支持文本框最小高度、文字隨內(nèi)容到最大高度xx時(shí)自適應(yīng)滑動(dòng)怎么做呢?
?editor.config.height?=?'100'
?
<style?lang="scss"?scoped>
::v-deep?.w-e-text-container?{
????min-height:?100px;
????max-height:?300px;
????height:?auto?!important;
????border:?1px?solid?#dbdbdb?!important;
????border-radius:?4px;
????overflow-y:?auto;
}
style>
四、@的功能的實(shí)現(xiàn)
按住shift + @ 的時(shí)候,彈出通知人列表
通過$event 可以獲取鍵盤的keyCode 達(dá)到監(jiān)聽的目的 e.preventDefault 可以阻止我輸入的@字符的默認(rèn)事件 getSelection 可以獲取光標(biāo)的位置、給插入標(biāo)簽一個(gè)坐標(biāo)。 要兼容中文輸入法的時(shí)候@的事件判斷(如:中文輸入法打“哈哈哈@” 這個(gè)時(shí)候不能監(jiān)聽@的事件 ) 中文輸入法的時(shí)候單獨(dú)輸入@的時(shí)
怎么判斷中文輸入?
當(dāng)用戶使用中文輸入法開始輸入中文時(shí),compositionstart事件就會(huì)被觸發(fā)。當(dāng)文中文輸入完成或取消時(shí), compositionend 事件將被觸發(fā)。利用這個(gè)機(jī)制我們就可以判斷是否中文狀態(tài)了
positionstart事件,當(dāng)用戶使用拼音輸入法開始輸入漢字時(shí),這個(gè)事件就會(huì)被觸發(fā)。compositionend事件, 當(dāng)文中文輸入完成時(shí), compositionend 事件將被觸發(fā)。
<template>
????<div
????????ref="editor"
????????@compositionstart="compositionstart"
????????@compositionend="compositionend"
????????@keydown="onKeyDownInput($event)"
????>div>
template>
<script>
export?default?{
????data()?{
????????return?{
????????????isChineseInputMethod:?false?//?是否中文輸入法狀態(tài)中
????????}
????},
????methods:?{
????????//?...code
????????//?中文輸入觸發(fā)
????????compositionstart()?{
????????????this.isChineseInputMethod?=?true
????????},
????????//?中文輸入關(guān)閉
????????compositionend()?{
????????????this.isChineseInputMethod?=?false
????????}
????}
}
記錄我們當(dāng)前的光標(biāo)位置
<template>
????<div
????????ref="editor"
????????@keydown="onKeyDownInput($event)"
????????@click="onClickEditor"
????>div>
template>
<script>
export?default?{
????data()?{
????????return?{
????????????position:?''
????????}
????},
????methods:?{
????????//?初始化編輯器
????????initEditor()?{
????????????//?...?init?code
????????????//?編輯的文本的時(shí)候記錄光標(biāo)。
????????????editor.config.onchange?=?html?=>?{
????????????????//?生成@的標(biāo)簽的時(shí)候會(huì)觸發(fā)渲染、此時(shí)不要記錄光標(biāo)坐標(biāo)
????????????????if?(this.isRendering?==?false)?{
????????????????????this.setRecordCoordinates()?//?記錄坐標(biāo)
????????????????}
????????????}
????????},
????????
????????//?每次點(diǎn)擊獲取更新坐標(biāo)
????????onClickEditor()?{
????????????this.setRecordCoordinates()
????????},
????????
????????//?keydown觸發(fā)事件?記錄光標(biāo)
????????onKeyDownInput(e)?{
????????????const?isCode?=?((e.keyCode?===?229?&&?e.key?===?'@')?||?(e.keyCode?===?229?&&?e.code?===?'Digit2')?||?e.keyCode?===?50)?&&?e.shiftKey
????????????if?(!this.isChineseInputMethod?&&?isCode)?{
????????????????this.setRecordCoordinates()?//?保存坐標(biāo)
????????????}
????????},
????????
????????//?獲取當(dāng)前光標(biāo)坐標(biāo)
????????setRecordCoordinates()?{
????????????try?{
????????????????// getSelection()?返回一個(gè) Selection 對象,表示用戶選擇的文本范圍或光標(biāo)的當(dāng)前位置。
????????????????const?selection?=?getSelection()
????????????????this.position?=?{
????????????????????range:?selection.getRangeAt(0),
????????????????????selection:?selection
????????????????}
????????????}?catch?(error)?{
????????????????console.log(error,?'光標(biāo)獲取失敗了~')
????????????}
????????}
????}
}
script>
@的功能的監(jiān)聽ps:鍵盤的@字符
英文code是 50, 判斷是否按住shift + @鍵 中文輸入法下標(biāo)點(diǎn)符號keyCode都是一樣的:229,推薦使用event.code或event.key作為@的判斷。
//?editor?keydown觸發(fā)事件
onKeyDownInput(e)?{
????//?@的鍵盤時(shí)間判斷
????const?isCode?=?((e.keyCode?===?229?&&?e.key?===?'@')?||?(e.keyCode?===?229?&&?e.code?===?'Digit2')?||?e.keyCode?===?50)?&&?e.shiftKey
????//?判斷狀態(tài)是否不是中文輸入法,并且監(jiān)聽到了@的事件
????if?(!this.isChineseInputMethod?&&?isCode)?{
????????//?記錄當(dāng)前文本光標(biāo)坐標(biāo)位置
????????this.setRecordCoordinates()?//?保存坐標(biāo)
????????//?打開彈窗的方法xxxx,這里就省略了
????????//?this.openXXX..
????}
}
說完@的事件的監(jiān)聽、現(xiàn)在我們可以聊聊怎么生成 @的標(biāo)簽了,而且 @的標(biāo)簽又是再怎么一鍵刪除的?
生成@的用戶標(biāo)簽的規(guī)則是:高亮、攜帶用戶ID、一鍵刪除信息、不可以編輯
/**
*?數(shù)據(jù)結(jié)構(gòu):
*?userList:?[{name:?'壞女人',?uid:?18},?{name:?'好男人',?uid:?888}]
*/
//彈窗列表?-?選人?-?生成@的內(nèi)容
createSelectElement(name,?id,?type?=?'default')?{
????//?獲取當(dāng)前文本光標(biāo)的位置。
????const?{?selection,?range?}?=?this.position
????//?生成需要顯示的內(nèi)容
????let?spanNodeFirst?=?document.createElement('span')
????spanNodeFirst.style.color?=?'#409EFF'
????spanNodeFirst.innerHTML?=?`@${name} `?//?@的文本信息
????spanNodeFirst.dataset.id?=?id?//?用戶ID、為后續(xù)解析富文本提供
????spanNodeFirst.contentEditable?=?false?//?當(dāng)設(shè)置為false時(shí),富文本會(huì)把成功文本視為一個(gè)節(jié)點(diǎn)。
????
????//?需要在字符前插入一個(gè)空格否則、在換行與兩個(gè)@標(biāo)簽連續(xù)的時(shí)候?qū)е聼o法刪除標(biāo)簽
????let?spanNode?=?document.createElement('span');
????spanNode.innerHTML?=?' ';
????//創(chuàng)建一個(gè)新的空白的文檔片段,拆入對應(yīng)文本內(nèi)容
????let?frag?=?document.createDocumentFragment()
????frag.appendChild(spanNode);
????frag.appendChild(spanNodeFirst);
????//?如果是鍵盤觸發(fā)的默認(rèn)刪除面前的@,前文中我們沒有阻止@的生成所以要?jiǎng)h除@的再插入ps:如果你是數(shù)組遍歷的請傳入type 不然會(huì)一直刪除你前面的字符。
????if?(type?===?'default')?{
????????const?textNode?=?range.startContainer;
????????range.setStart(textNode,?range.endOffset?-?1);
????????range.setEnd(textNode,?range.endOffset);
????????range.deleteContents();
????????this.isKeyboard?=?false?//?針對多選的邏輯
????}
????
????//?判斷是否有文本、是否有坐標(biāo)
????if?((this.editor.txt.text()?||?type?===?'default')&&?this.position?&&?range)?{
????????range.insertNode(frag)
????}?else?{
????????//?如果沒有內(nèi)容一開始就插入數(shù)據(jù)特別處理
????????this.editor.txt.append(`${id}"?style="color:?#409EFF"?contentEditable="false">@${name} `)
????}
},
擴(kuò)展知識:
getSelection()表示用戶選擇的文本范圍或光標(biāo)的當(dāng)前位置。Event.returnValue兼容IE取消默認(rèn)事件
到現(xiàn)在我們的核心功能已經(jīng)完成了。通過@人的監(jiān)聽事件,通過我們自定義的標(biāo)簽插入,通過getSelection獲取到的光標(biāo)位置。我就就可以做到:隨時(shí)@ 隨時(shí)插入的功能拉~
五、Android、IOS、Web顯示多端一致
每個(gè)端使用富文本都是不一樣的、那我們應(yīng)該如何做到統(tǒng)一數(shù)據(jù)統(tǒng)一呢?
現(xiàn)在采取的方案是通過解析富文本內(nèi)容生成評論數(shù)組列表。 通過各端解析數(shù)組列表、生成富文本... 兼容換行字符... 雖然不能做到完全統(tǒng)一但是能做到數(shù)據(jù)至少是一致的(現(xiàn)在又覺得、textarea、inpu方案的好了)
/**
*?例?富文本:?@小明?
?喂三點(diǎn)幾拉?@飲茶哥?出來飲茶
*?生成數(shù)組:?
*?[
*????{segment:?'@小明?\n',?userId:?'idxxxxx'},
*????{segment:?'喂三點(diǎn)幾拉',?userId:?null},
*????{segment:?'@飲茶哥',?userId:?'idxxxx'},
*????{segment:?'出來飲茶',?userId:?null}
*?]
*/
//?解析編輯器富文本?生成文本信息數(shù)組
fetchGenerateContentsArray()?{
????//?獲取編輯器的JSON對象
????const?data?=?this.editor.txt.getJSON()
????
????let?contents?=?[]
????//?解析html列表JSON,生成文本對象。
????const?generateArray?=?nodeList?=>?{
????????if?(Array.isArray(nodeList))?{
????????????nodeList.forEach(item?=>?{
????????????????//?對于換行符號處理?處理
?等標(biāo)簽保障顯示一致性
????????????????if?(item?&&?item.tag)?{
????????????????????//?針對富文本列表是特殊換行處理?{tag:p,?children:?[{tag:?'br'}]}?這件換行過濾
????????????????????const?notSpecialLabel?=?item.tag?==?'p'?&&?item.children[0]?&&?item.children[0].tag?==?'br'
????????????????????if?(!notSpecialLabel?&&?['p',?'br'].includes(item.tag)?&&?contents.length)?{
????????????????????????const?index?=?contents.length?-?1
????????????????????????//?在文本中拆入換行符號兼容android、ios的換行字符
????????????????????????contents[index].segment?+=?'\n'?
????????????????????}
????????????????}
????????????????
????????????????//??如果遍歷的屬性是?data-id?有ID的
????????????????if?(item?&&?item.attrs?&&?item.attrs.find(e?=>?e.name?===?'data-id'))?{
????????????????????const?id?=?item.attrs.find(e?=>?e.name?===?'data-id').value
????????????????????const?content?=?item.children?&&?item.children[0]?||?''
????????????????????contents.push({?segment:?content.replaceAll(/(
)|(
)/g,?''),?userId:?id?})
????????????????????return
????????????????}
????????????????
????????????????//?如果children?是數(shù)組則繼續(xù)遞歸遍歷下一層
????????????????if?(Array.isArray(item.children))?{
????????????????????generateArray(item.children)
????????????????????return
????????????????}
????????????????
????????????????//?如果沒有數(shù)組了、就是文本的內(nèi)容
????????????????//?刪除文本中的?
?字符
????????????????if?(item.trim())?{
????????????????????contents.push({segment:?item.replaceAll(/(
)|(
)/g,?''),?userId:?''})
????????????????}
????????????})
????????}
????}
????generateArray(data)
????return?contents
}
將生成的數(shù)組解析成為富文本
//?生成換行符號
createLineBreaks(str,?target)?{
????let?label?=?[]
????while(str.match(target)){
????????str?=?str.replace(target,'')
????????label.push('
')
????}
????return?label.join('')
},
//?解析數(shù)組內(nèi)容?生成富文本
createCommentHtml(data)?{
????//?生成@的標(biāo)簽
????const?anchorPoint?=?(id,?value)?=>?{
????????let?defaultSpan?=?` ${id}"?style="color:?#409EFF"?contentEditable="false">${value}`
????????//?判斷android?ios的換行符,替換為富文本的?
????????if?(/(\r\n)|(\n)/g.test(value))?{
????????????value.replaceAll(/(\r\n)|(\n)/g,?'')
????????????defaultSpan?=?defaultSpan?+?this.createLineBreaks(value,?'\n')
????????}
????????return?defaultSpan
????}
????
????//?將處理的文本放入數(shù)組中、通過join生成字符串。
????const?createHtml?=?[]
????data?&&?data.forEach(item?=>?{
????????//?如果有id用?錨點(diǎn)樣式
????????if?(item.userId)?{
????????????const?json?=?anchorPoint(item.userId,?item.segment)
????????????createHtml.push(json)
????????}?else?{
????????????createHtml.push(item.segment.replaceAll(/(\r\n)|(\n)/g,?'
'))
????????}
????})
????//?清除文本數(shù)據(jù)
????this.resetQuery()
????//?生成內(nèi)容插入到edito中
????this.editor.txt.html(`${createHtml.join('')}
`)
},
//?清除文本數(shù)據(jù)
resetQuery()?{
????this.editor.txt.clear()
}
六. 獲取光標(biāo)的坐標(biāo)在文本中的位置
caret-pos 從 textarea、contentedtiable 或 iframe 正文中獲取插入符號/光標(biāo)的位置/偏移量
import?{?position,?offset?}?from?'caret-pos'
//?獲取當(dāng)前光標(biāo)位置
getPosition?()?{
??const?ele?=?this.editor.$textElem.elems[0]
??const?pos?=?position(ele)
??const?off?=?offset(ele)
??const?parentW?=?ele.offsetWidth
??//?這個(gè)是彈窗列表
??const?childEle?=?document.getElementsByClassName("userPopupList")
??const?childW?=?childEle.offsetWidth
??//?彈框偏移超出父元素的寬高
??if?(parentW?-?pos.left?????this.left?=?off.left?-?childW
??}?else?{
????this.left?=?off.left
??}
??this.top?=?off.top?+?20
}
<div?class="userPopupList"?:style="{left:?left?+?'px',?top:?top?+?'px'}">
????...?you?@?popup?list
div>
七、總結(jié)
不要放棄探尋、探究問題的本質(zhì)。不要小看那些看似“無用”的知識、如果這份只是曾經(jīng)擺在你的面前你沒有拒絕它、此時(shí)你的學(xué)習(xí)成本又該降低多少呢?
這個(gè)功能只是在開發(fā)中擠出來的、很多東西寫的不夠好、不夠完善,希望本文能幫助您在開發(fā)中節(jié)約一點(diǎn)時(shí)間。也歡迎大家提出踴躍的反饋、希望能與大家共進(jìn)步,加油~
作者:InfinityTomorrow 授權(quán)轉(zhuǎn)載 鏈接:https://juejin.cn/post/6982251438332182542
???“分享、點(diǎn)贊、在看” 支持一波??
