annaghi / dnd-list / DnDList.Groups

If the list is groupable by a certain property, the items can be transferred between those groups. Instead of using drop zones, this module requires the list to be gathered by the grouping property and possibly prepared with auxiliary items. Here is a demo with groups, we will use it as an illustration throughout this page.

This module is a modified version of the DnDList module. The Config was extended with a new field called groups, and the movement field was withdrawn.

With groupable items the drag source and the drop target items can belong to the same group or to different groups. So now the internal sorting distinguishes between these two cases and we need to configure:

 

Meaningful type aliases

type alias DragIndex =
    Int

type alias DropIndex =
    Int

type alias DragElementId =
    String

type alias DropElementId =
    String

type alias Position =
    { x : Float
    , y : Float
    }

System


type alias System a msg =
{ model : Model
, subscriptions : Model -> Platform.Sub.Sub msg
, commands : Model -> Platform.Cmd.Cmd msg
, update : Msg -> Model -> List a -> ( Model
, List a )
, dragEvents : DragIndex -> DragElementId -> List (Html.Attribute msg)
, dropEvents : DropIndex -> DropElementId -> List (Html.Attribute msg)
, ghostStyles : Model -> List (Html.Attribute msg)
, info : Model -> Maybe Info 
}

A System encapsulates:

Later we will learn more about the Info object and the System fields.

create : Config a -> (Msg -> msg) -> System a msg

Creates a System object according to the configuration.

Suppose we have two groups:

type Group
    = Left
    | Right

and a list which is gathered by these groups and prepared with auxiliary items:

type alias Item =
    { group : Group
    , value : String
    , color : String
    }

preparedData : List Item
preparedData =
    [ Item Left "C" blue
    , Item Left "2" red
    , Item Left "A" blue
    , Item Left "footer" transparent
    , Item Right "3" red
    , Item Right "1" red
    , Item Right "B" blue
    , Item Right "footer" transparent
    ]

The auxiliary items separate the groups and they can be considered as header or footer of a particular group. In this case they are footers.

The sort operations were designed with the following list state invariant in mind:

And now the System is a wrapper type around the list item and our message types:

system : DnDList.Groups.System Item Msg
system =
    DnDList.Groups.create config MyMsg


type Msg

Internal message type. It should be wrapped within our message constructor:

type Msg
    = MyMsg DnDList.Groups.Msg

Config


type alias Config a =
{ beforeUpdate : DragIndex -> DropIndex -> List a -> List a
, listen : Listen
, operation : Operation
, groups : { listen : Listen
, operation : Operation
, comparator : a -> a -> Basics.Bool
, setter : a -> a -> a } 
}

Represents the System's configuration.

This is our configuration with a void beforeUpdate:

config : DnDList.Groups.Config Item
config =
    { beforeUpdate = \_ _ list -> list
    , listen = DnDList.Groups.OnDrag
    , operation = DnDList.Groups.Rotate
    , groups =
        { listen = DnDList.Groups.OnDrag
        , operation = DnDList.Groups.InsertBefore
        , comparator = comparator
        , setter = setter
        }
    }

comparator : Item -> Item -> Bool
comparator item1 item2 =
    item1.group == item2.group

setter : Item -> Item -> Item
setter item1 item2 =
    { item2 | group = item1.group }


type Listen
    = OnDrag
    | OnDrop

Represents the event for which the list sorting is available.


type Operation
    = InsertAfter
    | InsertBefore
    | Rotate
    | Swap
    | Unaltered

Represents the list sort operation. Detailed comparisons can be found here: sorting on drag and sorting on drop.

Info


type alias Info =
{ dragIndex : DragIndex
, dropIndex : DropIndex
, dragElementId : DragElementId
, dropElementId : DropElementId
, dragElement : Browser.Dom.Element
, dropElement : Browser.Dom.Element
, startPosition : Position
, currentPosition : Position 
}

Represents the information about the drag source and the drop target items. It is accessible through the System's info field.

You can check the Info object to decide what to render when there is an ongoing dragging, and what to render when there is no dragging:

itemView : Model -> ... -> Html.Html Msg
itemView model ... =
    ...
    case system.info model.dnd of
        Just _ ->
            -- Render when there is an ongoing dragging.

        Nothing ->
            -- Render when there is no dragging.

Or you can determine the current drag source item using the Info object:

maybeDragItem : DnDList.Groups.Model -> List Item -> Maybe Item
maybeDragItem dnd items =
    system.info dnd
        |> Maybe.andThen
            (\{ dragIndex } ->
                items
                    |> List.drop dragIndex
                    |> List.head
            )

Or you can control over generating styles for the dragged ghost element. For example adding an offset to the position:

