The background
When it comes to contenteditable
, it's a real pain –– even more so if you try to create a text editor with it (unpleasant memories unveiling...), such as handling line breaks, empty divs and poor browser compatibiliy of contenteditable with value.
The list is growing. Recently I stumbled upon an issue while rebuilding the editable card with contenteditable
for Saiku in Vue. I tried to use v-model
to two-way bind a contenteditable
block's text content, with the following pseudo code:
content
is limited to plain text in my scenario. I excluded the part where I sanitized user input in the snippet as it's not relevant to the topic, so for now let's just assume what we're dealing with here only involves plain text.
Normally we should've listened to the change
event and updated content
accordingly, but contenteditable
blocks don't even fire this event in the first place.
The code works as expected in Chrome. But in Safari and Firefox, the caret position always jumps to the beginning while typing in the editable block.
The problem
After some research I found the root cause:
- because
content
is reactive, Vue re-renders the editablediv
every time theinput
event is fired; - in Safari and Firefox, the caret position will default to the very start when the node is re-rendered.
Now we know what's under the hood, we could try to fix it.
The fix
Since altering the browser behavior is out of the question, I could think of two ways: either mimicking the change
event, or de-couple the re-rendering process from reactive value change.
Method 1: what's the equivalent of onchange
?
According to W3C's definition1, the condition to fire a change
event requires:
Using the keyboard or an assistive technology that emulates the keyboard, move focus to the input control and alter the input control's value.
Using the keyboard or an assistive technology that emulates the keyboard, move focus off the input control to trigger the onChange event.
So if we know there's any text content change happening between the focus shift, it qualifies as a change
event. To implement this, we need to monitor the blur
event and check if anything has changed.
Not sure if this fix covers every edge case and it needs further testing.
Method 2: Decoupling
We only want the node to re-render when the input stops. Though I'm not terribly familiar with React, I know there's a shouldComponentUpdate
that determines if any re-rendering is necessary. Vue doesn't have such a hook in its lifecycle, but it does provide v-once
directive to apply on the node that you want to skip subsequent re-renders. Additionally, we need to manually mutate the editable block's content (in this case, text) when a user stops typing.
The fix in fiddle:
“ONCHANGE Attribute for Input Elements.” HTML Test Suite for UAAG 1.0 (Draft), https://www.w3.org/WAI/UA/TS/html401/cp0102/0102-ONCHANGE-INPUT.html.↩