Elm - 文本区域选择范围消失

Elm - textarea selection range disappearing

我在 Elm 中实现了 <textarea>,这样制表符可以缩进和取消缩进,而不是将焦点转移到另一个 HTML 元素。效果很好,只是取消缩进有时会导致选择消失!如果我选择第 5 个字符到第 12 个字符,我按 shift-tab,然后它会删除 2 个制表符,但它也会选择更改为位置 10 处的光标。选择范围应保持不变..

我在 Ellie 获得了 SSCCE:https://ellie-app.com/3x2qQdLqpHga1/2

这里有一些截图来说明这个问题。按 设置 显示:

然后按Unindent 应该显示以下内容(选择"def\ng"仍然完好无损):

不幸的是,按 Unindent actually 显示以下内容。文本没有缩进,但选择范围消失了,gh:

之间只有一个光标

有趣的问题和出色的问题说明!

问题是,由于某些原因,当 一个 属性保持不变时,不会发生重新渲染。尝试将第 42 行的 5 更改为 6。

当您在元素结构中引入强制回流时,它会起作用。参见此处:https://ellie-app.com/6Q7h7Lm9XRya1(我将其更新为 0.19 以查看是否可以解决问题,但没有)。

请注意,这可能会重新呈现 whole textarea,因此如果 textarea 是一段巨大的代码,它可能会导致问题。你可以通过在两个相同的文本区域之间交替来解决这个问题,你可以在每个渲染中切换它们的可见性。

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