💨

Elmでわかる HTML5ドラッグ&ドロップAPI入門

2020/12/29に公開

HTML5のドラッグ&ドロップAPIですが使い方をすぐ忘れてしまうので備忘録的に記事を書いておきます。参考として、MDN ドラッグ&ドロップ APIdrag イベントのサンプルを利用しました。

今回サンプルとして作るアプリケーションは以下の仕様を満たします。

  • ドラッグを開始すると、ボタンが半透明になりラベルが「ふにゃ〜!!」に変わり、色が赤くなる
  • 「にゃ〜」が猫檻ゾーンに到達すると檻が灰色になる
    • 再び猫檻ゾーンから外れると檻が白色に戻る
  • 猫檻に「にゃ〜」をドロップすると「にゃーん。。。」になる
  • 猫檻ゾーンの外にドロップすると「にゃ〜」に変わり、色が黒くなる

Image from Gyazo

こちらが動くコードです。

ドラッグを開始すると、ボタンのラベルが「ふにゃ〜!!」に変わり、色が赤くなる(DragStart, DragEnd)

ボタンの透明度とラベルと色が変わるときはドラッグ中かそうでないかで変わります。ドラッグ中かどうかを判別するフラグはModelisDragged(初期値False)として定義しています。

type alias Model =
    { isDragged : Bool, isDragEnter : Bool, isPrisonEnter : Bool }


initialModel : Model
initialModel =
    { isDragged = False, isDragEnter = False, isPrisonEnter = False }

view関数では、buttonstylemodel.isDraggedを使用して値の出し分けをします。また、buttonをドラッグ可能な要素と見せたい場合は、Html.draggable関数に"true"を渡します。ドラッグ開始とドラッグ終了のイベントはそれぞれ、dragstart, dragendで表現されます。それらのイベントはElmの標準関数では用意されていないため、Html.Events.onを利用して、onDragStartonDragEndを定義しています。

view model =
...
button
    [ style "color" <|
        if model.isDragged then
            "red"

        else
            "black"
    , style "opacity"
        (if model.isDragged then
            "50%"

         else
            "100%"
        )
    , draggable "true"
    , onDragStart DragStart
    , onDragEnd DragEnd
    ]
    [ text <|
        if model.isDragged then
            "ふにゃ〜!!"

        else
            "にゃ〜"
    ]
    
    
onDragStart : msg -> Attribute msg
onDragStart msg =
    on "dragstart" <| JD.succeed msg

onDragEnd : msg -> Attribute msg
onDragEnd msg =
    on "dragend" <| JD.succeed msg

ドラッグ開始と終了のイベントはDragStartDragEndのMsgを発行します。ここでの変更は単純で、isDraggedの値を切り替えるだけです。

update : Msg -> Model -> Model
update msg model =
    case msg of
        NoOp ->
            model

        DragStart ->
            { model | isDragged = True }
	    
	DragEnd ->
            { model | isDragged = False }
	...

「にゃ〜」が猫檻ゾーンに到達すると檻が灰色になる・再び猫檻ゾーンから外れると檻が白色に戻る(DragEnter・DragLeave)

猫檻の色が変わるときはドラッグ領域に入ったかそうでないかで変わります。判別するフラグはModelisDragEnter(初期値False)として定義しています。

type alias Model =
    { isDragged : Bool, isDragEnter : Bool, isPrisonEnter : Bool }


initialModel : Model
initialModel =
    { isDragged = False, isDragEnter = False, isPrisonEnter = False }

猫檻を表すdivでは、model.isDragEnterを利用してbackgroundスタイルを変えています。ドラッグ領域に入ったかどうかを判定するイベントは、dragenteronDragEnter関数で定義しています。今回のサンプルでは必要ないのですが、ドラッグ領域の値をイベントをフックして取得することができます。event.target.classNameの値を取得するJsonデコーダtargetClassNameを定義しています。あとはイベント定義時に利用しています。ドラッグ領域を離れたときのイベントはdragleaveonDragLeave関数で定義しています。

, div
    [ class "prison"
    , onDragEnter DragEnter
    , onDragOver NoOp
    , onDragLeave DragLeave
    , onDrop Drop
    , style "background" <|
        if model.isDragEnter then
            "gray"

        else
            "white"
    ]
    
targetClassName : JD.Decoder String
targetClassName =
    JD.at [ "target", "className" ] JD.string

onDragEnter : (String -> msg) -> Attribute msg
onDragEnter tagger =
    on "dragenter" <| JD.map tagger targetClassName
    
onDragLeave : msg -> Attribute msg
onDragLeave msg =
    on "dragleave" <| JD.succeed msg

update関数では、isDragEnterを切り替えています。 せっかくなので、DragEnterの場合は、classNameで猫檻かどうかを判別をしています。

update msg model =
    case msg of
    ...
        DragEnter className ->
            if className == "prison" then
                { model | isDragEnter = True }

            else
                model

        DragLeave ->
            { model | isDragEnter = False }
   ...

猫檻に「にゃ〜」をドロップすると「にゃーん。。。」になる(Drop, DragOver)

猫檻にドロップされたかどうかは、model.isPrisonEnterフラグで判定します。

type alias Model =
    { isDragged : Bool, isDragEnter : Bool, isPrisonEnter : Bool }


initialModel : Model
initialModel =
    { isDragged = False, isDragEnter = False, isPrisonEnter = False }

「にゃ〜」がドロップされかどうかによって猫檻の中に「にゃーん。。。」を入れるかどうかをmodel.isPrisonEnterで判定しています。ドロップされたかどうかをdropイベントで検知するために、onDrop関数を定義しています。しかし、DropOverイベントをpreventDefaultしておかなければdropイベントに到達しません。ElmでpreventDefaultを使いたい場合にはonの代わりに、preventDefaultOnを利用します。preventDefaultにtrueを渡すためにタプル(msg, Bool)にする必要があるため補助関数alwaysStopを定義して利用します。dragoverのときはmodelは変化させる必要がないためNoOpのMsgを定義して発行をします。

, div
    [ class "prison"
    , onDragEnter DragEnter
    , onDragOver NoOp
    , onDragLeave DragLeave
    , onDrop Drop
    , style "background" <|
        if model.isDragEnter then
            "gray"

        else
            "white"
    ]
    [ text "猫檻"
    , if model.isPrisonEnter then
        button []
            [ text "にゃーん。。。"
            ]

      else
        emptyElement
    ]
    

onDrop : msg -> Attribute msg
onDrop msg =
    on "drop" <| JD.succeed msg
    

onDragOver : msg -> Attribute msg
onDragOver msg =
    preventDefaultOn "dragover" <| JD.map alwaysStop <| JD.succeed msg
    
    
alwaysStop : a -> ( a, Bool )
alwaysStop x =
    ( x, True )

以上でドラッグ&ドロップAPIをElmから利用することができました。

Discussion