Elm - text area selection range disappears

I have implemented <textarea>

in Elm so that the tabs are indent and unindent instead of changing focus to another HTML element. Works great, except that unindenting makes the selection disappear sometimes! If I select the 5th character for the 12th character, I press shift- tab, then it removes 2 tabs, but also makes the cursor change selection at position 10. The selection range should remain the same ..

I have SSCCE in Ellie: https://ellie-app.com/3x2qQdLqpHga1/2

Here are some screenshots to illustrate the problem. Pressing the Setup button displays the following:

Customizing text

Then pressing Unindent should show the following (when selecting "def \ ng" still out of stock):

Inconsistent with chaining preservation

Unfortunately, pressing Unindent actually shows the following. The text is not intense, but the selection range goes away and there is only a cursor between g

and h

:

No indication, no choice

+5


source to share


1 answer


An interesting problem and a great illustration of the problem!

The problem is, for some reason, the re-rendering does not occur when one of the selectionStart / selectionEnd properties remains the same. Try changing 5 to 6 on line # 42.

This works when you force a reflow in the element structure. See it here: https://ellie-app.com/6Q7h7Lm9XRya1 (I updated it to 0.19 to see if that solves the problem, but it doesn't).



Note that this is likely to re-render the entire text area from scratch, so this can cause problems if the text area is a huge piece of code. You can solve this problem by switching between two identical text areas, where you switch their visibility on 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


      

0


source







All Articles