type alias Offset =
    { x : Int
    , y : Int
    }

customGhostStyle : DnDList.Model -> DnDList.Info -> Offset -> List (Html.Attribute msg)
customGhostStyle dnd { element } offset =
    let
        px : Int -> String
        px x =
            String.fromInt x ++ "px"

        translate : Int -> Int -> String
        translate x y =
            "translate3d(" ++ px x ++ ", " ++ px y ++ ", 0)"
    in
    case system.info dnd of
        Just { currentPosition, startPosition } ->
            [ Html.Attribute.style "transform" <|
                translate
                    (round element.x + offset.x)
                    (round (currentPosition.y - startPosition.y + element.y) + offset.y)
            ]

        Nothing ->
            []

System fields

model


type Model

Represents the internal model of the current drag and drop features. It will be Nothing if there is no ongoing dragging. You should set it in your model and initialize through the System's model field.

type alias Model =
    { dnd : DnDList.Groups.Model
    , items : List Item
    }

initialModel : Model
initialModel =
    { dnd = system.model
    , items = preparedData
    }

subscriptions

subscriptions is a function to access the browser events during the dragging.

subscriptions : Model -> Sub Msg
subscriptions model =
    system.subscriptions model.dnd

commands

commands is a function to access the DOM for the drag source and the drop target as HTML elements.

update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        MyMsg msg ->
            let
                updatedModel = ...
            in
            ( updatedModel
            , system.commands updatedModel
            )

update

update is a function which returns an updated internal Model and the sorted list for your model.

update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        MyMsg msg ->
            let
                ( dnd, items ) =
                    system.update msg model.dnd model.items
            in
            ( { model | dnd = dnd, items = items }
            , system.commands dnd
            )

dragEvents

dragEvents is a function which wraps all the events up for the drag source items.

This and the following example will show us how to use auxiliary items and think about them in two different ways:

itemView : Model -> Int -> Int -> Item -> Html.Html Msg
itemView model offset localIndex { group, value, color } =
    let
        globalIndex : Int
        globalIndex =
            offset + localIndex

        itemId : String
        itemId =
            "id-" ++ String.fromInt globalIndex
    in
    case ( system.info model.dnd, maybeDragItem model.dnd model.items ) of
        ( Just _, Just _ ) ->
            -- Render when there is an ongoing dragging.

        _ ->
            if color == transparent && value == "footer" then
                Html.div
                    (Html.Attributes.id itemId
                        :: auxiliaryStyles
                    )
                    []

            else
                Html.div
                    (Html.Attributes.id itemId
                        :: itemStyles color
                        ++ system.dragEvents globalIndex itemId
                    )
                    [ Html.text value ]

dropEvents

dropEvents is a function which wraps all the events up for the drop target items.

itemView : Model -> Int -> Int -> Item -> Html.Html Msg
itemView model offset localIndex { group, value, color } =
    let
        globalIndex : Int
        globalIndex =
            offset + localIndex

        itemId : String
        itemId =
            "id-" ++ String.fromInt globalIndex
    in
    case ( system.info model.dnd, maybeDragItem model.dnd model.items ) of
        ( Just { dragIndex }, Just dragItem ) ->
            if color == transparent && value == "footer" && dragItem.group /= group then
                Html.div
                    (Html.Attributes.id itemId
                        :: auxiliaryStyles
                        ++ system.dropEvents globalIndex itemId
                    )
                    []

            else if color == transparent && value == "footer" && dragItem.group == group then
                Html.div
                    (Html.Attributes.id itemId
                        :: auxiliaryStyles
                    )
                    []

            else if dragIndex /= globalIndex then
                Html.div
                    (Html.Attributes.id itemId
                        :: itemStyles color
                        ++ system.dropEvents globalIndex itemId
                    )
                    [ Html.text value ]

            else
                Html.div
                    (Html.Attributes.id itemId
                        :: itemStyles gray
                    )
                    []

        _ ->
            -- Render when there is no dragging.

ghostStyles

ghostStyles is a function which wraps up the positioning styles of the ghost element. The ghost element has absolute position relative to the viewport.

ghostView : DnDList.Groups.Model -> List Item -> Html.Html Msg
ghostView dnd items =
    case maybeDragItem dnd items of
        Just { value, color } ->
            Html.div
                (itemStyles color ++ system.ghostStyles dnd)
                [ Html.text value ]

        Nothing ->
            Html.text ""

The following CSS will be added:

{
    position: fixed;
    left: 0;
    top: 0;
    transform: translate3d(the vector is calculated from the dragElement and the mouse position in pixels);
    height: the dragElement's height in pixels;
    width: the dragElement's width in pixels;
    pointer-events: none;
}

info

See Info.