1. 程式人生 > 其它 >alert獲取輸入框內容_實用開源:Web 聊天工具的富文字輸入框

alert獲取輸入框內容_實用開源:Web 聊天工具的富文字輸入框

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

倉庫地址:chat-input-box

預覽地址:codepen

首先來看看 demo 效果:

971904a8331e1383701796bb5952af71.gif

是不是覺得很神奇?接下來我會一步步講解這裡面的功能都是如何實現的。

輸入框富文字化

傳統的輸入框都是使用 來製作的,它的優勢是非常簡單,但最大的缺陷卻是無法展示圖片。為了能夠讓輸入框能夠展示圖片(富文字化),我們可以採用設定了 contenteditable="true" 屬性的

來實現這裡面的功能。

簡單建立一個 index.html 檔案,然後寫入如下內容:

class="editor" contenteditable="true">

src="https://static.easyicon.net/preview/121/1214124.gif" alt="">

開啟瀏覽器,就能看到一個預設已經帶了一張圖片的輸入框:

105d83de396e472e9f3e9a3b72ad9ade.png

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

接下來的任務,就是思考如何直接通過 control+v

把圖片貼上進去了。

處理貼上事件

任何通過“複製”或者 control+c 所複製的內容(包括螢幕截圖)都會儲存在剪貼簿,在貼上的時候可以在輸入框的 onpaste 事件裡面監聽到。

document.querySelector('.editor').addEventListener('paste', (e) => {

console.log(e.clipboardData.items)

})

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

fde918707440ced3ca439c8bf714e21c.png

細心的讀者會發現,如果直接在控制檯點開 DataTransferItemList

前的小箭頭,會發現物件的 length 屬性為0。說好的剪貼簿內容呢?其實這是 Chrome 除錯的一個小坑。在開發者工具裡面, console.log 出來的物件是一個引用,會隨著原始資料的改變而改變。由於剪貼簿的資料已經被“貼上”進輸入框了,所以展開小箭頭以後看到的 DataTransferItemList 就變成空的了。為此,我們可以改用 console.table 來展示實時的結果。

de459d14bd2d21972a59fea945acabcb.png

在明白了剪貼簿資料的存放位置以後,就可以編寫程式碼來處理它們了。由於我們的富文字輸入框比較簡單,所以只需要處理兩類資料即可,其一是普通的文字型別資料,包括 emoji 表情;其二則是圖片型別資料。

新建 paste.js 檔案:

const onPaste = (e) => {

// 如果剪貼簿沒有資料則直接返回

if (!(e.clipboardData && e.clipboardData.items)) {

return

}

// 用Promise封裝便於將來使用

return new Promise((resolve, reject) => {

// 複製的內容在剪貼簿裡位置不確定,所以通過遍歷來保證資料準確

for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {

const item = e.clipboardData.items[i]

// 文字格式內容處理

if (item.kind === 'string') {

item.getAsString((str) => {

resolve(str)

})

// 圖片格式內容處理

} else if (item.kind === 'file') {

const pasteFile = item.getAsFile()

// 處理pasteFile

// TODO(pasteFile)

} else {

reject(new Error('Not allow to paste this type!'))

}

}

})

}

export default onPaste

然後就可以在 onPaste 事件裡面直接使用了:

document.querySelector('.editor').addEventListener('paste', async (e) => {

const result = await onPaste(e)

console.log(result)

})

上面的程式碼支援文字格式,接下來就要對圖片格式進行處理了。玩過 type="file"> 的同學會知道,包括圖片在內的所有檔案格式內容都會儲存在 File 物件裡面,這在剪貼簿裡面也是一樣的。於是我們可以編寫一套通用的函式,專門來讀取 File 物件裡的圖片內容,並把它轉化成 base64 字串。

貼上圖片

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

新建一個 chooseImg.js 檔案:

/**

* 預覽函式

*

* @param {*} dataUrl base64字串

* @param {*} cb 回撥函式

*/

function toPreviewer (dataUrl, cb) {

cb && cb(dataUrl)

}

/**

* 圖片壓縮函式

*

* @param {*} img 圖片物件

* @param {*} fileType 圖片型別

* @param {*} maxWidth 圖片最大寬度

* @returns base64字串

*/

function compress (img, fileType, maxWidth) {

let canvas = document.createElement('canvas')

let ctx = canvas.getContext('2d')

const proportion = img.width / img.height

const width = maxWidth

const height = maxWidth / proportion

canvas.width = width

canvas.height = height

ctx.fillStyle = '#fff'

ctx.fillRect(0, 0, canvas.width, canvas.height)

ctx.drawImage(img, 0, 0, width, height)

const base64data = canvas.toDataURL(fileType, 0.75)

canvas = ctx = null

return base64data

}

