<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【實(shí)戰(zhàn)】我是如何在輸入框?qū)崿F(xiàn)@ At功能的

          共 10233字,需瀏覽 21分鐘

           ·

          2021-12-26 08:30


          作者: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)前需求的拆解

          1. 按住shift + @ 的時(shí)候,彈出通知列表
          2. 選擇時(shí) @的用戶標(biāo)簽插入當(dāng)前的光標(biāo)位置中
          3. 生成@的用戶標(biāo)簽的規(guī)則是:高亮、攜帶用戶ID、一鍵刪除信息、不可以編輯。
          4. 文本框要隨內(nèi)容自適應(yīng)高度
          5. AndroidIOS、Web顯示多端一致。
          6. 具有擴(kuò)張性,未來評論可能插入圖片文件等....

          市面流行方案對比

          ps: 方案有很多種方式,適合自己、適合團(tuán)隊(duì)的才是最佳實(shí)踐。沒有完美的方案(ps:只有不聽話的產(chǎn)品經(jīng)理??????) 的產(chǎn)品經(jīng)理??????)

          • textareainput(例:新浪微博)

            • 流程大概都是(監(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>

          拓展知識:

          1. 為什么在上文中使用 ”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è)問題?。?!)。
          1. 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-postextarea、contentedtiableiframe 正文中獲取插入符號/光標(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)贊、在看” 支持一波??

          瀏覽 69
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美视频一区二区三区四区五区 | 人人爱人人摸人人操 | 欧美精品一区二区婷婷 | 毛片一A免费网站 | 国产乱子伦视频国产印度 |