I implemented a <textarea>
in Elm such that tabs indent and unindent instead of change focus to another HTML element. Works great except that unindenting sometimes causes the selection to disappear! If I'm selecting the 5th character to the 12th character, I press shift-tab, then it removes 2 tab characters, but it also makes the selection change to a cursor at position 10. The selection range should remain the same..
I have an SSCCE at Ellie: https://ellie-app.com/3x2qQdLqpHga1/2
Here are some screenshots to illustrate the problem. Pressing Setup shows this:
Then pressing Unindent should show the following (with the selection of "def\ng" still intact):
Unfortunately, pressing Unindent actually shows the following. The text is unindented fine, but the selection range goes away and there's just a cursor between the g
and the h
:
Interesting issue and excellent problem illustration!
The problem is that for some reason re-rendering doesn't occur when one of the selectionStart/selectionEnd properties remains the same. Try changing 5 to 6 on line #42.
It works when you introduce a forced reflow in the element structure. See here: https://ellie-app.com/6Q7h7Lm9XRya1 (I updated it to 0.19 to see if that would solve the problem, but it didn't).
Note that this probably re-renders the whole textarea anew so it might cause problems if the textarea is a huge piece of code. You could solve that by alternating between two identical textareas where you toggle their visibility every render.
module Main exposing (Model, Msg(..), main, update, view)
-- Note: this is Elm 0.19
import Browser
import Browser.Dom exposing (focus)
import Html exposing (Html, button, div, text, textarea)
import Html.Attributes exposing (attribute, class, cols, id, property, rows, style, value)
import Html.Events exposing (onClick)
import Html.Lazy exposing (lazy2)
import Json.Encode as Encode
import Task exposing (attempt)
type alias Model =
{ content : String
, selectionStart : Int
, selectionEnd : Int
-- keep counter of renderings for purposes of randomness in rendering loop
, renderCounter : Int
}
main =
Browser.element
{ init = initModel
, view = view
, update = update
, subscriptions = \s -> Sub.none
}
initModel : () -> ( Model, Cmd Msg )
initModel flags =
( Model "" 0 0 0, Cmd.batch [] )
type Msg
= Setup
| Unindent
| NoOp (Result Browser.Dom.Error ())
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
newRenderCounter =
model.renderCounter + 1
in
case msg of
Setup ->
( { model
| content = "\tabc\n\tdef\n\tghi"
, selectionStart = 5
, selectionEnd = 12
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
Unindent ->
( { model
| content = "\tabc\ndef\nghi"
, selectionStart = 5
, selectionEnd = 10
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
NoOp _ ->
( model, Cmd.batch [] )
view : Model -> Html Msg
view model =
div []
(viewTextarea model model.renderCounter
++ [ button [ onClick Setup ] [ text "Setup" ]
, button [ onClick Unindent ] [ text "Unindent" ]
]
)
viewTextarea : Model -> Int -> List (Html msg)
viewTextarea model counter =
let
rerenderForcer =
div [attribute "style" "display: none;"] []
ta =
textarea
[ id "ta"
, cols 40
, rows 20
, value model.content
, property "selectionStart" <| Encode.int model.selectionStart
, property "selectionEnd" <| Encode.int model.selectionEnd
]
[]
in
-- this is the clue. by alternating this every render, it seems to force Elm to render the textarea anew, fixing the issue. Probably not very performant though. For a performant version, use an identical textarea instead of the div and make sure the two selectionStart/end properties both differ from the previous iteration. Then alternate visibility between the two every iteration.
if isEven counter then
[ ta, rerenderForcer ]
else
[ rerenderForcer, ta ]
isEven : Int -> Bool
isEven i =
modBy 2 i == 0
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With