成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Web聊天工具的富文本輸入框

開發 前端
最近折騰 Websocket,打算開發一個聊天室應用練練手。在應用開發的過程中發現可以插入 emoji ,粘貼圖片的富文本輸入框其實蘊含著許多有趣的知識,于是便打算記錄下來和大家分享。

最近折騰 Websocket,打算開發一個聊天室應用練練手。在應用開發的過程中發現可以插入 emoji ,粘貼圖片的富文本輸入框其實蘊含著許多有趣的知識,于是便打算記錄下來和大家分享。

輸入框富文本化

傳統的輸入框都是使用 <textarea> 來制作的,它的優勢是非常簡單,但***的缺陷卻是無法展示圖片。為了能夠讓輸入框能夠展示圖片(富文本化),我們可以采用設置了 contenteditable="true" 屬性的 <div> 來實現這里面的功能。

簡單創建一個 index.html 文件,然后寫入如下內容: 

  1. <div class="editor" contenteditable="true">  
  2.   <img src="https://static.easyicon.net/preview/121/1214124.gif" alt="">  
  3. </div> 

打開瀏覽器,就能看到一個默認已經帶了一張圖片的輸入框:

光標可以在圖片前后移動,同時也可以輸入內容,甚至通過退格鍵刪除這張圖片——換句話說,圖片也是可編輯內容的一部分,也意味著輸入框的富文本化已經體現出來了。

接下來的任務,就是思考如何直接通過 control + v 把圖片粘貼進去了。

處理粘貼事件

任何通過“復制”或者 control + c 所復制的內容(包括屏幕截圖)都會儲存在剪貼板,在粘貼的時候可以在輸入框的 onpaste 事件里面監聽到。 

  1. <div class="editor" contenteditable="true">  
  2.   <img src="https://static.easyicon.net/preview/121/1214124.gif" alt="">  
  3. </div> 

而剪貼板的的內容則存放在 DataTransferItemList 對象中,可以通過 e.clipboardData.items 訪問到:

細心的讀者會發現,如果直接在控制臺點開 DataTransferItemList 前的小箭頭,會發現對象的 length 屬性為0。說好的剪貼板內容呢?其實這是 Chrome 調試的一個小坑。在開發者工具里面,console.log 出來的對象是一個引用,會隨著原始數據的改變而改變。由于剪貼板的數據已經被“粘貼”進輸入框了,所以展開小箭頭以后看到的 DataTransferItemList 就變成空的了。為此,我們可以改用 console.table 來展示實時的結果。

在明白了剪貼板數據的存放位置以后,就可以編寫代碼來處理它們了。由于我們的富文本輸入框比較簡單,所以只需要處理兩類數據即可,其一是普通的文本類型數據,包括 emoji 表情;其二則是圖片類型數據。