/**

* 選擇圖片函式

*

* @param {*} e input.onchange事件物件

* @param {*} cb 回撥函式

* @param {number} [maxsize=200 * 1024] 圖片最大體積

*/

function chooseImg (e, cb, maxsize = 200 * 1024) {

const file = e.target.files[0]

if (!file || !/\/(?:jpeg|jpg|png)/i.test(file.type)) {

return

}

const reader = new FileReader()

reader.onload = function () {

const result = this.result

let img = new Image()

if (result.length <= maxsize) {

toPreviewer(result, cb)

return

}

img.onload = function () {

const compressedDataUrl = compress(img, file.type, maxsize / 1024)

toPreviewer(compressedDataUrl, cb)

img = null

}

img.src = result

}

reader.readAsDataURL(file)

}

export default chooseImg

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

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

const imgEvent = {

target: {

files: [pasteFile]

}

}

chooseImg(imgEvent, (url) => {

resolve(url)

})

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

輸入框中插入內容

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

對於插入內容,我們可以直接通過 document.execCommand 方法進行。關於這個方法詳細用法可以在MDN文件裡面找到,在這裡我們只需要使用 insertTextinsertImage 即可。

document.querySelector('.editor').addEventListener('paste', async (e) => {

const result = await onPaste(e)

const imgRegx = /^data:image\/png;base64,/

const command = imgRegx.test(result) ? 'insertImage': 'insertText'

document.execCommand(command, false, result)

})

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

當我們在程式碼中呼叫 window.getSelection() 後會獲得一個 Selection 物件。如果在頁面中選中一些文字,然後在控制檯執行 window.getSelection().toString(),就會看到輸出是你所選擇的那部分文字。

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

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

對於插入圖片來說,要先從 window.getSelection() 獲取 range ,然後往裡面插入圖片。

document.querySelector('.editor').addEventListener('paste', async (e) => {

// 讀取剪貼簿的內容

const result = await onPaste(e)

const imgRegx = /^data:image\/png;base64,/

// 如果是圖片格式(base64),則通過構造range的辦法把標籤插入正確的位置

// 如果是文字格式,則通過document.execCommand('insertText')方法把文字插入

if (imgRegx.test(result)) {

const sel = window.getSelection()

if (sel && sel.rangeCount === 1 && sel.isCollapsed) {

const range = sel.getRangeAt(0)

const img = new Image()

img.src = result

range.insertNode(img)

range.collapse(false)

sel.removeAllRanges()

sel.addRange(range)

}

} else {

document.execCommand('insertText', false, result)

}

})

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

插入 emoji

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

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

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

/**

* 獲取游標位置

* @param {DOMElement} element 輸入框的dom節點

* @return {Number} 游標位置

*/

export const getCursorPosition = (element) => {

let caretOffset = 0

const doc = element.ownerDocument || element.document

const win = doc.defaultView || doc.parentWindow

const sel = win.getSelection()

if (sel.rangeCount > 0) {

const range = win.getSelection().getRangeAt(0)

const preCaretRange = range.cloneRange()

preCaretRange.selectNodeContents(element)

preCaretRange.setEnd(range.endContainer, range.endOffset)

caretOffset = preCaretRange.toString().length

}

return caretOffset

}

/**

* 設定游標位置

* @param {DOMElement} element 輸入框的dom節點

* @param {Number} cursorPosition 游標位置的值

*/

export const setCursorPosition = (element, cursorPosition) => {

const range = document.createRange()

range.setStart(element.firstChild, cursorPosition)

range.setEnd(element.firstChild, cursorPosition)

const sel = window.getSelection()

sel.removeAllRanges()

sel.addRange(range)

}

有了這兩個方法以後,就可以放入 editor 節點裡面使用了。首先在節點的 keyupclick 事件裡記錄游標位置:

let cursorPosition = 0

const editor = document.querySelector('.editor')

editor.addEventListener('click', async (e) => {

cursorPosition = getCursorPosition(editor)

})

editor.addEventListener('keyup', async (e) => {

cursorPosition = getCursorPosition(editor)

})

記錄下游標位置後,便可通過呼叫 insertEmoji() 方法插入 emoji 字元了。

insertEmoji (emoji) {

const text = editor.innerHTML

// 插入 emoji

editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)

// 游標位置後挪一位,以保證在剛插入的 emoji 後面

setCursorPosition(editor, this.cursorPosition + 1)

// 更新本地儲存的游標位置變數(注意 emoji 佔兩個位元組大小,所以要加1)

cursorPosition = getCursorPosition(editor) + 1 // emoji 佔兩位

}

尾聲

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

9fd254cca9337b9483e44c1314246e8e.png

04a1f6b80f656477db731919c33c695f.png

5350dcb1d0dcba5c420524cb02a678c2.png

來都來了,點個在看再走吧~~~

2d86c5689b2496e7788a6660bf7ec710.gif