Elmで親子関係のあるdrag移動を実現する

どうも、うじまるです

今作ってるものが親子関係のあるdrag移動が必要なものでそれをElmでいい感じに書けたので共有したいと思います。

リポジトリは[こちら](https://github.com/uzimaru0000/elm-drag-demo)です。

# 環境
- Elm 0.19

# 利用した主なpackages
- [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/Element)
- [elm-reference](package.elm-lang.org/packages/arowM/elm-reference/latest/Reference)

# 概要
- ドラッグしたら移動する
- 親を移動したら子も移動する

# 手順
### 移動させる対象の型を定義する
今回は`Entity`という名前にしました。
定義は以下のようにしました。

```elm
type Entity = Entity Transform Content


type Content
    = Note (List Entity)
    | Sticky String


type alias Transform =
    { pos : Point
    , size : Point
    }


type alias Point =
    { x : Int
    , y : Int
    }
```

`Entity`は自分の位置とサイズと内容（`Note`の場合は`Entity`のリスト、`Sticky`の場合は文字列）を持っています。
この際、自分の位置は親に対する**相対位置**ということにします。

### Model
`Model`型は以下のようにしました。

```elm
type alias Model =
    { entities : List Entity
    , ref : Maybe (Reference Entity (List Entity))
    }
```

entitiesは上で定義した`Entity`のリストです。
refは現在移動中の`Entity`への**参照**です。

え、Elmって参照ないですよね？？って思ったそこのあなた！
それを実現するpackageが[elm-reference](package.elm-lang.org/packages/arowM/elm-reference/latest/Reference)です。

詳しい説明は[一覧のViewを自在に操る elm-reference](https://qiita.com/arowM/items/6eaadc5f43880e1f3787)を参照してください。

これを使うことで`Entity`型のようなちょっと複雑な値の更新が楽になります。

### View
```elm
view : Model -> Element Msg
view model =
    UI.row
        (Reference.top model.entities
            |> Reference.unwrap entityView
        )
        []

entityView : Reference Entity (List Entity) -> UI.Attribute Msg
entityView ref =
    let
        commonAttrs transform =
            transformAttr transform
                ++ [ Utils.stopPropagationOn "mouseup" End
                   , Utils.stopPropagationOn "mousedown" <| Select ref
                   , Html.style "cursor" "move" |> UI.htmlAttribute
                   ]
    in
    case Reference.this ref of
        Entity transform (Note entities) ->
            UI.el
                (commonAttrs transform
                    ++ (Reference.fromRecord
                            { this = entities
                            , rootWith = Reference.rootWith ref << Entity transform << Note
                            }
                            |> Reference.unwrap entityView
                       )
                    ++ [ BG.color <| UI.rgb 0.2 1.0 0.5 ]
                )
                UI.none
                |> UI.inFront

        Entity transform (Sticky str) ->
            UI.el
                (commonAttrs transform
                    ++ [ BG.color <| UI.rgb 0.2 0.5 1.0 ]
                )
                (UI.text str)
                |> UI.inFront
```

ここで注意するべき点は、`stopPropagationOn`を使うことです。今回は、`Html.Events`に入っている
`stopPropagationOn : String -> Decoder (msg, Bool) -> Attribute msg`
を使いやすいように
`stopPropagationOn : String -> msg -> Element.Attribute msg`
にしています。
`stopPropagationOn`を使うことでイベントがこれ以上伝播しなくなります。そうしないと子オブジェクトでmousedownしたときに親オブジェクトのmousedownが呼ばれてしまいます！

そういえばこのview関数ではmousemoveをつけていませんね！
安心してください、ここで設定していないだけです！

### Subscriptions
```elm
subscriptions : Model -> Sub Msg
subscriptions model =
    if model.ref /= Nothing then
        Point.decoder ( ["movementX"], ["movementY"] )
            |> JD.map Drag
            |> Browser.onMouseMove

    else
        Sub.none
```
model.refがNothingじゃないとき（何らかのEntityをクリックしているとき）にグローバルなmousemoveを発行します。
なぜグローバルなのかというと、マウスポインターの移動とMsgの発行がズレてしまうとDomからポインターがはみ出るという悲しい自体が起こってしまうからです。

### Update
```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Select ref ->
            ( { model | ref = Just ref }
            , Cmd.none
            )

        Drag p ->
            let
                updatedReference =
                    model.ref
                        |> Maybe.map (Reference.modify <| Entity.move p)
            in
            ( { model
                | entities =
                    updatedReference
                        |> Maybe.map Reference.root
                        |> Maybe.withDefault model.entities
                , ref = updatedReference
              }
            , Cmd.none
            )

        End ->
            ( { model | ref = Nothing }
            , Cmd.none
            )
```

`Select`と`End`はいいと思うので`Drag`の説明をしましょう。
`Drag`はさっきのSubscriptionsで発行されるMsgです。マウスの移動量を受け取ります。
あとは`model.ref`に`Reference.modify`で移動量を足し合わせてあげ、`model.entities`を更新します。refも移動したものに更新してあげます。

# まとめ
`elm-reference`で参照を手に入れることでいい感じに書くことができます！
リストに使うのも有効ですが、今回のようなツリー構造のようなデータには`elm-reference`はとても有効なのでそのような機会があったらぜひ使ってみてください！