So now I have run into the classic problem of making changes to a
contenteditable from code resulting in the caret not knowing where it should be, so jumping back to the start of the line.
The problem in Elm is that the updates to the text happen in the virtual dom code, so I cannot modify its behaviour to save and restore the caret position like this.
To work around this I had to do something a bit icky. Essentially, I capture the mutation event, and figure out from that what the users editing intention was, and insert the text in the right place in the Elm-side Model. When the view is re-rendered, the line is replaced entirely with the correct text. The
contenteditable caret is made invisible and my own custom caret is rendered in Elm. It does not really matter where on the line the browsers caret is, so long as it is on the right line.
I also ensure that when edits happen on a line, the whole line is forced to re-render. This is done with
Html.Keyed by bumping a counter on each edit, so that a unique key can be set against the edit line. Since this changes on each edit, the virtual dom forces a redraw of the line. This helps when syntax highlighting is triggered, since that makes structural changes to the DOM, in the
I’m definitely getting into brittle code territory with this, so don’t feel great about it - am I relying on specifics of the vdom implementation? and is this going to work nicely on all browsers? Can’t really see another way though.
In that stack overflow question, the code to maintain the caret position whilst making changes to the text looks like:
var position = getCaretCharacterOffsetWithin(input.get(0));
var text = input.text();
text = text.replace(new RegExp('\\btest\\b', 'ig'), '<span style="background-color: yellow">test</span>');
A pseudo-code outline is:
- Get the caret position.
- Update the text/html in the element.
- Set the caret back to the correct position.
The Elm virtual DOM just does step 2.
I also tried to complete the algorithm by storing the current caret position in my Model (step 1), and then passing that as an attribute into the
elm-editor custom element and have it do step 3.
The problem with that is that the attribute seems to get updated by the
virtual-dom first, and then the contents. So it gets steps 2 and 3 the wrong way around. The result is that the caret ends up at the start of the line.
I also tried using an animation frame event to do step 3. It kind of works, but you can see the caret ever so briefly appears at the start of the line before jumping back to the correct place. The result is, if you type fast or fat finger the keyboard, you often get characters appearing at the start of the line, instead of where they are meant to go. I wasn’t really expecting this approach to work…
Time to step back a bit I think and work out what a more robust solution is going to look like.