Photo by Csabi Elter

Getting plain text from user input on a contenteditable element

It is no news that HTML contenteditable is a mess, however it is challenging even getting a plain text value of the user input.

Alberto Gasparin

--

Let me begin by saying that unless you have really specific needs, textarea is what you should use to get plain text input. The reason why I had to use contenteditable is because I need the box to be content aware and adapt its size dynamically (both width and height).

Getting content changed notifications

Assuming you have no other choice but using a contentEditable div as a textarea, your first problem is how to listen to user interactions. On modern browsers there is a unified event that you can subscribe to in order to get content changes: input. It will fire every time content changes in reaction to a user action. Problem solved, except it is not supported by IE on contentEditable elements (yep, I still have to deal with IE11).

A quick Google search will point to several StackOverflow answers, suggesting a combination of keyup blur cut paste events in order to achieve something similar to the missing input event. Simple enough, but not 100% accurate. In my case I’m dealing with an IE11 Webview on an enterprise app running under Windows 10, and contentEditable elements do not fire keyup events (wtf!).

Time to try with more creative solution: MutationObserver events. MutationObserver API allows you to listen to markup changes on an element, and content changes on a contentEditable element are included (and it is supported by IE11! Such a modern browser…). So, replacing all the event handlers with a single MutationObserver allowed me to get content changes notifications consistently.

let element = document.querySelector('div[contenteditable]');
let observer = new MutationObserver(mutations =>
mutations.forEach(mutation => {
console.log('input changed');
})
);
observer.observe(element, {
childList: true,
characterData: true,
subtree: true,
});

Next: getting the plain text content

Next challenge was trying to retrive the element contents as plain text. The first thing you might want to try is using the once-non-standard innerText API. It was initially not supported by Firefox, but luckily it has been standardised and added some releases ago so we can safely use it. However, innerText output is not consistent: Firefox has a bug that makes it ignore line breaks on user typed content.

To get the same output across every browser I used a temporary element to copy the html contents with all tags stripped, replacing new lines tags (either p, div or br) with a special marker (I settled on {ß®} but could be anything unusual). Then the innerText of that element will be clean and replacing the special marker with a standard new line \n will return a plain text representation with even html entities correctly handled (as innerHTML returns them as &xyz;).

function getText() {
let element = document.querySelector('div[contenteditable]');
let firstTag = element.firstChild.nodeName;
let keyTag = new RegExp(
firstTag === '#text' ? '<br' : '</' + firstTag,
'i'
);
let tmp = document.createElement('p');
tmp.innerHTML = element.innerHTML
.replace(/<[^>]+>/g, (m, i) => (keyTag.test(m) ? '{ß®}' : ''))
.replace(/{ß®}$/, '');
return tmp.innerText.replace(/{ß®}/g, '\n');
}

Those ten lines will handle the basic use cases, however if you need more control and more features it is better to find a well supported editor instead of crafting your own solution: the amount of edge cases is endless.

Is the contenteditable API going to improve anytime soon? Well, sadly looks like that despite some efforts we will continue to fight with the current version for a long time, but it is never too late to raise your voice!

--

--

Alberto Gasparin

Being a Frontend Developer is learning something new every day