Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ by default. If you want more control over the scrolling however, you can create
* offset: The amount of space in pixels between the element to scroll to and the top of the viewport that is to remain after scrolling. Defaults to 12.
* speed: The speed with which the scrolling is to take place. Experiment to find the value that works for you. 100 is the default.
* easing: The easing function to use. Check out the [easing functions](https://package.elm-lang.org/packages/elm-community/easing-functions/latest/) package for more information. `Ease.outQuint` is the default.
* container: Which element to scroll inside of. Defaults to the document body, but can be configured with `containerElement`.

You can then pass this config record to the `scrollToWithOptions` function.

Expand Down
94 changes: 94 additions & 0 deletions example/src/ExampleInner.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module ExampleInner exposing (main)

import Browser exposing (Document)
import Browser.Dom as Dom
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import SmoothScroll exposing (containerElement, defaultConfig, scrollToWithOptions)
import Task exposing (Task)


main =
Browser.document
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}


init : () -> ( Model, Cmd Msg )
init _ =
( { foo = "bar" }, Cmd.none )


type alias Model =
{ foo : String }


type Msg
= NoOp
| SmoothScroll String String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )

SmoothScroll containerId targetId ->
( model
, targetId
|> scrollToWithOptions { defaultConfig | speed = 15, container = containerElement containerId }
|> Task.attempt (always NoOp)
)


view : Model -> Document Msg
view model =
{ title = "Scroll Inside Elements"
, body =
[ div topBarStyles
[ div []
[ button [ onClick (SmoothScroll "left-half" "anchor-left-100") ] [ text "100" ]
, button [ onClick (SmoothScroll "left-half" "anchor-left-25") ] [ text "25" ]
, button [ onClick (SmoothScroll "left-half" "anchor-left-1") ] [ text "1" ]
]
, div []
[ button [ onClick (SmoothScroll "right-half" "anchor-right-100") ] [ text "100" ]
, button [ onClick (SmoothScroll "right-half" "anchor-right-25") ] [ text "25" ]
, button [ onClick (SmoothScroll "right-half" "anchor-right-1") ] [ text "1" ]
]
]
, div rowStyles
[ div ([ id "left-half", style "background" "cornflowerblue" ] ++ columnStyles)
[ ul []
(List.range 1 100 |> List.map (anchorView "left"))
]
, div ([ id "right-half", style "background" "wheat" ] ++ columnStyles)
[ ul []
(List.range 1 100 |> List.map (anchorView "right"))
]
]
]
}


anchorView side num =
li [ String.fromInt num |> String.append ("anchor-" ++ side ++ "-") |> id ]
[ text <| String.fromInt num ]


topBarStyles =
[ style "position" "fixed", style "top" "0", style "height" "40px", style "left" "0", style "right" "0", style "display" "flex", style "justify-content" "space-around" ]


rowStyles =
[ style "font-size" "18px", style "font-family" "sans-serif", style "position" "fixed", style "top" "40px", style "bottom" "0", style "left" "0", style "right" "0", style "display" "flex" ]


columnStyles =
[ style "flex-grow" "1", style "overflow-y" "scroll", style "border" "1px solid #ccc" ]
26 changes: 26 additions & 0 deletions example/src/elm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"type": "application",
"source-directories": [
".",
"../../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm-community/easing-functions": "2.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
76 changes: 66 additions & 10 deletions src/SmoothScroll.elm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module SmoothScroll exposing
( Config
, defaultConfig
, containerElement
, scrollTo
, scrollToWithOptions
)
Expand All @@ -12,6 +13,7 @@ module SmoothScroll exposing

@docs Config
@docs defaultConfig
@docs containerElement


# Scrolling
Expand All @@ -27,29 +29,40 @@ import Internal.SmoothScroll exposing (animationSteps)
import Task exposing (Task)


{-| Configuration options for smooth scrolling. Has three options:
{-| Configuration options for smooth scrolling. Has four options:

- offset: The amount of space in pixels between the element to scroll to and the top of the viewport that is to remain after scrolling
- speed: The higher this number, the faster the scrolling!
- easing: The easing function to use. Check out the [easing functions](https://package.elm-lang.org/packages/elm-community/easing-functions/latest/) package for more information.
- container: Which element to scroll inside of. Defaults to the document body, but can be configured with [`containerElement`](#containerElement)

-}
type alias Config =
{ offset : Int
, speed : Int
, easing : Ease.Easing
, container : Container
}


{-|
{-| An internal type for configuring which element to scroll within.
-}
type Container
= DocumentBody
| InnerNode String


import SmoothScroll
{-| The default configuration which can be modified

import Ease
import SmoothScroll exposing (defaultConfig)

defaultConfig : Config
defaultConfig =
{ offset = 12
, speed = 200
, easing = Ease.outQuint
, container = DocumentBody
}

-}
Expand All @@ -58,12 +71,25 @@ defaultConfig =
{ offset = 12
, speed = 200
, easing = Ease.outQuint
, container = DocumentBody
}


{-| Configure which DOM node to scroll inside of

import SmoothScroll exposing (scrollToWithOptions, defaultConfig, containerElement)

scrollToWithOptions { defaultConfig | container = containerElement "article-list" } "article-42"

-}
containerElement : String -> Container
containerElement elementId =
InnerNode elementId


{-| Scroll to the element with the given id, using the default configuration

import SmoothScroll
import SmoothScroll exposing (scrollTo)

scrollTo "article"

Expand All @@ -75,18 +101,48 @@ scrollTo =

{-| Scroll to the element with the given id, using a custom configuration

import SmoothScroll exposing (defaultConfig)
import SmoothScroll exposing (defaultConfig, scrollToWithOptions)

scrollToWithOptions { defaultConfig | offset = 60 } "article"

-}
scrollToWithOptions : Config -> String -> Task Dom.Error (List ())
scrollToWithOptions config id =
let
tasks from to =
List.map (Dom.setViewport 0)
(animationSteps config.speed config.easing from (to - toFloat config.offset))
( getViewport, setViewport ) =
case config.container of
DocumentBody ->
( Dom.getViewport, Dom.setViewport 0 )

InnerNode containerId ->
( Dom.getViewportOf containerId, Dom.setViewportOf containerId 0 )

getContainerInfo =
case config.container of
DocumentBody ->
Task.succeed Nothing

InnerNode containerId ->
Task.map Just (Dom.getElement containerId)

scrollTask { scene, viewport } { element } container =
let
destination =
case container of
Nothing ->
element.y - toFloat config.offset

Just containerInfo ->
viewport.y + element.y - toFloat config.offset - containerInfo.element.y

clamped =
destination
|> min (scene.height - viewport.height)
|> max 0
in
animationSteps config.speed config.easing viewport.y clamped
|> List.map setViewport
|> Task.sequence
in
Task.map2 Tuple.pair Dom.getViewport (Dom.getElement id)
|> Task.andThen (\( { viewport }, { element } ) -> tasks viewport.y element.y)
Task.map3 scrollTask getViewport (Dom.getElement id) getContainerInfo
|> Task.andThen identity