前陣子剛好在用 contenteditable 來做可以隨使用者輸入內容自動長高的輸入框,為了要能多行輸入而且日後可能會導入些 mention、RTE 功能所以就不是使用 input 或 textarea,但也還沒打算在現階段採用現成 RTE 工具,就先自己用個 contenteditable 扛一下吧
而在過程中發現有很多眉角要處理,像是要在 React 架構下給 initial value、若輸入內容有換行字元則取出來的值會跟你預期的不一樣等等,最終找的了些堪用的手段來處理 get、set value
contenteditable 怎麼了嗎?
直接切入主題,如同上面所述,當我們在 contenteditable 元素中輸入換行字元,不同瀏覽器會有不同的邏輯來呈現「換行」
Use of
contenteditable
across different browsers has been painful for a long time because of the differences in generated markup between browsers. For example, even something as simple as what happens when you press Enter/Return to create a new line of text inside an editable element was handled differently across the major browsers (Firefox inserted<br>
elements, IE/Opera used<p>
, Chrome/Safari used<div>
).
不僅僅可能會遇到瀏覽器用不同元素來包起每一列內容,還會像上方可以看到在 Chrome 跟 FireFox 於同一個 contenteditable 元素中輸入相同的內容卻會產出不同的 HTML markup,Chrome 的第一個 @
沒有被 div 包起來,而 FireFox 在最後會多加一個 <br />
此外,透過上方 GIF 你又會發現第一個輸入的換行跟之後的換行行為不太一樣,而後 ctrl + a
del
卻沒有把內容清乾淨,留下了個 \n
,導致在抓取 contenteditable 值時要做一些額外的處理,如下範例
Get value
首先來看一下下面幾個狀況,以 Chrome 為例
因此可以觀察出使用者輸入的每一列剛好可以對應到 contenteditable 的 childNodes
,於是我有個大膽的想法,那就靠 childNode 的內容加上換行不就能解決原本使用 contenteditableElement.innerText
取出來會有一堆不合預期的 \n
,於是些修改了 handleInput
如下,就有了 get value 的雛形
Set value
在進入重點前,先來解決複製貼上的問題,由於在 contenteditable 中執行貼上的話會連格式也一起貼上
所以我們需要去聽 onPaste
再把使用者要貼上的內容轉換成純文字貼在我們的 contenteditable 中
const handlePaste = e => {
e.preventDefault();
document.execCommand(
'insertText',
false,
e.clipboardData.getData('text')
)
};return <div contenteditable onPaste={handlePaste} />
Set initial value
由於一般元素套上 contenteditable 後也不會有如同 input 的 value 屬性可以使用,若要給予初始值的話就要像 textarea 給 children 或操作 innerHTML,但在 React 中直接塞 children 到 contenteditable 元素中可能不是那麼適合
當你在 contenteditable 中塞了 children 就會得到這樣的 warning,除了 warning 外也會導致在動態改變 children 時產生預期外的結果甚至有機會讓 component crash
於是去翻了一下相關套件是乎都是用 dangerouslySetInnerHTML
來 set value,所以我們可以改寫成下面這樣,如此一來就能正常的給予初始值並正確取得使用者輸入的內容了
其他處理
在某些情況下雖然拿到得值是空字串但 DOM 中還存在著 <br />
導致 placeholder 沒有照預期顯示,所以需要另外在 handleInput
時判斷當 text === ''
把 innerHTML
清空
然而你可能會有多個地方要用到 contenteditable,這時候可以把這些 get/set value 的邏輯抽成 custom Hook 讓大家共用,最終的完成體就會變成這樣:
大功告成~
參考資料
最後,感謝你的閱讀,若文中有任何錯誤的地方還請糾正,喜歡本篇文章的話可以拍 11 下手,不喜歡的話可以拍 1 下