<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>

          在 Web 中實現(xiàn)表情符號的輸入

          共 11163字,需瀏覽 23分鐘

           ·

          2021-03-14 13:32

          作者:EdwardUp
          來源:SegmentFault 思否社區(qū)



          如果你準備在Web中開發(fā)一個可以聊天互動的應用,那么一個支持表情符號的輸入框很可能會是必備的內(nèi)容項。但具體到Web環(huán)境來說,我們知道,表單元素<input>和<textarea>只能輸入純文本,這樣的話,表情符號的支持具體要如何做呢?

          讓我們從熟悉的東西開始。



          來自微博和微信的兩種風格


          下圖是微信里的聊天:


          下圖是微博里的寫微博:


          綜合以上微信和微博的表情輸入設(shè)計,我們可以看出有兩種風格可以采用:

          • 一種是像微信這樣,只用純文本,通過類似[旺柴]這樣的符號標識來替代表情,最后輸出時再顯示成真正的表情。
          • 另一種是像微博這樣,所見即所得,輸入框本身就是表情和文字混合在一起。

          看起來似乎微博這種風格要復雜一點,我們就從微博的這種開始吧。



          表情圖和文字在一起的場景


          對HTML來說,文字和圖片放在一起是非?;A(chǔ)的能力。但是,我們還要求它可以作為輸入框來使用,這就需要用到HTML屬性contenteditable。它可能不太常用,但其實是一個支持范圍很廣,歷史悠久的HTML屬性。

          使用contenteditable,就可以得到這個簡單卻滿足要求的輸入框元素:

          <div contenteditable="true"></div>

          表情的顯示

          現(xiàn)在的輸入框已經(jīng)是一個<div>,所以,你可以用任意的HTML標簽來顯示表情,而其中最為常用的就是圖片<img>。以前面的微博內(nèi)容為例,它和輸入框一起,應該構(gòu)成像下面這樣的HTML代碼:

          <div contenteditable="true">
              一條帶表情<img src="/path/to/emoji/3.gif"><img src="/path/to/emoji/3.gif">的微博<img src="/path/to/emoji/9.gif">
          </div>

          可以看出,插入表情實際就是插入一段HTML代碼。HTML代碼和剩余的純文本一起,共同構(gòu)成帶表情符號的輸入內(nèi)容。

          表情輸入功能的要點


          接下來,我們參照以下示例界面,來完成微博風格的輸入框。


          這個界面中間的橫線,就是contenteditable的<div>輸入框元素。結(jié)合這個界面,我們可以分析出接下來的兩個實現(xiàn)要點:

          • 點擊下方的表情,就將該表情對應的HTML代碼插入到輸入框<div>。
          • 表情HTML代碼插入的位置要符合輸入框<div>的當前光標位置。

          顯然,只是第一個要點的話是很容易的,關(guān)鍵是第二個。

          這第二個要點,就需要了解Selection和Range的概念了。



          Selection 和 Range


          在網(wǎng)頁中,你一定很熟悉下圖展示的兩種狀態(tài):


          • 一種是有一個不斷閃爍的光標,表示著當前正在輸入或準備輸入的位置。它一般只出現(xiàn)在網(wǎng)頁的可以輸入的元素內(nèi),比如文本輸入框。
          • 另一種是一部分內(nèi)容呈現(xiàn)藍底白字(這個顏色可以修改,但默認是這個顏色)的狀態(tài),表示當前被選中。它可以出現(xiàn)在任意的網(wǎng)頁元素內(nèi),我們也常用來部分復制網(wǎng)頁內(nèi)容。

          以上兩種狀態(tài)雖然表現(xiàn)形式不同,但它們在Web領(lǐng)域都叫做Selection。Selection描述的正是網(wǎng)頁中的“當前選擇”。閃爍的光標也算作一種特殊的選擇,稱為已折疊(Collapsed)的選擇。

          Selection在JavaScript中對應的是Selection對象,它可以通過window.getSelection()或document.getSelection()獲取到。

          任何時候,當網(wǎng)頁中“當前選擇”發(fā)生改變時,都會觸發(fā)document.onselectionchange事件。這個事件處理函數(shù)僅存在于document。

          Range的設(shè)計意義


          Selection已經(jīng)表示了“當前選擇”,那Range是做什么的呢?簡單來說,Range是Selection的“預備軍”,它和Selection類似,都可以具體描述網(wǎng)頁中的“選擇”狀態(tài),只是Selection是可見的,Range是不可見的。

          通過Selection的和Range有關(guān)的方法,可以把Range應用到Selection,這時候就可以看到Range的選擇效果了。這就好像Selection代表了舞臺,Range則是一個又一個幕后的演員,它們可以輪換上場和退場。

          除Firefox外,其他瀏覽器的Selection都只支持單個Range,因此,我們一般在同一時間只能應用一個Range到Selection。

          一個Range由兩個邊界點組成,分別是起始邊界點和結(jié)尾邊界點。這兩個邊界點在一起,就可以描述任意的“選擇”狀態(tài)。當兩個邊界點完全相同時,這個“選擇”狀態(tài)就稱為折疊的(Collapsed),也就是閃爍光標的狀態(tài)。

          Selection的和Range有關(guān)的方法很重要,具體如下:

          • getRangeAt(i) - 按索引獲取Selection的當前Range。除Firefox外,其他瀏覽器只固定使用索引0。
          • addRange(range) - 將range應用到Selection。除Firefox外,如果Selection當前已經(jīng)有其他Range,將忽略此方法調(diào)用。
          • removeRange(range) - 從Selection中取消應用range。
          • removeAllRanges() - 取消應用所有Range。
          • empty() - 等同于removeAllRanges()。

          關(guān)于Selection和Range的更詳細的介紹和說明,推薦閱讀這篇Selection And Range。



          符合光標位置的表情插入


          了解了Selection和Range的基礎(chǔ)知識后,我們繼續(xù)來完成微博風格的表情輸入。前面說過,要點是表情HTML代碼的插入位置要符合輸入框的光標位置,所以我們首先要做的就是記錄這個光標位置。

          先標記輸入框為inputBox(本示例使用Vue):

          <div 
              ref="inputBox" 
              class="input-box" 
              contenteditable="true"></div>

          然后使用前面提到的document.onselectionchange監(jiān)聽選擇變化事件:

          document.onselectionchange = () => {
              let selection = document.getSelection();

              if (selection.rangeCount > 0) {
                  const range = selection.getRangeAt(0);

                  if (vmEmoji.$refs.inputBox.contains(range.commonAncestorContainer)) {
                      rangeOfInputBox = range;
                  }
              }
          };

          這段代碼的作用是,在“當前選擇”發(fā)生變化(鼠標點擊或觸摸動作等)后,如果變化后的Selection位于輸入框inputBox內(nèi)部,就用變量rangeOfInputBox保存它。這里也可以看到,Selection是用Range來保存的。

          selection.rangeCount是Selection的屬性,它表示Selection正在應用的Range數(shù)目。當它大于0時,說明當前是“有選擇”的狀態(tài)。

          range.commonAncestorContainer是Range的屬性,它表示Range的兩個邊界點的距離最近的共同父元素。這里用于判斷Range發(fā)生在inputBox內(nèi)。

          最后,當點擊表情時,執(zhí)行插入表情的方法insertEmoji:

          insertEmoji (name) {
              let emojiEl = document.createElement("img");
              emojiEl.src = `${this.emoji.path}${name}${this.emoji.suffix}`;

              if (!rangeOfInputBox) {
                  rangeOfInputBox = new Range();
                  rangeOfInputBox.selectNodeContents(this.$refs.inputBox);
              }

              if (rangeOfInputBox.collapsed) {
                  rangeOfInputBox.insertNode(emojiEl);
              } else {
                  rangeOfInputBox.deleteContents();
                  rangeOfInputBox.insertNode(emojiEl);
              }
              rangeOfInputBox.collapse(false);
          }

          這段代碼中,參數(shù)name代表了不同表情,從而生成不同表情對應的不同HTML元素(都是<img>)。

          如果rangeOfInputBox不存在,說明還沒有過任何發(fā)生在輸入框內(nèi)的選擇事件,此時就指定一個默認的Range。selectNodeContents(node)是Range的方法,將一個Range設(shè)定為選中整個node元素內(nèi)容。

          insertNode(node)是Range的方法,可以將node元素插入到Range的起始邊界點。它是本示例的關(guān)鍵方法,用于完成表情HTML元素插入。這里需要對Range的狀態(tài)做判斷,如果Range是折疊的(閃爍光標),直接插入表情元素,如果Range不是折疊的(選中了一部分輸入框內(nèi)容),就先刪除選中的內(nèi)容,再插入表情元素(相當于替換內(nèi)容的效果)。deleteContent()也是Range的方法,可以將Range包含的內(nèi)容從網(wǎng)頁文檔中刪除。

          結(jié)尾調(diào)用的collapse(toStart)仍然是Range的方法,它可以將Range的兩個邊界點變成相同的,也就是折疊的狀態(tài)。如果參數(shù)toStart為true則取起始邊界點的位置,如果為false則是取結(jié)尾邊界點。這里取的是結(jié)尾邊界點,這樣就好像是在插入一個表情后,自動將光標移動到剛插入的表情元素后方,從而支持表情的連續(xù)輸入。

          到此,微博風格的表情輸入就已經(jīng)實現(xiàn)了:


          把輸入框內(nèi)的內(nèi)容作為HTML代碼(富文本),就可以提交給后臺,或者像圖里這樣簡單展示在上方的聊天窗口內(nèi)。

          完善點擊表情時的光標置位


          這種文字和表情圖混合在一起的風格還存在一個待完善的地方:如果點擊文字,光標會正確定位到選中的文字前方,而點擊表情圖,就沒有任何動作。這個光標置位的功能我們可以手動補全。

          為輸入框增加click事件處理:

          <div 
              ref="inputBox" 
              @click="handleBoxClick"
              class="input-box" 
              contenteditable="true"></div>

          對應的handleBoxClick()事件處理方法如下:

          handleBoxClick (event) {
              let target = event.target;
              this.setCaretForEmoji(target);
          },
          setCaretForEmoji (target) {
              if (target.tagName.toLowerCase() === "img") {
                  let range = new Range();
                  range.setStartBefore(target);
                  range.collapse(true);
                  document.getSelection().removeAllRanges();
                  document.getSelection().addRange(range);
              }
          },

          setStartBefore(node)是Range的方法,可以設(shè)定邊界起始點的位置到一個元素之前。這段代碼整體來說就是,如果當前click的是<img>元素,就創(chuàng)建一個Range,設(shè)定它為折疊狀態(tài),位置在剛才點擊的表情圖之前,然后應用這個Range到Selection,變成真實可見的選擇效果。



          用純文本符號來替代表情的場景


          現(xiàn)在,我們重新開始,來實現(xiàn)微信風格的表情輸入。

          前面說過,微信是使用類似[旺柴]這樣的符號標識來替代表情的風格。這種風格全部使用純文本,因此,輸入框會很容易實現(xiàn),可以直接使用表單元素的文本輸入框:

          <input
              ref="formInput"
              @keydown="handleFormInputKeydown" 
              class="form-input"
              type="text">

          這里預留的handleFormInputKeydown()輸入事件處理方法,將在后文中使用。

          和微博風格類似,接下來也是可以分成兩個實現(xiàn)要點:

          • 點擊下方的表情,就將該表情對應的純文本符號插入到輸入框<input>。
          • 純文本符號的插入位置要符合輸入框<input>的當前光標位置。

          雖然同樣是結(jié)合Selection和Range的概念,按光標位置來插入純文本符號,但<input>會更加簡單。

          按光標位置來插入純文本


          表單元素<input>自身有以下3個屬性是關(guān)于“選擇”的:

          • input.selectionStart - 選擇的起始位置。它的值是一個索引數(shù)字,比如6。
          • input.selectionEnd - 選擇的結(jié)尾位置。值的格式同上。
          • input.selectionDirection - 選擇的方向??蛇x值"forward","backward"和"none"。一般對應的情況是指鼠標拖拽選擇時是從前向后,還是從后向前,又或者是雙擊選中。

          通過這些屬性,就可以實現(xiàn)對“選擇”狀態(tài)的讀取和寫入,而無需使用Selection和Range。

          現(xiàn)在,點擊表情時,執(zhí)行插入表情的方法insertEmojiText:

          insertEmojiText (name) {
              let input = this.$refs.formInput;
              let emojiText = `[${name}]`;
              input.focus();
              input.setRangeText(emojiText, input.selectionStart, input.selectionEnd, "end");
              input.blur();
          }

          可以看到純文本的表情插入非常簡單。這里也是用[name]的符號來表示表情。

          input.setRangeText(replacement, [start], [end], [selectionMode])是input的方法,可以將索引位置從start到end的文本,替換成replacement的文本。而如果start等于end,就相當于閃爍光標的狀態(tài),沒有文本會被替換,變成了插入文本的效果。末尾參數(shù)selectionMode決定了在文本替換(或插入)操作完畢后,input如何更新選擇狀態(tài)。這里取"end"表示將選擇狀態(tài)設(shè)定為“閃爍光標,位置在新插入文本的后方”,從而支持表情連續(xù)輸入。

          使用input.setRangeText(),無論當前狀態(tài)是閃爍光標,還是已經(jīng)選擇了一些文本,都會以符合我們輸入習慣的方式插入表情文本。

          關(guān)于input.setRangeText()的更詳細的說明,同樣推薦閱讀這篇Selection And Range。

          這段代碼中的input.focus()和input.blur(),是因為僅在<input>元素被focus的情況下進行文本編輯操作,才能確保input.selectionStart和input.selectionEnd兩個值正確更新。同時,這里又并不希望<input>元素被真地focus,所以又用了input.blur()來取消。

          到這里,微信風格的表情輸入就基本可用了。但是,這種純文本符號的風格也有一個應完善的地方:用退格鍵(Backspace)來刪除文本時,代表一個表情的純文本符號應該以作為一個整體被刪除。比如[旺柴]這樣的表情符號,在光標位于]的后方時,一個退格鍵就應該刪除這一整段文本。這也是微信里存在的功能。

          退格鍵支持 - 以表情符號為整體刪除文本


          前文示例中為<input>元素預留的handleFormInputKeydown()方法,就是用于實現(xiàn)這一功能:

          handleFormInputKeydown (event) {
              let input = this.$refs.formInput;
              let chatString = input.value;

              // "Backspace" and selection type "Caret"
              if (event.keyCode === 8 && input.selectionStart === input.selectionEnd) {
                  let indexEnd = input.selectionStart - 1;
                  let charToDelete = chatString.charAt(indexEnd);

                  // delete the whole [***]
                  if (charToDelete === "]") {
                      event.preventDefault();
                      let indexStart = chatString.lastIndexOf("[", indexEnd);
                      input.setRangeText("", indexStart, indexEnd + 1, "end");
                  }
              }
          }

          這段代碼是判斷當選擇狀態(tài)為閃爍光標,且剛好位于字符]后按下了退格鍵的時候,就找出整個[name]表情文本,使用input.setRangeText()實現(xiàn)整段刪除。
          到此,微信風格的表情輸入也就完成了:


          在提交給后臺或者圖中這樣展示在上方聊天窗口內(nèi)的時候,取輸入框內(nèi)的純文本,然后將所有[name]格式的文本符號,替換成對應表情的HTML(比如[1]變成<img src="/path/to/emoji/1.gif">)即可。



          完整代碼示例


          兩種風格的完整代碼示例:

          • 微博風格(表情圖和文字一起)
          • https://codesandbox.io/s/emoji-input-contenteditable-75qe8
          • 微信風格(表情用純文本符號替代)
          • https://codesandbox.io/s/emoji-input-text-yqfe3




          補充


          光標顏色


          Selection在可輸入元素內(nèi)的折疊狀態(tài),也就是閃爍光標,它的顏色也是可以修改的,比如:

          input {
              caret-color: red;
          }

          會將閃爍光標修改為紅色。更詳細的說明請查看MDN上的caret-color。

          輸入法里的表情字符



          在手機上,你可能注意到像搜狗這樣的輸入法也給你提供了一套表情(上圖中的Emoji),它們在微信中也可以使用,而且可以直接顯示在微信的輸入框內(nèi)。這種不依賴其他東西就可以使用的表情,本質(zhì)上是Unicode字符,你可以到Unicode Character Table上查找更多的表情字符。

          Unicode字符表情最終呈現(xiàn)的樣子取決于它所處的環(huán)境。比如不同手機,不同操作系統(tǒng),都可能有不同的外觀。

          定義虛擬鍵盤的動作鍵



          手機上的輸入法鍵盤,右下角的動作鍵可以通過HTML屬性enterkeyhint設(shè)置為不同的類型:

          <div
              ref="inputBox" 
              enterkeyhint="send"
              contenteditable="true"></div>

          這里值send對應的就是前面圖中的“發(fā)送”。其他可用的值可以參考MDN上的enterkeyhint。

          如果想要像微信那樣,點擊虛擬鍵盤右下角的“發(fā)送”就可以發(fā)送消息(而不是點擊網(wǎng)頁上的按鈕),監(jiān)聽輸入元素的鍵盤事件,并確認按鍵為enter鍵即可。



          結(jié)語


          “可以輸入表情”對于聊天交流而言可以說是非常棒的一項增強。不管具體用哪一種風格實現(xiàn),最終都是讓大家可以表達出更多。

          希望本文的表情功能開發(fā)指南可以幫到你。



          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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 V在线视频 | 99精品视频免费观看, | 日日干日 | 手机版AV |