4
votes

How can I have the cursor kept in the right place when typing in a contenteditable div?

I am using Elm to create a text box, and I need the text in the box to be processed on every input. The reason for using a contenteditable div rather than textarea is that I need to be able to recognize certain words and make them into clickable links as you type.

The problem is that when the text gets updated, the cursor goes back to the beginning of the text, so everything is typed in back-to-front.

Here is a minimal example that demonstrates the problem:

module Main exposing (..)

import Html
import Html.Events as Hev
import Html.Attributes as Hat
import Json.Decode as Json

main : Program Never Model Msg
main =
    Html.beginnerProgram
        { model = ""
        , update = update
        , view = view
        }

type Msg = NewText String

type alias Model = String

view : Model -> Html.Html Msg
view model =
    Html.div
        [ Hat.contenteditable True
        , Hev.on "input" (Json.map NewText targetTextContent)
        , Hat.style
            [ ("border", "1px solid darkgray") ]
        ]
        [ Html.text model
        ]

targetTextContent : Json.Decoder String
targetTextContent =
    Json.at ["target", "innerText"] Json.string

update : Msg -> Model -> Model
update msg model =
    case msg of
        NewText text -> text
1
You will probably need to maintain the position of the cursor yourself, which might sounds more complicated than it is. This project has done similar thing to keep track of the cursor and selection: Janiczek's elm-editorTomáš Zemanovič

1 Answers

0
votes

This seems very similar to the problem here: Elm - textarea selection range disappearing

The problem there was that the changed selection property did not get rerendered by the browser when Elm diffed and re-rendered the view, keeping the HTML. I solved it there using a hack that reflows the content on every iteration, thereby forcing the elements to actually re-render, and the selection property to be applied correctly.

Put a counter in your model and increase it every time you render the view. Then alternatingly insert an empty div in front of your text box element. (if i % 2 == 0 ...)