├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json ├── img └── looping.gif └── src ├── Styles.elm └── elm-sortable-list.elm /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Wouter In 't Velt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Elm Sortable List** 2 | 3 | Example app demonstrating a list with 4 | drag and drop reordering. 5 | 6 | ![gif](/img/looping.gif) 7 | 8 | 9 | # Get started 10 | - Make sure you have elm 0.18 installed 11 | - The example also needs the Mouse package 12 | - Copy or clone the 2 files from the /src folder 13 | - You can run elm-sortable-table.elm in elm-reactor 14 | 15 | 16 | ## Summary 17 | The example uses subscriptions to `Mouse.moves` and `Mouse.ups` 18 | to track the drag movement in the vertical axis. 19 | 20 | The basic drag function is an adaption from the drag example on 21 | http://elm-lang.org/examples/drag 22 | 23 | Combined with styles for animation, 24 | and a helper function to move the dragged item to the new position. 25 | 26 | For readability of the main app, the styles have been placed in 27 | a separate Styles.elm module. 28 | 29 | 30 | ## How it works 31 | 32 | ### Model 33 | The app's main model keeps track of: 34 | - Indicate whether app is in reorderable state (top-right button) `isReordering : Bool` 35 | - List of `data : List String` 36 | - Possible drag tracking `drag : Maybe Drag` 37 | 38 | The `Drag` is a record containing: 39 | - index of the item being moved 40 | - startY position of the mouse 41 | - currentY position of the mouse 42 | 43 | The difference between currentY and startY is used to move the item while dragging. 44 | 45 | 46 | ### Update 47 | The app's update function handles the following message types: 48 | - `ToggleReorder` - to turn reordering state on or off 49 | - `DragStart Int Position` - start of drag, with index of item clicked + mouse position 50 | - `DragAt Position` - a mouse movement with new mouse position 51 | - `DragEnd Position` - a mouse up event to drop the item 52 | 53 | The `DragStart` message is generated by a (custom) `onMouseDown` event on an item from the list. 54 | The `DragAt` and `DragEnd` messages are both generated by a subscription. 55 | The `DragEnd` message calls a helper function, that moves the item to its new location in the list. 56 | 57 | 58 | ### Calculating the move 59 | Since the **items have a known height of 50px**, 60 | the calculation of the offset (how many items to move up or down) 61 | is relatively straightforward. 62 | The offset is based on pixels of drag that took place (currentY - startY), divided by 50. 63 | Some correction is done to round up or down if dragged for about half an item. 64 | 65 | 66 | ### View 67 | Once an item is in drag mode - the view derives this from the drag state in the model - 68 | a styles are added to lift it visually, and put it in front of all other items in the list. 69 | 70 | The currentY and startY are used to determine the position of the dragged item. 71 | This is translated to a `transform: translateY()` style on the item for display. 72 | 73 | For all the other items in the list, the view checks whether the item being dragged is 74 | above or passed it, to make room for the dragged item by shifting up or down. 75 | With styles, a transition is added, so the items slide smoothly beneath the dragged item. 76 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Example demonstrating drag and drop reordering of a list", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /img/looping.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintvelt/elm-sortable-list/5fd64282f313d69aef88e7e197becdcdb0be20e3/img/looping.gif -------------------------------------------------------------------------------- /src/Styles.elm: -------------------------------------------------------------------------------- 1 | module Styles exposing (..) 2 | {-| Static styles used in elm-sortable-table 3 | -} 4 | 5 | type alias StyleList = List (String, String) 6 | 7 | 8 | -- for page container (root element) 9 | pageContainer : StyleList 10 | pageContainer = 11 | [ ("width","360px") 12 | , ("margin","auto") 13 | , ("padding","0 0 8px 0") 14 | , ("backgroundColor","#fafafa") 15 | , ("fontFamily","sans-serif") 16 | ] 17 | 18 | -- for list header (with title and toggle button) 19 | listHeader : StyleList 20 | listHeader = 21 | [("display","flex") 22 | ,("padding","8px") 23 | ,("margin","8px 0") 24 | ] 25 | 26 | -- for title in header 27 | headerTitle : StyleList 28 | headerTitle = 29 | [ ("flex","1 0 auto") 30 | , ("margin","0") 31 | ] 32 | 33 | -- for list container (ul) 34 | listContainer : StyleList 35 | listContainer = 36 | [ ("transformStyle","preserve-3d") 37 | , ("padding","0") 38 | , ("margin","8px 0") 39 | ] 40 | 41 | -- for list item (li) 42 | listItem : StyleList 43 | listItem = 44 | [ ("listStyleType","none") 45 | , ("margin","8px") 46 | , ("padding","8px") 47 | , ("height","24px") 48 | , ("backgroundColor", "white") 49 | , ("border","1px solid rgba(0,0,0,.27)") 50 | , ("border-radius","2px") 51 | , ("box-shadow", "0 1px 2px rgba(0,0,0,0.24)") 52 | , ("display","flex") 53 | ] 54 | 55 | -- for text in list item container 56 | itemText : StyleList 57 | itemText = 58 | [ ("flex","1 0 auto") 59 | , ("display","inline-block") 60 | ] -------------------------------------------------------------------------------- /src/elm-sortable-list.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | {-| An example of a sortable list using drag and drop 4 | See the README.md file for more information 5 | -} 6 | 7 | import Html exposing (..) 8 | import Html.Attributes exposing (..) 9 | import Html.Events exposing (onClick, on) 10 | import Json.Decode as Decode 11 | import Mouse exposing (Position) 12 | import Styles 13 | 14 | 15 | main = 16 | Html.program 17 | { init = init 18 | , view = view 19 | , update = update 20 | , subscriptions = subscriptions 21 | } 22 | 23 | 24 | 25 | -- MODEL 26 | 27 | 28 | type alias Model = 29 | { isReordering : Bool 30 | , data : List String 31 | , drag : Maybe Drag 32 | } 33 | 34 | 35 | init : ( Model, Cmd Msg ) 36 | init = 37 | { isReordering = False 38 | , data = initialList |> List.sort 39 | , drag = Nothing 40 | } 41 | ! [] 42 | 43 | 44 | type alias Drag = 45 | { itemIndex : Int 46 | , startY : Int 47 | , currentY : Int 48 | } 49 | 50 | 51 | initialList = 52 | [ "Shawshank Redemption" 53 | , "Godfather" 54 | , "Dark Knight" 55 | , "12 Angry Men" 56 | , "Schindler’s List" 57 | , "Pulp Fiction" 58 | , "Lord of the Rings" 59 | , "The Good, the Bad and the Ugly" 60 | , "Fight Club" 61 | , "The Empire Strikes Back" 62 | ] 63 | 64 | 65 | 66 | -- UPDATE 67 | 68 | 69 | type Msg 70 | = ToggleReorder 71 | | DragStart Int Position 72 | | DragAt Position 73 | | DragEnd Position 74 | 75 | 76 | update : Msg -> Model -> ( Model, Cmd Msg ) 77 | update msg model = 78 | case msg of 79 | ToggleReorder -> 80 | { model | isReordering = not model.isReordering } ! [] 81 | 82 | DragStart idx pos -> 83 | { model 84 | | drag = Just <| Drag idx pos.y pos.y 85 | } 86 | ! [] 87 | 88 | DragAt pos -> 89 | { model 90 | | drag = 91 | Maybe.map (\{ itemIndex, startY } -> Drag itemIndex startY pos.y) model.drag 92 | } 93 | ! [] 94 | 95 | DragEnd pos -> 96 | case model.drag of 97 | Just { itemIndex, startY, currentY } -> 98 | { model 99 | | data = 100 | moveItem 101 | itemIndex 102 | ((currentY - startY 103 | + if currentY < startY then 104 | -20 105 | else 106 | 20 107 | ) 108 | // 50 109 | ) 110 | model.data 111 | , drag = Nothing 112 | } 113 | ! [] 114 | 115 | Nothing -> 116 | { model 117 | | drag = Nothing 118 | } 119 | ! [] 120 | 121 | 122 | moveItem : Int -> Int -> List a -> List a 123 | moveItem fromPos offset list = 124 | let 125 | listWithoutMoved = 126 | List.take fromPos list ++ List.drop (fromPos + 1) list 127 | 128 | moved = 129 | List.take 1 <| List.drop fromPos list 130 | in 131 | List.take (fromPos + offset) listWithoutMoved 132 | ++ moved 133 | ++ List.drop (fromPos + offset) listWithoutMoved 134 | 135 | 136 | 137 | -- SUBSCRIPTIONS 138 | 139 | 140 | subscriptions : Model -> Sub Msg 141 | subscriptions model = 142 | case model.drag of 143 | Nothing -> 144 | Sub.none 145 | 146 | Just _ -> 147 | Sub.batch [ Mouse.moves DragAt, Mouse.ups DragEnd ] 148 | 149 | 150 | 151 | -- VIEW 152 | 153 | 154 | view : Model -> Html Msg 155 | view model = 156 | div 157 | [ style Styles.pageContainer ] 158 | [ div 159 | [ style Styles.listHeader ] 160 | [ h3 161 | [ style Styles.headerTitle ] 162 | [ text "Sortable favorite movies" ] 163 | , toggleButton model 164 | ] 165 | , ul 166 | [ style Styles.listContainer ] 167 | <| 168 | List.indexedMap (itemView model) model.data 169 | ] 170 | 171 | 172 | toggleButton : Model -> Html Msg 173 | toggleButton model = 174 | let 175 | buttonTxt = 176 | if model.isReordering then 177 | "Reordering" 178 | else 179 | "Click to reorder" 180 | in 181 | button [ onClick ToggleReorder ] [ text buttonTxt ] 182 | 183 | 184 | itemView : Model -> Int -> String -> Html Msg 185 | itemView model idx item = 186 | let 187 | buttonStyle = 188 | if model.isReordering then 189 | [ ( "display", "inline-block" ) ] 190 | else 191 | [ ( "display", "none" ) ] 192 | 193 | moveStyle = 194 | case model.drag of 195 | Just { itemIndex, startY, currentY } -> 196 | if itemIndex == idx then 197 | [ ( "transform", "translateY( " ++ toString (currentY - startY) ++ "px) translateZ(10px)" ) 198 | , ( "box-shadow", "0 3px 6px rgba(0,0,0,0.24)" ) 199 | , ( "willChange", "transform" ) 200 | ] 201 | else 202 | [] 203 | 204 | Nothing -> 205 | [] 206 | 207 | makingWayStyle = 208 | case model.drag of 209 | Just { itemIndex, startY, currentY } -> 210 | if (idx < itemIndex) && (currentY - startY) < (idx - itemIndex) * 50 + 20 then 211 | [ ( "transform", "translateY(50px)" ) 212 | , ( "transition", "transform 200ms ease-in-out" ) 213 | ] 214 | else if (idx > itemIndex) && (currentY - startY) > (idx - itemIndex) * 50 - 20 then 215 | [ ( "transform", "translateY(-50px)" ) 216 | , ( "transition", "transform 200ms ease-in-out" ) 217 | ] 218 | else if idx /= itemIndex then 219 | [ ( "transition", "transform 200ms ease-in-out" ) ] 220 | else 221 | [] 222 | 223 | Nothing -> 224 | [] 225 | in 226 | li [ style <| Styles.listItem ++ moveStyle ++ makingWayStyle ] 227 | [ div [ style Styles.itemText ] [ text item ] 228 | , button 229 | [ style buttonStyle, onMouseDown <| DragStart idx ] 230 | [ text "drag" ] 231 | ] 232 | 233 | 234 | onMouseDown : (Position -> msg) -> Attribute msg 235 | onMouseDown msg = 236 | on "mousedown" (Decode.map msg Mouse.position) 237 | --------------------------------------------------------------------------------