新建 paste.js 文件: 

  1. const onPaste = (e) => {  
  2.   // 如果剪貼板沒有數據則直接返回  
  3.   if (!(e.clipboardData && e.clipboardData.items)) {  
  4.     return  
  5.   }  
  6.   // 用Promise封裝便于將來使用  
  7.   return new Promise((resolve, reject) => {  
  8.     // 復制的內容在剪貼板里位置不確定,所以通過遍歷來保證數據準確  
  9.     for (let i = 0len = e.clipboardData.items.length; i < len; i++) {  
  10.       const item = e.clipboardData.items[i]  
  11.       // 文本格式內容處理  
  12.       if (item.kind === 'string') {  
  13.         item.getAsString((str) => {  
  14.           resolve(str)  
  15.         })  
  16.       // 圖片格式內容處理  
  17.       } else if (item.kind === 'file') {  
  18.         const pasteFile = item.getAsFile()  
  19.         // 處理pasteFile  
  20.         // TODO(pasteFile)  
  21.       } else {  
  22.         reject(new Error('Not allow to paste this type!'))  
  23.       }  
  24.     }  
  25.   })  
  26.  
  27. export default onPaste 

然后就可以在 onPaste 事件里面直接使用了: 

  1. document.querySelector('.editor').addEventListener('paste', async (e) => {  
  2.     const result = await onPaste(e)  
  3.     console.log(result)  
  4. }) 

上面的代碼支持文本格式,接下來就要對圖片格式進行處理了。玩過 <input type="file"> 的同學會知道,包括圖片在內的所有文件格式內容都會儲存在 File 對象里面,這在剪貼板里面也是一樣的。于是我們可以編寫一套通用的函數,專門來讀取 File 對象里的圖片內容,并把它轉化成 base64 字符串。

粘貼圖片

為了更好地在輸入框里展示圖片,必須限制圖片的大小,所以這個圖片處理函數不僅能夠讀取 File 對象里面的圖片,還能夠對其進行壓縮。

新建一個 chooseImg.js 文件: 

  1. /**  
  2.  * 預覽函數  
  3.  *  
  4.  * @param {*} dataUrl base64字符串  
  5.  * @param {*} cb 回調函數  
  6.  */  
  7. function toPreviewer (dataUrl, cb) {  
  8.   cb && cb(dataUrl)  
  9.  
  10. /**  
  11.  * 圖片壓縮函數  
  12.  *  
  13.  * @param {*} img 圖片對象  
  14.  * @param {*} fileType  圖片類型  
  15.  * @param {*} maxWidth 圖片***寬度  
  16.  * @returns base64字符串  
  17.  */  
  18. function compress (img, fileType, maxWidth) {  
  19.   let canvas = document.createElement('canvas')  
  20.   let ctx = canvas.getContext('2d')  
  21.   const proportion = img.width / img.height  
  22.   const width = maxWidth  
  23.   const height = maxWidth / proportion  
  24.   canvas.width = width  
  25.   canvas.height = height  
  26.   ctx.fillStyle = '#fff'  
  27.   ctx.fillRect(0, 0, canvas.width, canvas.height)  
  28.   ctx.drawImage(img, 0, 0, width, height)  
  29.   const base64data = canvas.toDataURL(fileType, 0.75)  
  30.   canvas = ctx = null  
  31.   return base64data  
  32.  
  33. /**  
  34.  * 選擇圖片函數  
  35.  *  
  36.  * @param {*} e input.onchange事件對象  
  37.  * @param {*} cb 回調函數  
  38.  * @param {number} [maxsize=200 * 1024] 圖片***體積  
  39.  */  
  40. function chooseImg (e, cb, maxsize = 200 * 1024) {  
  41.   const file = e.target.files[0]  
  42.   if (!file || !/\/(?:jpeg|jpg|png)/i.test(file.type)) {  
  43.     return  
  44.   }  
  45.   const reader = new FileReader()  
  46.   reader.onload = function () {  
  47.     const result = this.result  
  48.     let img = new Image()  
  49.     if (result.length <= maxsize) {  
  50.       toPreviewer(result, cb)  
  51.       return  
  52.     }  
  53.     img.onload = function () {  
  54.       const compresscompressedDataUrl = compress(img, file.type, maxsize / 1024)  
  55.       toPreviewer(compressedDataUrl, cb)  
  56.       img = null  
  57.     }   
  58.     img.src = result  
  59.   }  
  60.   reader.readAsDataURL(file)  
  61.  
  62. export default chooseImg 

關于使用 canvas 壓縮圖片和使用 FileReader 讀取文件的內容在這里就不贅述了,感興趣的讀者可以自行查閱。

回到上一步的 paste.js 函數,把其中的 TODO() 改寫成 chooseImg() 即可: 

  1. const imgEvent = {  
  2.   target: {  
  3.     files: [pasteFile]  
  4.   }  
  5.  
  6. chooseImg(imgEvent, (url) => {  
  7.   resolve(url)  
  8. }) 

回到瀏覽器,如果我們復制一張圖片并在輸入框中執行粘貼的動作,將可以在控制臺看到打印出了以 data:image/png;base64 開頭的圖片地址。

輸入框中插入內容

經過前面兩個步驟,我們后已經可以讀取剪貼板中的文本內容和圖片內容了,接下來就是把它們正確的插入輸入框的光標位置當中。

對于插入內容,我們可以直接通過 document.execCommand 方法進行。關于這個方法詳細用法可以在MDN文檔里面找到,在這里我們只需要使用 insertText 和 insertImage 即可。 

  1. document.querySelector('.editor').addEventListener('paste', async (e) => {  
  2.     const result = await onPaste(e)  
  3.     const imgRegx = /^data:image\/png;base64,/  
  4.     const command = imgRegx.test(result) ? 'insertImage': 'insertText'   
  5.     document.execCommand(command, false, result)  
  6. }) 

但是在某些版本的 Chrome 瀏覽器下,insertImage 方法可能會失效,這時候便可以采用另外一種方法,利用 Selection 來實現。而之后選擇并插入 emoji 的操作也會用到它,因此不妨先來了解一下。

當我們在代碼中調用 window.getSelection() 后會獲得一個 Selection 對象。如果在頁面中選中一些文字,然后在控制臺執行 window.getSelection().toString(),就會看到輸出是你所選擇的那部分文字。

與這部分區域文字相對應的,是一個 range 對象,使用 window.getSelection().getRangeAt(0) 即可以訪問它。range 不僅包含了選中區域文字的內容,還包括了區域的起點位置 startOffset 和終點位置 endOffset。

我們也可以通過 document.createRange() 的辦法手動創建一個 range,往它里面寫入內容并展示在輸入框中。

對于插入圖片來說,要先從 window.getSelection() 獲取 range ,然后往里面插入圖片。 

  1. document.querySelector('.editor').addEventListener('paste', async (e) => {  
  2.   // 讀取剪貼板的內容  
  3.   const result = await onPaste(e)  
  4.   const imgRegx = /^data:image\/png;base64,/  
  5.   // 如果是圖片格式(base64),則通過構造range的辦法把<img>標簽插入正確的位置  
  6.   // 如果是文本格式,則通過document.execCommand('insertText')方法把文本插入  
  7.   if (imgRegx.test(result)) {  
  8.     const sel = window.getSelection()  
  9.     if (sel && sel.rangeCount === 1 && sel.isCollapsed) {  
  10.       const range = sel.getRangeAt(0)  
  11.       const img = new Image()  
  12.       img.src = result  
  13.       range.insertNode(img)  
  14.       range.collapse(false)  
  15.       sel.removeAllRanges()  
  16.       sel.addRange(range)  
  17.     }  
  18.   } else {  
  19.     document.execCommand('insertText', false, result)  
  20.   }  
  21. }) 

這種辦法也能很好地完成粘貼圖片的功能,并且通用性會更好。接下來我們還會利用 Selection,來完成 emoji 的插入。

插入 emoji

不管是粘貼文本也好,還是圖片也好,我們的輸入框始終是處于聚焦(focus)狀態。而當我們從表情面板里選擇 emoji 表情的時候,輸入框會先失焦(blur),然后再重新聚焦。由于 document.execCommand 方法必須在輸入框聚焦狀態下才能觸發,所以對于處理 emoji 插入來說就無法使用了。

上一小節講過,Selection 可以讓我們拿到聚焦狀態下所選文本的起點位置 startOffset 和終點位置 endOffset,如果沒有選擇文本而僅僅處于聚焦狀態,那么這兩個位置的值相等(相當于選擇文本為空),也就是光標的位置。只要我們能夠在失焦前記錄下這個位置,那么就能夠通過 range 把 emoji 插入正確的地方了。

首先編寫兩個工具方法。新建一個 cursorPosition.js 文件: 

  1. /**  
  2.  * 獲取光標位置  
  3.  * @param {DOMElement} element 輸入框的dom節點  
  4.  * @return {Number} 光標位置  
  5.  */  
  6. export const getCursorPosition = (element) => {  
  7.   let caretOffset = 0  
  8.   const doc = element.ownerDocument || element.document  
  9.   const win = doc.defaultView || doc.parentWindow  
  10.   const sel = win.getSelection()  
  11.   if (sel.rangeCount > 0) {  
  12.     const range = win.getSelection().getRangeAt(0)  
  13.     const preCaretRange = range.cloneRange()  
  14.     preCaretRange.selectNodeContents(element)  
  15.     preCaretRange.setEnd(range.endContainer, range.endOffset)  
  16.     caretOffset = preCaretRange.toString().length  
  17.   }  
  18.   return caretOffset  
  19.  
  20. /**  
  21.  * 設置光標位置  
  22.  * @param {DOMElement} element 輸入框的dom節點  
  23.  * @param {Number} cursorPosition 光標位置的值  
  24.  */  
  25. export const setCursorPosition = (element, cursorPosition) => {  
  26.   const range = document.createRange()  
  27.   range.setStart(element.firstChild, cursorPosition)  
  28.   range.setEnd(element.firstChild, cursorPosition)  
  29.   const sel = window.getSelection()  
  30.   sel.removeAllRanges()  
  31.   sel.addRange(range)  

有了這兩個方法以后,就可以放入 editor 節點里面使用了。首先在節點的 keyup 和 click 事件里記錄光標位置: 

  1. let cursorPosition = 0  
  2. const editor = document.querySelector('.editor')  
  3. editor.addEventListener('click', async (e) => {  
  4.   cursorPosition = getCursorPosition(editor)  
  5. })  
  6. editor.addEventListener('keyup', async (e) => {  
  7.   cursorPosition = getCursorPosition(editor)  
  8. }) 

記錄下光標位置后,便可通過調用 insertEmoji() 方法插入 emoji 字符了。 

  1. insertEmoji (emoji) {  
  2.   const text = editor.innerHTML  
  3.   // 插入 emoji  
  4.   editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)  
  5.   // 光標位置后挪一位,以保證在剛插入的 emoji 后面  
  6.   setCursorPosition(editor, this.cursorPosition + 1)  
  7.   // 更新本地保存的光標位置變量(注意 emoji 占兩個字節大小,所以要加1)  
  8.   cursorPosition = getCursorPosition(editor) + 1 //  emoji 占兩位  

尾聲

文章涉及的代碼已經上傳到倉庫,為了簡便起見采用 VueJS 處理了一下,不影響閱讀。***想說的是,這個 Demo 僅僅完成了輸入框最基礎的部分,關于復制粘貼還有很多細節要處理(比如把別處的行內樣式也復制了進來等等),在這里就不一一展開了,感興趣的讀者可以自行研究,更歡迎和我留言交流~ 

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2010-07-08 14:35:32

UDP協議

2010-07-13 08:19:10

Linux聊天工具

2011-11-30 10:48:21

2022-02-12 12:18:59

Delta Chat聊天應用開源

2011-06-27 10:58:31

Qt 局域網 聊天

2011-12-21 17:39:03

imo即時通訊

2017-05-10 11:10:15

LinuxUbuntuDiscord

2014-09-01 10:33:34

2015-04-27 14:29:53

C#UDP實現P2P語音聊天工具

2012-02-20 09:57:12

2011-12-15 10:30:51

即時通訊imo

2022-02-12 10:39:59

FBI網絡犯罪加密

2021-09-27 14:44:48

鴻蒙HarmonyOS應用

2016-04-29 17:41:53

北信源/企業IM

2020-09-24 14:06:19

Vue

2011-03-30 20:44:46

上網行為管理管理策略網康科技

2010-10-26 14:41:18

2009-04-17 09:30:33

Firefox插件瀏覽器

2020-12-23 11:45:27

鴻蒙HarmonyOSTextField組件

2023-02-15 14:07:03

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 91精品国产综合久久小仙女图片 | 成人av高清在线观看 | 欧美一区二区三区高清视频 | 精品在线一区 | 北条麻妃国产九九九精品小说 | 色综合天天天天做夜夜夜夜做 | 精精国产xxxx视频在线播放7 | 亚洲精品九九 | 观看av| 亚洲国产情侣自拍 | 视频二区在线观看 | 福利片在线观看 | 久久99视频精品 | 亚洲精品电影网在线观看 | 亚洲最大的成人网 | 亚洲第一成年免费网站 | 欧美日韩1区2区 | 日韩中文字幕久久 | 亚洲毛片网站 | 亚洲黄色av| 在线观看特色大片免费网站 | 人人草天天草 | 男人的天堂在线视频 | 欧美 日韩 在线播放 | 中文字幕日韩一区二区 | www天天操 | 国产在线一区二区三区 | 亚洲精品在线观看视频 | 国产亚韩 | 福利网址 | 国产精品一区久久久 | 欧美精品在线免费 | 龙珠z在线观看 | 欧美高清一区 | 日本a∨精品中文字幕在线 亚洲91视频 | 亚洲在线一区 | 精品久久久精品 | 亚洲精品国产成人 | 亚洲女人天堂成人av在线 | 欧美日韩一区二区在线观看 | 懂色av色香蕉一区二区蜜桃 |