【實(shí)戰(zhàn)】我是如何在輸入框?qū)崿F(xiàn) @ 功能的
以下內(nèi)容來自公眾號逆鋒起筆,關(guān)注每日干貨及時送達(dá)
作者:InfinityTomorrow 授權(quán)轉(zhuǎn)載
鏈接:https://juejin.cn/post/6982251438332182542
一、前言
明確目標(biāo)

二、技術(shù)方案分析
在尋求我們的技術(shù)方案的時候、我們首先要明確我們想要的功能是什么
當(dāng)前需求的拆解
按住 shift + @的時候,彈出通知列表選擇時 @的用戶標(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)...), 但是...相信我如果你手寫,你不會快樂的?。?!所以推薦下面的庫給大家、只要稍作改動就可以使用啦~~ Tribute.js(推薦, ES6) At.js JQ) contenteditable (例:QQ空間, 掘金)
HTML5新屬性規(guī)定元素內(nèi)容是否可編輯、可以做為編輯器使用,由于時間原因并沒有深入體會、感興趣的小伙伴可以看一下以下內(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.0npm?i?wangeditor?--save
<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?=?'寫評論~可手動輸入@通知其他人'
????????????editor.config.menus?=?[]?//?顯示菜單按鈕
????????????editor.config.showFullScreen?=?false?//?不顯示全屏按鈕
????????????editor.config.pasteIgnoreImg?=?true?//?如果復(fù)制的內(nèi)容有圖片又有文字,則只粘貼文字,不粘貼圖片。
????????????editor.config.height?=?'100'
????????????editor.config.focus?=?false??//?取消自動?focus
????????????editor.create()
????????????this.editor?=?editor
????????????//?銷毀編輯器,定義與銷毀應(yīng)該在同一個地方,增加閱讀性,方便后期維護(hù)。
????????????this.$once('hook:beforeDestroy',?()?=>?{?
????????????????this.editor.destroy()
????????????????this.editor?=?null
????????????})
????????}
????}
}
script>
為什么在上文中使用 ”new E(this.$refs.editor)“ 使用ref的方式而不是ID的方式呢?
使用ref的好處是具有良好的可重用性和范圍。因?yàn)閞ef只留在這個組件中,所以當(dāng)您操作這個ref時,它不會干擾其他組件。 如果您使用id,它就有重復(fù)的問題,這就意味著你不可能重用某個元素。 例:我再生成一個富文本組件就會初始化失敗、因?yàn)閕d是唯一的。這就是為什么很多人推薦盡量少用ID的原因。(不要問我為什么知道這個問題?。?!)。
wangeditor的配置只支持固定高度,如果我們想支持文本框最小高度、文字隨內(nèi)容到最大高度xx時自適應(yī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 + @ 的時候,彈出通知人列表通過$event 可以獲取鍵盤的keyCode 達(dá)到監(jiān)聽的目的 e.preventDefault 可以阻止我輸入的@字符的默認(rèn)事件 getSelection 可以獲取光標(biāo)的位置、給插入標(biāo)簽一個坐標(biāo)。 要兼容中文輸入法的時候@的事件判斷(如:中文輸入法打“哈哈哈@” 這個時候不能監(jiān)聽@的事件 ) 中文輸入法的時候單獨(dú)輸入@的時
compositionstart事件就會被觸發(fā)。當(dāng)文中文輸入完成或取消時, compositionend 事件將被觸發(fā)。利用這個機(jī)制我們就可以判斷是否中文狀態(tài)了positionstart事件,當(dāng)用戶使用拼音輸入法開始輸入漢字時,這個事件就會被觸發(fā)。compositionend事件, 當(dāng)文中文輸入完成時, 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
????????}
????}
}
<template>
????<div
????????ref="editor"
????????@keydown="onKeyDownInput($event)"
????????@click="onClickEditor"
????>div>
template>
<script>
export?default?{
????data()?{
????????return?{
????????????position:?''
????????}
????},
????methods:?{
????????//?初始化編輯器
????????initEditor()?{
????????????//?...?init?code
????????????//?編輯的文本的時候記錄光標(biāo)。
????????????editor.config.onchange?=?html?=>?{
????????????????//?生成@的標(biāo)簽的時候會觸發(fā)渲染、此時不要記錄光標(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()?返回一個 Selection 對象,表示用戶選擇的文本范圍或光標(biāo)的當(dāng)前位置。
????????????????const?selection?=?getSelection()
????????????????this.position?=?{
????????????????????range:?selection.getRangeAt(0),
????????????????????selection:?selection
????????????????}
????????????}?catch?(error)?{
????????????????console.log(error,?'光標(biāo)獲取失敗了~')
????????????}
????????}
????}
}
script>
英文code是 50, 判斷是否按住shift + @鍵 中文輸入法下標(biāo)點(diǎn)符號keyCode都是一樣的:229,推薦使用event.code或event.key作為@的判斷。
//?editor?keydown觸發(fā)事件
onKeyDownInput(e)?{
????//?@的鍵盤時間判斷
????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..
????}
}
生成@的用戶標(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時,富文本會把成功文本視為一個節(jié)點(diǎn)。
????
????//?需要在字符前插入一個空格否則、在換行與兩個@標(biāo)簽連續(xù)的時候?qū)е聼o法刪除標(biāo)簽
????let?spanNode?=?document.createElement('span');
????spanNode.innerHTML?=?' ';
????//創(chuàng)建一個新的空白的文檔片段,拆入對應(yīng)文本內(nèi)容
????let?frag?=?document.createDocumentFragment()
????frag.appendChild(spanNode);
????frag.appendChild(spanNodeFirst);
????//?如果是鍵盤觸發(fā)的默認(rèn)刪除面前的@,前文中我們沒有阻止@的生成所以要刪除@的再插入ps:如果你是數(shù)組遍歷的請傳入type 不然會一直刪除你前面的字符。
????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} `)
????}
},
getSelection()表示用戶選擇的文本范圍或光標(biāo)的當(dāng)前位置。Event.returnValue兼容IE取消默認(rèn)事件
五、Android、IOS、Web顯示多端一致
現(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
}
//?生成換行符號
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
??//?這個是彈窗列表
??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é)
作者:InfinityTomorrow 授權(quán)轉(zhuǎn)載 鏈接:https://juejin.cn/post/6982251438332182542
逆鋒起筆專注于程序員圈子,你不但可以學(xué)習(xí)到java、python等主流技術(shù)干貨,還可以第一時間獲悉最新技術(shù)動態(tài)、內(nèi)測資格、BAT大佬的經(jīng)驗(yàn)、精品視頻教程、副業(yè)賺錢經(jīng)驗(yàn),微信搜索readdot關(guān)注!
評論
圖片
表情
