如何針對 contenteditable 元素做簡易 get/set value,以 React 為例

​
Aug 3 · 7 min read

前陣子剛好在用 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 />

line break in contenteditable (Chrome)

此外,透過上方 GIF 你又會發現第一個輸入的換行跟之後的換行行為不太一樣,而後 ctrl + a del 卻沒有把內容清乾淨,留下了個 \n,導致在抓取 contenteditable 值時要做一些額外的處理,如下範例


Get value

首先來看一下下面幾個狀況,以 Chrome 為例

當輸入內容只有一列時,瀏覽器並不會額外使用其他元素來包住內容
輸入多列時,第一列是赤裸裸的 text node,而後每列都會被 div 包起來
輸入換行會以 <div><br /></div> 呈現

因此可以觀察出使用者輸入的每一列剛好可以對應到 contenteditable 的 childNodes,於是我有個大膽的想法,那就靠 childNode 的內容加上換行不就能解決原本使用 contenteditableElement.innerText 取出來會有一堆不合預期的 \n,於是些修改了 handleInput 如下,就有了 get value 的雛形


Set value

在進入重點前,先來解決複製貼上的問題,由於在 contenteditable 中執行貼上的話會連格式也一起貼上

paste formatted text

所以我們需要去聽 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 元素中可能不是那麼適合

console.error(warning)

當你在 contenteditable 中塞了 children 就會得到這樣的 warning,除了 warning 外也會導致在動態改變 children 時產生預期外的結果甚至有機會讓 component crash

於是去翻了一下相關套件是乎都是用 dangerouslySetInnerHTML 來 set value,所以我們可以改寫成下面這樣,如此一來就能正常的給予初始值並正確取得使用者輸入的內容了


其他處理

在某些情況下雖然拿到得值是空字串但 DOM 中還存在著 <br /> 導致 placeholder 沒有照預期顯示,所以需要另外在 handleInput 時判斷當 text === ''innerHTML 清空

placeholder issue

然而你可能會有多個地方要用到 contenteditable,這時候可以把這些 get/set value 的邏輯抽成 custom Hook 讓大家共用,最終的完成體就會變成這樣:

大功告成~



最後,感謝你的閱讀,若文中有任何錯誤的地方還請糾正,喜歡本篇文章的話可以拍 11 下手,不喜歡的話可以拍 1 下

​

Written by

YY​ ​-​ ​ʟ​ᴠ​.​ ​1​3​ ​見​習​前​端​ ​I

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade