jeongoon / elmnt-scrollpicker / Elmnt.BaseScrollPicker

This module is an implementation of picker by scrolling and basic view type is elm-ui. and animation can be done a bit tricky but easily thanks to elm-style-animation. Due to some non-standard way to hiding scrollbar, elm-css is also required.

Note: Type annotation is probably too long to see. However, it might be useful if you want add some feature with your own picker model.

Type


type alias MinimalState vt msg =
{ idString : String
, optionIds : List String
, optionIdToRecordDict : Dict String (Option vt msg)
, targetIdString : Maybe String
, pseudoAnimState : Animation.Messenger.State msg
, lastScrollClock : Time.Posix
, scrollTraceMP : Set Basics.Int
, finalTargetScrollPosMP : Basics.Int
, scrollStopCheckTime : Basics.Int 
}

Provide Minimal model (or state) to work with. most of funciton in this module works well with your own record type generally, as I used more generic type constraint in function definition

..
, pseudoAnimState : Animation.Messenger.State msg
..

Note: elm-style-animation module doesn't supply low-level functions to get intermediate states of animation so I need more research but now I'm using renderPairs function to get the states of current values in 'String' format which will be traslated into number. Even though one state value is used, I need to use Animation.style function to generate the state which can contain a lot more information


type Direction
    = Horizontal
    | Vertical

Picker direction


type StartEnd
    = Start
    | End

this type is for general use, and also used in the picker shading part from the beginning and the end.


type alias Option vt msg =
{ idString : String
, index : Basics.Int
, value : vt
, element : Element msg 
}

Option record for each item in the list from which user will choose.

This record depends on the type of value and element (Element)


type Msg vt msg
    = SyncLastScroll Time.Posix Basics.Bool
    | OnScroll
    | OnKey String
    | TriggerSnapping Time.Posix
    | CheckInitialTargetOption (List (Option vt msg)) (Option vt msg) (List (Option vt msg))
    | DetermineTargetOption (Result Error ( List (Option vt msg), Maybe ( String, Basics.Float ) ))
    | SetSnapToTargetOption String Basics.Float Basics.Float
    | MoveToTargetOption String
    | ScrollPickerSuccess (Option vt msg)
    | ScrollPickerFailure Error
    | Animate Animation.Msg
    | AnimateSnapping Basics.Int
    | NoOp

Msg chain generally covers the following steps

1. Detect any scroll which has delayed 'Cmd' to check
   whether scrolling is stopped or not.
2. If scroll stopped, trigger snapping to nearst item.
3. Find and decide which item will be target to snap.
4. Do animation related to snapping
5. Inform the snapping is done so outside world is able
   to know the which item(Option) is selected.

Unfortunately we need own Msg here which means you might need to map over those message into your own Msg type.

There are examples in this module regarding message mapping you could possibly search keyword 'messageMap' where I need to map the Msg' intomsg'

type Msg vt msg
    = SyncLastScroll            Time.Posix Bool
    | OnScroll
    | TriggerSnapping           Time.Posix
    | CheckInitialTargetOption
          (List (Option vt msg))
          -- ^ options before the sample
          (Option vt msg)
          -- ^ initial sample to check
          (List (Option vt msg))
          -- ^ options after the sample

    | DetermineTargetOption
          (Result Error (List (Option vt msg)
                              --^  other candidates
                        , Maybe ( String
                                  --^ current name of closest
                                  --  Option
                                 , Float )
                                 --^ current closest position
                                 --   of an Option
                        )
          )


    | SetSnapToTargetOption     String Float Float
                                -- ^ id, frame position,
                                --   relative pos to snap
    | MoveToTargetOption        String
    | ScrollPickerSuccess       (Option vt msg)
    | ScrollPickerFailure       Error
    | Animate                   Animation.Msg
    | AnimateSnapping           Int
    | NoOp


type Error

Error used for Task x a

XXX: this module doesn't really analyse the error status very well, but those data types are explaining the stauts in the code instead of any other types of comments.

State(picker model) Creation, Modification and Query

initMinimalState : String -> MinimalState vt msg

