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:
type alias DragIndex =
Int
type alias DropIndex =
Int
type alias DragElementId =
String
type alias DropElementId =
String
type alias Position =
{ x : Float
, y : Float
}
{ 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:
the internal model, subscriptions, commands, and update,
the bindable events and styles, and
the Info
object.
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
Internal message type. It should be wrapped within our message constructor:
type Msg
= MyMsg DnDList.Groups.Msg
{ 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.
beforeUpdate
: This is a hook and gives you access to your list before it will be sorted.
The first number is the drag index, the second number is the drop index.
The Towers of Hanoi uses this hook to update the disks' tower
property.
listen
: This setting is for the operation performing on the same group.
The items can listen for drag events or for drop events.
In the first case the list will be sorted again and again while the mouse moves over the different drop target items.
In the second case the list will be sorted only once on that drop target where the mouse was finally released.
operation
: This setting is for the operation performing on the same group.
Different kinds of sort operations can be performed on the list.
You can start to analyze them with
sorting on drag
and sorting on drop.
groups
: This setting is for the operation performing on different groups,
when the drag source and the drop target belong to different groups.
To have a better understanding of how this works
see sorting between groups on drag
and sorting between groups on drop.
listen
: Same as the plain listen
but applied when transferring items between groups.operation
: Same as the plain operation
but applied when transferring items between groups.comparator
: You should provide this function, which determines if two items are from different groups.setter
: You should provide this function, which updates the second item's group by the first item's group.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 }
Represents the event for which the list sorting is available.
OnDrag
: The list will be sorted when the ghost element is being dragged over a drop target item.
OnDrop
: The list will be sorted when the ghost element is dropped on a drop target item.
Represents the list sort operation. Detailed comparisons can be found here: sorting on drag and sorting on drop.
InsertAfter
: The drag source item will be inserted after the drop target item.
InsertBefore
: The drag source item will be inserted before the drop target item.
Rotate
: The items between the drag source and the drop target items will be circularly shifted.
Swap
: The drag source and the drop target items will be swapped.
Unaltered
: The list items will keep their initial order.
{ 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.
dragIndex
: The index of the drag source.
dropIndex
: The index of the drop target.
dragElementId
: HTML id of the drag source.
dropElementId
: HTML id of the drop target.
dragElement
: Information about the drag source as an HTML element, see Browser.Dom.Element
.
dropElement
: Information about the drop target as an HTML element, see Browser.Dom.Element
.
startPosition
: The x, y position of the ghost element when dragging started.
currentPosition
: The x, y position of the ghost element now.
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 ->
[]
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
is a function to access the browser events during the dragging.
subscriptions : Model -> Sub Msg
subscriptions model =
system.subscriptions model.dnd
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
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
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
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
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;
}
See Info.