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

YY
7 min readAug 3, 2019

--

就是一張意義不明的圖

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

不僅僅可能會遇到瀏覽器用不同元素來包起每一列內容,還會像上方可以看到在 Chrome 跟 FireFox 於同一個 contenteditable 元素中輸入相同的內容卻會產出不同的 HTML markup,Chrome 的第一個 @ 沒有被 div 包起來,而 FireFox 在最後會多加一個 <br />

line break in contenteditable (Chrome)

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

contenteditable demo 1

Get value

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

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

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

contenteditable demo 2

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

contenteditable demo 3

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

contenteditable demo 4

其他處理

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

placeholder issue

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

contenteditable demo 5

大功告成~

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

--

--

YY
YY

Written by YY

為五斗米折腰的前端打雜仔

No responses yet