Helper function to initialise the minimal model. You can call setOptions after this.

    initMinimalState "myPicker"
        |> setOptions
           String.fromInt
           [ ( 1, Element.text "1" )
           , ( 2, Element.text "2" )
           ...

setOptions : (vt -> String) -> List ( vt, Element msg ) -> { state | idString : String, optionIds : List String, optionIdToRecordDict : Dict String (Option vt msg) } -> { state | idString : String, optionIds : List String, optionIdToRecordDict : Dict String (Option vt msg) }

Save options from the list of pair of ( data, Element ) option Ids are stored separately and details stored in a Dict there is no way to know how to make data value to string you should suggest the function (vt -> String)

getOptions : { state | optionIds : List String, optionIdToRecordDict : Dict String (Option vt msg) } -> List (Option vt msg)

get a list of Option record data from the whole options by searching option ID in a Dict.

The order of options in the same one of optionID list.

setScrollStopCheckTime : Basics.Int -> { state | scrollStopCheckTime : Basics.Int } -> { state | scrollStopCheckTime : Basics.Int }

Every scroll is being watched to check whether it is stopped at the moment and this function will change the timing to wait until checking.

Limitation: minimum value is 75 (ms). Animation will fail or work unexpectedly under 75 ms.

anyNewOptionSelected : Msg vt msg -> Maybe (Option vt msg)

Check the Msg, and return if there is any new selected option

please check this Example.

Update

updateWith : { a | messageMapWith : String -> Msg vt msg -> msg, pickerDirection : Direction } -> Msg vt msg -> { b | idString : String, lastScrollClock : Time.Posix, scrollTraceMP : Set Basics.Int, finalTargetScrollPosMP : Basics.Int, scrollStopCheckTime : Basics.Int, optionIdToRecordDict : Dict String (Option vt msg), optionIds : List String, pseudoAnimState : Animation.Messenger.State msg, targetIdString : Maybe String } -> ( { b | idString : String, lastScrollClock : Time.Posix, scrollTraceMP : Set Basics.Int, finalTargetScrollPosMP : Basics.Int, scrollStopCheckTime : Basics.Int, optionIdToRecordDict : Dict String (Option vt msg), optionIds : List String, pseudoAnimState : Animation.Messenger.State msg, targetIdString : Maybe String }, Platform.Cmd.Cmd msg )

updateWith function needs your own app model to ask to get messageMapWith and pickerDirection from it. So if you want to use multiple picker, you can keep the same information in the same place in benefit.

As other update function supposed to do, updateWith also does the job described in the Msg of the module.

Subscriptions

subscriptionsWith : List { state | idString : String, lastScrollClock : Time.Posix, scrollTraceMP : Set Basics.Int, finalTargetScrollPosMP : Basics.Int, scrollStopCheckTime : Basics.Int, optionIdToRecordDict : Dict String (Option vt msg), optionIds : List String, pseudoAnimState : Animation.Messenger.State msg, targetIdString : Maybe String } -> { model | messageMapWith : String -> Msg vt msg -> msg } -> Platform.Sub.Sub msg

Pass the list of the scroll states with your own application model to inform the function messageMapWith function, and you will get subscription (Sub msg).

Important: no animation will work withought subscriptions!!!

View

viewAsElement : { appModel | messageMapWith : String -> Msg vt msg -> msg, pickerDirection : Direction } -> BaseTheme { palette | accent : Color, surface : Color, background : Color, on : { paletteOn | background : Color, surface : Color }, toElmUiColor : Color -> Element.Color } msg -> { state | idString : String, optionIds : List String, optionIdToRecordDict : Dict String (Option vt msg) } -> Element msg

Generating Element with theme setting and state value each function only try to some state value in the whole record so if you can apply this funciton with additional state you might want to use.

BaseTheme DOES NOT use all the color in the Palette. the Colors used in the theme are 'accent', 'surface', 'background' 'on.background', 'on.surface'. as you can see in the long signature

This means the color listed above are should be in your own palette at least, even if you are using your own color accessor(function) with your theme.

defaultTheme : BaseTheme Internal.Palette.Palette msg

All setting values are set to Theme.Default, which can be applied to scrollPicker function.

...
exampleView model
    = let
        theme
            = defaultTheme

        picker
            = viewAsElement model theme

...


<a name="//apple_ref/cpp/Type/BaseTheme" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<div class="mono"><br /><strong> <span class="green"> type alias </span><a class="mono" name="BaseTheme" href="#BaseTheme">BaseTheme</a></strong> palette msg<span class="grey"> =</span> </div> 

    { palette : palette
    , borderWidth : Elmnt.Theme.Value Basics.Int
    , borderColorFn : Elmnt.Theme.Value (palette -> Color)
    , shadingColorFn : Elmnt.Theme.Value (palette -> Color)
    , focusColorFn : Elmnt.Theme.Value (palette -> Color)
    , backgroundColorFn : Elmnt.Theme.Value (palette -> Color)
    , fontColorFn : Elmnt.Theme.Value (palette -> Color)
    , fontSize : Elmnt.Theme.Value Basics.Int
    , shadeLength : Elmnt.Theme.Value Basics.Int
    , pickerLength : Elmnt.Theme.Value Basics.Int
    , pickerWidth : Elmnt.Theme.Value Basics.Int
    , shadeAttrsFn : Elmnt.Theme.Value (Direction -> StartEnd -> List (Element.Attribute msg)) 
    }

 An example settings value type in use here


<a name="//apple_ref/cpp/Type/BaseSettings" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<div class="mono"><br /><strong> <span class="green"> type alias </span><a class="mono" name="BaseSettings" href="#BaseSettings">BaseSettings</a></strong> compatible msg<span class="grey"> =</span> </div> 

    { lengthSetter : Element.Length -> Element.Attribute msg
    , widthSetter : Element.Length -> Element.Attribute msg
    , longitudinalContainer : List (Element.Attribute msg) -> List (Element msg) -> Element msg
    , ancherString : String
    , windowEdges : { top : Basics.Int
    , right : Basics.Int
    , bottom : Basics.Int
    , left : Basics.Int }
    , centerLateral : Element.Attribute msg
    , cssWidthSetter : Css.LengthOrAuto compatible -> Css.Style
    , cssOverFlowLongitudinal : Css.Overflow compatible -> Css.Style
    , cssOverFlowLateral : Css.Overflow compatible -> Css.Style
    , fontSize : Basics.Int
    , shadeLength : Basics.Int
    , borderWidth : Basics.Int
    , pickerLength : Basics.Int
    , pickerWidth : Basics.Int 
    }

 Settings generated from the picker [`Direction`](#Direction) for function
such as 'viewAsElement' and 'defaultShadeAttrsWith'.


# Helper functions

<a name="//apple_ref/cpp/Function/getOptionIdString" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="getOptionIdString" href="#getOptionIdString">getOptionIdString</a> <span class="grey"> :</span> </strong><span class="mono">(vt <span class="grey">-&gt;</span> String) <span class="grey">-&gt;</span> String <span class="grey">-&gt;</span> vt <span class="grey">-&gt;</span> String</span>

 make option id string value for 'Option.idString' which will be
useful if you want to access the id on the page.


# Low-level Data types and functions

<a name="//apple_ref/cpp/Function/isSnapping" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="isSnapping" href="#isSnapping">isSnapping</a> <span class="grey"> :</span> </strong><span class="mono">{ state | targetIdString : Maybe String } <span class="grey">-&gt;</span> Basics.Bool</span>


minimal testing function if the picker is snapping to some item
at the moment


<a name="//apple_ref/cpp/Function/stopSnapping" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="stopSnapping" href="#stopSnapping">stopSnapping</a> <span class="grey"> :</span> </strong><span class="mono">{ state | targetIdString : Maybe String, finalTargetScrollPosMP : Basics.Int, scrollTraceMP : Set Basics.Int, pseudoAnimState : Animation.Messenger.State msg } <span class="grey">-&gt;</span> { state | targetIdString : Maybe String, finalTargetScrollPosMP : Basics.Int, scrollTraceMP : Set Basics.Int, pseudoAnimState : Animation.Messenger.State msg }</span>


reset some states for runtime to stop snapping
which includes current target, scroll position to snap to, clock when scroll
happened etc.

**Note:** this function only try to stop snapping but asynchronous messasing
will produce more animation after calling this function, so keep in mind
that animation for snapping is not guaranteed to be done even if call this
function in `model' part.


<a name="//apple_ref/cpp/Function/unsafeSetScrollCheckTime" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="unsafeSetScrollCheckTime" href="#unsafeSetScrollCheckTime">unsafeSetScrollCheckTime</a> <span class="grey"> :</span> </strong><span class="mono">Basics.Int <span class="grey">-&gt;</span> { state | scrollStopCheckTime : Basics.Int } <span class="grey">-&gt;</span> { state | scrollStopCheckTime : Basics.Int }</span>

 You can test any value -- even under 75 ms -- however which is not recommended


<a name="//apple_ref/cpp/Function/defaultShadeLengthWith" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="defaultShadeLengthWith" href="#defaultShadeLengthWith">defaultShadeLengthWith</a> <span class="grey"> :</span> </strong><span class="mono">Direction <span class="grey">-&gt;</span> Basics.Int</span>

 Takes the direction of picker and gives the shade length


<a name="//apple_ref/cpp/Function/defaultShadeAttrsWith" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="defaultShadeAttrsWith" href="#defaultShadeAttrsWith">defaultShadeAttrsWith</a> <span class="grey"> :</span> </strong><span class="mono">BaseTheme { palette | accent : Color, surface : Color, background : Color, on : { paletteOn | background : Color, surface : Color }, toElmUiColor : Color <span class="grey">-&gt;</span> Element.Color } msg <span class="grey">-&gt;</span> Direction <span class="grey">-&gt;</span> StartEnd <span class="grey">-&gt;</span> List (Element.Attribute msg)</span>

 and helper function for shade elm-ui attributes (List Element.Attribute)


<a name="//apple_ref/cpp/Function/defaultBaseSettingsWith" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="defaultBaseSettingsWith" href="#defaultBaseSettingsWith">defaultBaseSettingsWith</a> <span class="grey"> :</span> </strong><span class="mono">Direction <span class="grey">-&gt;</span> { theme | fontSize : Elmnt.Theme.Value Basics.Int, borderWidth : Elmnt.Theme.Value Basics.Int, shadeLength : Elmnt.Theme.Value Basics.Int, pickerLength : Elmnt.Theme.Value Basics.Int, pickerWidth : Elmnt.Theme.Value Basics.Int } <span class="grey">-&gt;</span> BaseSettings compatible msg</span>

 Generate setting values for a picker which has `Direction`


<a name="//apple_ref/cpp/Type/Geom" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<div class="mono"><br /><strong> <span class="green"> type alias </span><a class="mono" name="Geom" href="#Geom">Geom</a></strong><span class="grey"> =</span> </div> 

    { x : Basics.Float
    , y : Basics.Float
    , width : Basics.Float
    , height : Basics.Float 
    }

 geometry data type which can be seen in 'Browser.Dom.Viewport'


<a name="//apple_ref/cpp/Function/getCenterPosOf" class="dashAnchor"></a>

<div style="padding: 0px; margin: 0px; height: 1px; background-color: rgb(216, 221, 225);"></div>

<strong> <a class="mono" name="getCenterPosOf" href="#getCenterPosOf">getCenterPosOf</a> <span class="grey"> :</span> </strong><span class="mono">(Geom <span class="grey">-&gt;</span> Basics.Float) <span class="grey">-&gt;</span> (Geom <span class="grey">-&gt;</span> Basics.Float) <span class="grey">-&gt;</span> (rec <span class="grey">-&gt;</span> Geom) <span class="grey">-&gt;</span> rec <span class="grey">-&gt;</span> Basics.Float</span>

 [`Browser.Dom.Viewport`](/packages/elm/browser/latest/Browser-Dom#Viewport), [`Browser.Dom.Element`](/packages/elm/browser/latest/Browser-Dom#Element) share basic record accessor
like `.x`  `.y`  `.width`  `.height`

getCenterPosOf function try to get center poisition of the some field.

ex) to get center 'y' position of viewport, you can try

```elm
getCenterPosOf .y .height .viewport aRecord

partitionOptionsHelper : (Geom -> Basics.Float) -> (Geom -> Basics.Float) -> { state | optionIdToRecordDict : Dict String (Option vt msg), optionIds : List String } -> { vp | scene : { d | height : Basics.Float, width : Basics.Float }, viewport : Geom } -> OptionPartition vt msg

when all the items are distributed uniformly, it might be easier to get the option to focus(for snapping). partitionOptions will provide one candidate to snap and previous options prior to current one and next options as well. Calulating relative position in Viewport probably be only way to test whether the target is correct one or not, however you might need to check around current candidate.

getRelPosOfElement : (Geom -> Basics.Float) -> (Geom -> Basics.Float) -> { pos | element : Geom, viewport : Geom } -> Basics.Float

To get relative position of element in the viewport.

you need to apply position accessor and length accessor which are normally .x and .width for Horizontal scroll picker and .y and .height for Vertical scroll picker.

Note: the element is got from getElement, viewport is got from getViewport

taskTargetOptionRelPosHelper : (Geom -> Basics.Float) -> (Geom -> Basics.Float) -> String -> String -> Task Error Basics.Float

A Task helper function to get relative distance of the item from frame which is measured from the center position of each other. This value has sign -- negtative value shows that the item is left or above the centre of view frame

taskGetViewport : String -> Task Error Browser.Dom.Viewport

Task helper to get viewport of the item id.

taskGetViewportPosHelper : (Geom -> Basics.Float) -> String -> Task Error Basics.Float

Task helper to retreive the position. posAccessor

toMilliPixel : Basics.Float -> Basics.Int

An utility which converts an floating value to an integer value which contains upto milli of base unit (pixel in this case)

fromMilliPixel : Basics.Int -> Basics.Float

An utility which converts an integer value(which contains up to thousandth value of original) to an float value.

subscriptionsWithHelper : (String -> Animation.Msg -> msg) -> List { a | idString : String, pseudoAnimState : Animation.Messenger.State msg } -> Platform.Sub.Sub msg

FIXME

- add keyboard input support