Florian’s Blog

Content Editable is a Scary Place

Once in a while, people want to make part of a web page editable. They hear about the HTML contentEditable attribute, and wonder if this may be a solved problem. It is not. It is a minefield.

Whole books could probably be written about this, but here’s a little primer about how things are today, why it is a hard problem, and how there’s hope that it is going to get better.

TL;DR: If you want to use contenteditable now, don’t do it directly and instead use a pre-made javascript editor, such as CKEditor, tiny MCE, and the like. If they don’t do what you want, and you need to do this yourself now, be prepared for a lot of pain, or for waiting for newer standards to stabilise, or both.

Now let’s dive in.

Contenteditable is an attempt at having a high level construct that would enable rich text editing in web pages, letting browsers do all the heavy lifting, and letting the user (via typing, keyboard shortcuts, contextual menus…) or the javascript (via invocations of execCommand) just ask for these things to happen.

There are a ton of entangled reasons why this is complex, but just to get a sense of it, here is a contrived example. You can try playing with it here but I encourage you to think through it before trying:

<div contenteditable=true>
	<table id=t1>
		<tr><td>lorem <td>ipusm <td>dolor
		<tr><td>si <td>amet <td>consectetur
		<tr><td>adipiscing <td>elit <td>Quisque
	</table>
	<table id=t2>
		<tr><td>elem <td>constur <td>sem <td>et <td>supit
		<tr><td>poror <td>faubus <td>tindunt <td>Pheus <td>aliam
		<tr><td>ecitur <td>pesque <td>Maenas <td>ex <td>liga
		<tr><td>soidin <td>codo <td>Mis <td>sotun <td>dissim
	</table>
</div>
<ol> <li>1 <li>2 <li>3 <li>4 <li>5 <li>6 <li>7 </ol>
table { border-collapse: collapse; }

#t1 td{
	background: red;
	font-family: serif
}

#t2 { border: dashed 5px gray; }
#t2 td {
	font-family: sans-serif;
	border: solid 1px black;
	font-weight: bold;
}
#t2 tr { background: #bbffbb; }
#t2 tr:nth-of-type(2n) { background: #ccccff; }
#t2 td:first-child { user-select: none; }

body > ol {
	font-family: monospace;
	border: dotted orange;
}

Got that? Now the user creates a selection that goes from the last cell of the last row of the first table to the second cell in the first row of the second table. Then they press “a” on the keyboard. Generally, selecting something and then typing means replacing the selection with what was typed, but in this case, what does that mean?

There’s a million subtleties like this, many of which don’t have an obvious correct answer, as it depends what you’re trying to do.

The end result is that browsers are full of bugs and are inconsistent with each other, that the specs (ContentEditableTrue and execCommand) don’t cover all the cases and aren’t followed particularly closely by the browsers anyway. Even if that was solved and everybody harmonised on one behaviour (which isn’t happening, as browsers have mostly given up), it still wouldn’t be good enough, because as a user maybe that harmonised behaviour is not the one you wanted, and now you want a separate method or way to opt into that alternative behavior.

So web-based editors (CKEditor, TinyMCE, google docs…) go to great lengths to work around contenteditable, instead of using it. For example they do live DOM diffing, to try and figure out what contenteditable did to the document and for what reason, undo it, and do it again in a different way.

So we come to plan B.

What people are working on now (with Johannes as a spec editor) is a completely different approach, where the browser does not do the heavy lifting, and instead, just provides events to inform a javascript based editor about what it is that the user is trying to do, and APIs to facilitate doing that.

Step 1 in that story (which is reasonably far along) is to make sure that everything that would cause a change in a contenteditable element fires a Javascript event before that change occurs, which:

Step 2 in that story is to provide multiple modes of contenteditable, where contenteditable=true is the one we know today, kept for legacy reasons, but other contenteditable=[something else than true or false] provide modes where all the events described in step 1 still fire, the insertion caret is still drawn, but depending on the mode, some of the events do not have a default action provided by the browser, and unless js reacts to them, nothing happens at all.