摘要:由于我們的富文本輸入框比較簡單,所以只需要處理兩類數據即可,其一是普通的文本類型數據,包括表情其二則是圖片類型數據。
最近折騰 Websocket,打算開發(fā)一個聊天室應用練練手。在應用開發(fā)的過程中發(fā)現可以插入 emoji ,粘貼圖片的富文本輸入框其實蘊含著許多有趣的知識,于是便打算記錄下來和大家分享。
倉庫地址:chat-input-box
預覽地址:https://codepen.io/jrainlau/p...
首先來看看 demo 效果:
是不是覺得很神奇?接下來我會一步步講解這里面的功能都是如何實現的。
輸入框富文本化傳統(tǒng)的輸入框都是使用 來制作的,它的優(yōu)勢是非常簡單,但最大的缺陷卻是無法展示圖片。為了能夠讓輸入框能夠展示圖片(富文本化),我們可以采用設置了 contenteditable="true" 屬性的 簡單創(chuàng)建一個 index.html 文件,然后寫入如下內容: 打開瀏覽器,就能看到一個默認已經帶了一張圖片的輸入框: 光標可以在圖片前后移動,同時也可以輸入內容,甚至通過退格鍵刪除這張圖片——換句話說,圖片也是可編輯內容的一部分,也意味著輸入框的富文本化已經體現出來了。 接下來的任務,就是思考如何直接通過 control + v 把圖片粘貼進去了。 任何通過“復制”或者 control + c 所復制的內容(包括屏幕截圖)都會儲存在剪貼板,在粘貼的時候可以在輸入框的 onpaste 事件里面監(jiān)聽到。 而剪貼板的的內容則存放在 DataTransferItemList 對象中,可以通過 e.clipboardData.items 訪問到: 細心的讀者會發(fā)現,如果直接在控制臺點開 DataTransferItemList 前的小箭頭,會發(fā)現對象的 length 屬性為0。說好的剪貼板內容呢?其實這是 Chrome 調試的一個小坑。在開發(fā)者工具里面,console.log 出來的對象是一個引用,會隨著原始數據的改變而改變。由于剪貼板的數據已經被“粘貼”進輸入框了,所以展開小箭頭以后看到的 DataTransferItemList 就變成空的了。為此,我們可以改用 console.table 來展示實時的結果。 在明白了剪貼板數據的存放位置以后,就可以編寫代碼來處理它們了。由于我們的富文本輸入框比較簡單,所以只需要處理兩類數據即可,其一是普通的文本類型數據,包括 emoji 表情;其二則是圖片類型數據。 新建 paste.js 文件: 然后就可以在 onPaste 事件里面直接使用了: 上面的代碼支持文本格式,接下來就要對圖片格式進行處理了。玩過 的同學會知道,包括圖片在內的所有文件格式內容都會儲存在 File 對象里面,這在剪貼板里面也是一樣的。于是我們可以編寫一套通用的函數,專門來讀取 File 對象里的圖片內容,并把它轉化成 base64 字符串。 為了更好地在輸入框里展示圖片,必須限制圖片的大小,所以這個圖片處理函數不僅能夠讀取 File 對象里面的圖片,還能夠對其進行壓縮。 新建一個 chooseImg.js 文件: 回到上一步的 paste.js 函數,把其中的 TODO() 改寫成 chooseImg() 即可: 回到瀏覽器,如果我們復制一張圖片并在輸入框中執(zhí)行粘貼的動作,將可以在控制臺看到打印出了以 data:image/png;base64 開頭的圖片地址。 經過前面兩個步驟,我們后已經可以讀取剪貼板中的文本內容和圖片內容了,接下來就是把它們正確的插入輸入框的光標位置當中。 對于插入內容,我們可以直接通過 document.execCommand 方法進行。關于這個方法詳細用法可以在MDN文檔里面找到,在這里我們只需要使用 insertText 和 insertImage 即可。 但是在某些版本的 Chrome 瀏覽器下,insertImage 方法可能會失效,這時候便可以采用另外一種方法,利用 Selection 來實現。而之后選擇并插入 emoji 的操作也會用到它,因此不妨先來了解一下。 當我們在代碼中調用 window.getSelection() 后會獲得一個 Selection 對象。如果在頁面中選中一些文字,然后在控制臺執(zhí)行 window.getSelection().toString(),就會看到輸出是你所選擇的那部分文字。 與這部分區(qū)域文字相對應的,是一個 range 對象,使用 window.getSelection().getRangeAt(0) 即可以訪問它。range 不僅包含了選中區(qū)域文字的內容,還包括了區(qū)域的起點位置 startOffset 和終點位置 endOffset。 我們也可以通過 document.createRange() 的辦法手動創(chuàng)建一個 range,往它里面寫入內容并展示在輸入框中。 對于插入圖片來說,要先從 window.getSelection() 獲取 range ,然后往里面插入圖片。 這種辦法也能很好地完成粘貼圖片的功能,并且通用性會更好。接下來我們還會利用 Selection,來完成 emoji 的插入。 不管是粘貼文本也好,還是圖片也好,我們的輸入框始終是處于聚焦(focus)狀態(tài)。而當我們從表情面板里選擇 emoji 表情的時候,輸入框會先失焦(blur),然后再重新聚焦。由于 document.execCommand 方法必須在輸入框聚焦狀態(tài)下才能觸發(fā),所以對于處理 emoji 插入來說就無法使用了。 上一小節(jié)講過,Selection 可以讓我們拿到聚焦狀態(tài)下所選文本的起點位置 startOffset 和終點位置 endOffset,如果沒有選擇文本而僅僅處于聚焦狀態(tài),那么這兩個位置的值相等(相當于選擇文本為空),也就是光標的位置。只要我們能夠在失焦前記錄下這個位置,那么就能夠通過 range 把 emoji 插入正確的地方了。 首先編寫兩個工具方法。新建一個 cursorPosition.js 文件: 有了這兩個方法以后,就可以放入 editor 節(jié)點里面使用了。首先在節(jié)點的 keyup 和 click 事件里記錄光標位置: 記錄下光標位置后,便可通過調用 insertEmoji() 方法插入 emoji 字符了。 文章涉及的代碼已經上傳到倉庫,為了簡便起見采用 VueJS 處理了一下,不影響閱讀。最后想說的是,這個 Demo 僅僅完成了輸入框最基礎的部分,關于復制粘貼還有很多細節(jié)要處理(比如把別處的行內樣式也復制了進來等等),在這里就不一一展開了,感興趣的讀者可以自行研究,更歡迎和我留言交流~ 文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。 轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102311.htmldocument.querySelector(".editor").addEventListener("paste", (e) => {
console.log(e.clipboardData.items)
})
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
document.querySelector(".editor").addEventListener("paste", async (e) => {
const result = await onPaste(e)
console.log(result)
})
/**
* 預覽函數
*
* @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 讀取文件的內容在這里就不贅述了,感興趣的讀者可以自行查閱。
const imgEvent = {
target: {
files: [pasteFile]
}
}
chooseImg(imgEvent, (url) => {
resolve(url)
})
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)
})
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)
}
})
/**
* 獲取光標位置
* @param {DOMElement} element 輸入框的dom節(jié)點
* @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節(jié)點
* @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)
}
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) {
const text = editor.innerHTML
// 插入 emoji
editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)
// 光標位置后挪一位,以保證在剛插入的 emoji 后面
setCursorPosition(editor, this.cursorPosition + 1)
// 更新本地保存的光標位置變量(注意 emoji 占兩個字節(jié)大小,所以要加1)
cursorPosition = getCursorPosition(editor) + 1 // emoji 占兩位
}
尾聲
摘要:中大多數的輸入框都是標簽,但是由于業(yè)務中前端有使用到基于實現的富文本編輯器,在實現自動化測試編寫時自然會涉及到對富文本器進行操作處理檢查該編輯器的元素,可以看到和正常的不同,該結構為一個里面裹了個而在其中輸入文字,則是在改變中的如果有換行的 web中大多數的輸入框都是標簽,但是由于業(yè)務中前端有使用到基于REACT實現的富文本編輯器,在實現自動化測試編寫時自然會涉及到對富文本器進行操作處...
摘要:轉載來源包管理器管理著庫,并提供讀取和打包它們的工具。能構建更好應用的客戶端包管理器。一個整合和的最佳思想,使開發(fā)者能快速方便地組織和編寫前端代碼的下一代包管理器。很棒的組件集合。隱秘地使用和用戶數據。 轉載來源:https://github.com/jobbole/aw... 包管理器管理著 javascript 庫,并提供讀取和打包它們的工具。?npm – npm 是 javasc...
摘要:轉載來源包管理器管理著庫,并提供讀取和打包它們的工具。能構建更好應用的客戶端包管理器。一個整合和的最佳思想,使開發(fā)者能快速方便地組織和編寫前端代碼的下一代包管理器。很棒的組件集合。隱秘地使用和用戶數據。 轉載來源:https://github.com/jobbole/aw... 包管理器管理著 javascript 庫,并提供讀取和打包它們的工具。?npm – npm 是 javasc...
摘要:一個專注于瀏覽器端和兼容的包管理器。一個整合和的最佳思想,使開發(fā)者能快速方便地組織和編寫前端代碼的下一代包管理器。完全插件化的工具,能在中識別和記錄模式。健壯的優(yōu)雅且功能豐富的模板引擎。完整的經過充分測試和記錄數據結構的庫。 【導讀】:GitHub 上有一個 Awesome – XXX 系列的資源整理。awesome-javascript 是 sorrycc 發(fā)起維護的 JS 資源列表...
摘要:官網全新的靜態(tài)包管理器。官網一個整合和官網的最佳思想,使開發(fā)者能快速方便地組織和編寫前端代碼的下一代包管理器。官網小巧的兼容的所見即所得的富文本編輯器。官網富文本編輯器。官網由制作,適用于每天寫作的富文本編輯器。 1. 包管理器 管理著 javascript 庫,并提供讀取和打包它們的工具。 npm:npm 是 javascript 的包管理器。官網 cnpm:cnpm 是 由于國...
閱讀 961·2023-04-26 02:49
閱讀 1172·2021-11-25 09:43
閱讀 2541·2021-11-18 10:02
閱讀 2919·2021-10-18 13:32
閱讀 1281·2019-08-30 13:54
閱讀 2074·2019-08-30 12:58
閱讀 3008·2019-08-29 14:06
閱讀 2154·2019-08-28 18:10