Elm - textarea selection range disappearing
Asked Answered
M

1

6

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:

Text setup

Then pressing Unindent should show the following (with the selection of "def\ng" still intact):

Unindented with selection 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:

Unindented without selection

Mononuclear answered 21/6, 2017 at 1:20 Comment(2)
I'm either misunderstanding the problem or having a hard time recreating your issue in your sample. tab and shift-tab for me leaves focus (with text selected or not selected). Pressing setup then unindent tabs all lines and then unindent untabs the second two lines.Lifeordeath
@Lifeordeath Pressing setup then unindent should not only unindent the tabs on the last 2 lines, but it should also keep the selection highlighted on "def\ng". The problem is the selection goes away.Mononuclear
W
2

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

Weisburgh answered 4/10, 2019 at 9:27 Comment(1)
Interesting workaround with a hidden <div> to reflow the <textarea> element. Thank you for the compliment on my problem description.Mononuclear

© 2022 - 2024 — McMap. All rights reserved.