4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elmでcontenteditableを使いたかった

Last updated at Posted at 2019-10-05

Elmでcontenteditableな要素を使いたかったのですが、いろいろ問題があって難しいようです。
途中までがんばった結果
今のところ以下のようになっているのですが、それに至るまでの流れを書いておこうと思います。

module Main exposing (main)

import Browser
import Html exposing (Html, div, label, text)
import Html.Attributes exposing (contenteditable, value)
import Html.Events exposing (keyCode, on, preventDefaultOn)
import Json.Decode as JD


main : Platform.Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , view = view
        , update = update
        }


type alias Model =
    { text : String
    }


type Msg
    = UpdateText String
    | NoOp


init =
    Model "init"


view : Model -> Html Msg
view model =
    div []
        [ div
            [ contenteditable True
            , preventDefaultOn "keydown"
                (JD.map (\x -> ( NoOp, isEnterCode x )) <| keyCode)
            , on "blur" <|
                JD.map UpdateText
                    (JD.at [ "target", "textContent" ] JD.string)
            ]
            [ text model.text ]
        , label [] [ text <| "edited: " ++ model.text ]
        ]


update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateText newText ->
            { model | text = newText }

        NoOp ->
            model


isEnterCode : Int -> Bool
isEnterCode code =
    let
        enterCode =
            13
    in
    code == enterCode

1. onInputではevent.target.valueを参照しているため(?)失敗した

一番最初に書いたのが以下のようなコードでした。event.target.valueが定義されていないためか、そもそもUpdateTextが呼ばれません。

view model =
    div []
        [ div [ contenteditable True, onInput UpdateText ] [ text model.text ]
        , label [] [ text <| "edited: " ++ model.text ]
        ]

2. onInputではだめだった

event.target.valueがあればいいのかと思って以下のようにしました。UpdateTextは呼ばれましたが、画面で編集しているのはevent.target.valueではなくtextContentなのでmodelは更新されません。

view : Model -> Html Msg
view model =
    div []
        [ div [ contenteditable True, onInput UpdateText, value model.text ] [ text model.text ]
        , label [] [ text <| "edited: " ++ model.text ]
        ]

3. カーソルが勝手に移動する&TEAが壊れる

textContentを使ってUpdateTextが呼ばれればいいはずなので自前でMsgを作りました。
modelは期待通り更新されましたが、更新時にカーソルが要素の先頭に移動してしまいます。
また、文面では伝えづらいですが改行を入力するとTEAが壊れることに気づきました。1

view model =
    div []
        [ div
            [ contenteditable True
            , on "input" <|
                JD.map UpdateText
                    (JD.at [ "target", "textContent" ] JD.string)
            ]
            [ text model.text ]
        , label [] [ text <| "edited: " ++ model.text ]
        ]

4. リアルタイム更新を妥協した

inputでの更新を諦めてblurイベントを取るようにし、カーソルが移動する問題を回避しました。
Enterキーを検知してpreventDefaultすることで、TEAが壊れる問題を回避しました。
ここまでが冒頭に出した結果になります。

view model =
    div []
        [ div
            [ contenteditable True
            , preventDefaultOn "keydown"
                (JD.map (\x -> ( NoOp, isEnterCode x )) <| keyCode)
            , on "blur" <|
                JD.map UpdateText
                    (JD.at [ "target", "textContent" ] JD.string)
            ]
            [ text model.text ]
        , label [] [ text <| "edited: " ++ model.text ]
        ]

5. まだ問題がある

大体解決したように見えますが、改行を含む文字列をcontenteditableな要素に貼り付けると、やはりTEAが壊れます。
対策するとしたらpasteイベントを検知して事前に除外するくらいでしょうか…2

といったようにElmでcontenteditableを使うのは大変なようです。気が向いたら続きをやるかもしれません。

  1. 改行入力時にブラウザのデフォルト動作でタグが追加されてしまうため、VDOMと整合性が取れなくなったものと思われます。

  2. ドラッグ&ドロップとかでも問題起きそうですね

4
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?