diff --git a/README.md b/README.md index 53d9889..81fa5d3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/example/src/ExampleInner.elm b/example/src/ExampleInner.elm new file mode 100644 index 0000000..502d1b8 --- /dev/null +++ b/example/src/ExampleInner.elm @@ -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" ] diff --git a/example/src/elm.json b/example/src/elm.json new file mode 100644 index 0000000..b94b7ff --- /dev/null +++ b/example/src/elm.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/SmoothScroll.elm b/src/SmoothScroll.elm index b2d8eb6..b8a29ba 100644 --- a/src/SmoothScroll.elm +++ b/src/SmoothScroll.elm @@ -1,6 +1,7 @@ module SmoothScroll exposing ( Config , defaultConfig + , containerElement , scrollTo , scrollToWithOptions ) @@ -12,6 +13,7 @@ module SmoothScroll exposing @docs Config @docs defaultConfig +@docs containerElement # Scrolling @@ -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 } -} @@ -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" @@ -75,7 +101,7 @@ 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" @@ -83,10 +109,40 @@ scrollTo = 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