This is an alternative site for discovering Elm packages. You may be looking for the official Elm package site instead.

Bootstrap.Popover

Add small overlay content, like those found in iOS, to any element for housing secondary information.

type alias Model =
    { popoverState : Popover.State }


-- Define a message to handle popover state changes

type Msg
    = PopoverMsg Popover.State


-- Initialize the popover state

initialState : ( Model, Cmd Msg )
initialState =
    ( { popoverState = Popover.initialState }, Cmd.none )


-- Step the popover state forward in your update function

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PopoverMsg state ->
            ( { model | popoverState = state }, Cmd.none )


-- Compose a popover in your view (or a view helper function)

view : Model -> Html Msg
view model =
    Popover.config
        (Button.button
            -- Here configure the popover to be shown when the mouse is above the button ( tooltip basically !)
            [ Button.attrs <| Popover.onHover model.popoverState PopoverMsg ]
            [ text "Toggle tooltip" ]
        )
        |> Popover.right
        |> Popover.titleH4 [] [ text "My title" ]
        |> Popover.content []
            [ text "Some content for my popover."
            , p [] [ text "Different elements ok..." ]
            ]
        |> Popover.view model.popoverState

You should be aware that the triggering element is wrapped by an inline-block div with relative positioning and that the popover is added as a sibling of the triggering element. This will limit it's usage and there are bound to be cases where they don't work as you'd expect. So make sure you test your views when using them !

Setup

config : Html.Html msg -> Config msg

Creates a default view config for a popover

  • triggerElement - The element that will trigger the popover
initialState : State

Initial default view state.

view : State -> Config msg -> Html.Html msg

This function creates the view representation for a Popover. Whether it's displayed or not is determined by it's view state.

  • state - The current view state for the popover
  • config - The view configuration for the popover
type Config msg = Config { triggerElement : Html.Html msg , direction : Position , title : Maybe (Title msg) , content : Maybe (Content msg) }

Opaque representation of the view configuration for a Popover

type State = State { isActive : Bool , domState : DOMState }

Opaque representation of the view state for a Popover

Triggering

onClick : State -> (State -> msg) -> List (Html.Attribute msg)

Creates a click handler that will toggle the visibility of a popover

  • state - The current state of the popover to toggle
  • toMsg - Message tagger function to handle state changes to a popover
onHover : State -> (State -> msg) -> List (Html.Attribute msg)

Creates a mouseenter and mouseleave message handler that will toggle the visibility of a popover

  • state - The current state of the popover to toggle
  • toMsg - Message tagger function to handle state changes to a popover

View composition

title : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover title.

  • attributes - List of attributes
  • children - List of child elements
content : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define the popover body content.

titleH1 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h1 title.

  • attributes - List of attributes
  • children - List of child elements
titleH2 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h2 title.

  • attributes - List of attributes
  • children - List of child elements
titleH3 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h3 title.

  • attributes - List of attributes
  • children - List of child elements
titleH4 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h4 title.

  • attributes - List of attributes
  • children - List of child elements
titleH5 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h5 title.

  • attributes - List of attributes
  • children - List of child elements
titleH6 : List (Html.Attribute msg) -> List (Html.Html msg) -> Config msg -> Config msg

Define a popover h6 title.

  • attributes - List of attributes
  • children - List of child elements

Positioning

left : Config msg -> Config msg

Show popover to the left of the triggering element.

right : Config msg -> Config msg

Show popover to the right of the triggering element.

top : Config msg -> Config msg

Show popover above the triggering element.

bottom : Config msg -> Config msg

Show popover below the triggering element.

module Bootstrap.Popover
    exposing
        ( view
        , onClick
        , onHover
        , config
        , initialState
        , content
        , left
        , right
        , top
        , bottom
        , title
        , titleH1
        , titleH2
        , titleH3
        , titleH4
        , titleH5
        , titleH6
        , Config
        , State
        )

{-| Add small overlay content, like those found in iOS, to any element for housing secondary information.

    type alias Model =
        { popoverState : Popover.State }


    -- Define a message to handle popover state changes

    type Msg
        = PopoverMsg Popover.State


    -- Initialize the popover state

    initialState : ( Model, Cmd Msg )
    initialState =
        ( { popoverState = Popover.initialState }, Cmd.none )


    -- Step the popover state forward in your update function

    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
        case msg of
            PopoverMsg state ->
                ( { model | popoverState = state }, Cmd.none )


    -- Compose a popover in your view (or a view helper function)

    view : Model -> Html Msg
    view model =
        Popover.config
            (Button.button
                -- Here configure the popover to be shown when the mouse is above the button ( tooltip basically !)
                [ Button.attrs <| Popover.onHover model.popoverState PopoverMsg ]
                [ text "Toggle tooltip" ]
            )
            |> Popover.right
            |> Popover.titleH4 [] [ text "My title" ]
            |> Popover.content []
                [ text "Some content for my popover."
                , p [] [ text "Different elements ok..." ]
                ]
            |> Popover.view model.popoverState

*You should be aware that the triggering element is wrapped by an `inline-block` div with relative positioning and that
the popover is added as a sibling of the triggering element. This will limit it's usage and there are bound to be
cases where they don't work as you'd expect. So make sure you test your views when using them !*


# Setup

@docs config, initialState, view, Config, State


# Triggering

@docs onClick, onHover


# View composition

@docs title, content, titleH1, titleH2, titleH3, titleH4, titleH5, titleH6


# Positioning

@docs left, right, top, bottom

-}

import Html.Styled as Html
import Html.Styled.Attributes exposing (class, classList, style, attribute)
import Html.Styled.Events
import Json.Decode as Json
import DOM


{-| Opaque representation of the view configuration for a Popover
-}
type Config msg
    = Config
        { triggerElement : Html.Html msg
        , direction : Position
        , title : Maybe (Title msg)
        , content : Maybe (Content msg)
        }


{-| Opaque representation of the view state for a Popover
-}
type State
    = State
        { isActive : Bool
        , domState : DOMState
        }


type alias DOMState =
    { rect : DOM.Rectangle
    , offsetWidth : Float
    , offsetHeight : Float
    }


type Position
    = Top
    | Right
    | Bottom
    | Left


type Title msg
    = Title (Html.Html msg)


type Content msg
    = Content (Html.Html msg)


type alias Pos =
    { left : Float
    , top : Float
    , arrowTop : Maybe Float
    , arrowLeft : Maybe Float
    }


{-| Initial default view state.
-}
initialState : State
initialState =
    State
        { isActive = False
        , domState =
            { rect = { left = 0, top = 0, width = 0, height = 0 }
            , offsetWidth = 0
            , offsetHeight = 0
            }
        }


{-| This function creates the view representation for a Popover. Whether it's displayed or not
is determined by it's view state.

  - `state` - The current view state for the popover
  - `config` - The view configuration for the popover

-}
view : State -> Config msg -> Html.Html msg
view state ((Config { triggerElement }) as config) =
    Html.div
        [ style
            [ ( "position", "relative" )
            , ( "display", "inline-block" )
            ]
        ]
        [ triggerElement
        , popoverView state config
        ]


popoverView : State -> Config msg -> Html.Html msg
popoverView (State { isActive, domState }) (Config config) =
    let
        px f =
            (toString f) ++ "px"

        pos =
            calculatePos config.direction domState

        styles =
            if isActive then
                [ ( "left", px pos.left )
                , ( "top", px pos.top )
                , ( "display", "inline-block" )
                , ( "width", px domState.offsetWidth )
                ]
            else
                [ ( "left", "-5000px" )
                , ( "top", "-5000px" )
                ]

        arrowStyles =
            [ Maybe.map (\t -> ( "top", px t )) pos.arrowTop
            , Maybe.map (\l -> ( "left", px l )) pos.arrowLeft
            ]
                |> List.filterMap identity
    in
        Html.div
            [ classList
                [ ( "popover", True )
                , ( "fade", True )
                , ( "show", isActive )
                , positionClass config.direction
                ]
            , style styles
            , directionAttr config.direction
            ]
            ([ Just <| Html.div [ class "arrow", style arrowStyles ] []
             , Maybe.map (\(Title t) -> t) config.title
             , Maybe.map (\(Content c) -> c) config.content
             ]
                |> List.filterMap identity
            )


directionAttr : Position -> Html.Attribute msg
directionAttr position =
    attribute "x-placement" <|
        case position of
            Left ->
                "left"

            Right ->
                "right"

            Top ->
                "top"

            Bottom ->
                "bottom"


positionClass : Position -> ( String, Bool )
positionClass position =
    case position of
        Left ->
            ( "bs-popover-left", True )

        Right ->
            ( "bs-popover-right", True )

        Top ->
            ( "bs-popover-top", True )

        Bottom ->
            ( "bs-popover-bottom", True )


{-| Creates a click handler that will toggle the visibility of
a popover

  - `state` - The current state of the popover to toggle
  - `toMsg` - Message tagger function to handle state changes to a popover

-}
onClick : State -> (State -> msg) -> List (Html.Attribute msg)
onClick state toMsg =
    [ class "popover-trigger"
    , Html.Styled.Events.on "click" <| toggleState state toMsg
    ]


{-| Creates a `mouseenter` and `mouseleave` message handler that will toggle the visibility of
a popover

  - `state` - The current state of the popover to toggle
  - `toMsg` - Message tagger function to handle state changes to a popover

-}
onHover : State -> (State -> msg) -> List (Html.Attribute msg)
onHover state toMsg =
    [ class "popover-trigger"
    , Html.Styled.Events.on "mouseenter" <| toggleState state toMsg
    , Html.Styled.Events.on "mouseleave" <| forceClose state toMsg
    ]


forceClose : State -> (State -> a) -> Json.Decoder a
forceClose (State state) toMsg =
    Json.succeed <|
        toMsg <|
            State { state | isActive = False }


toggleState : State -> (State -> msg) -> Json.Decoder msg
toggleState (State ({ isActive } as state)) toMsg =
    stateDecoder
        |> Json.andThen
            (\v ->
                Json.succeed <|
                    toMsg <|
                        if not isActive then
                            State
                                { isActive = True
                                , domState = v
                                }
                        else
                            State { state | isActive = False }
            )


{-| Creates a default view config for a popover

  - `triggerElement` - The element that will trigger the popover

-}
config : Html.Html msg -> Config msg
config triggerElement =
    Config
        { triggerElement = triggerElement
        , direction = Top
        , title = Nothing
        , content = Nothing
        }


{-| Define the popover body content.
-}
content :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
content attributes children (Config config) =
    Config
        { config
            | content =
                Html.div (class "popover-body" :: attributes) children
                    |> Content
                    |> Just
        }


{-| Define a popover title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
title :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
title =
    titlePrivate Html.div


{-| Define a popover h1 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH1 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH1 =
    titlePrivate Html.h1


{-| Define a popover h2 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH2 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH2 =
    titlePrivate Html.h2


{-| Define a popover h3 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH3 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH3 =
    titlePrivate Html.h3


{-| Define a popover h4 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH4 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH4 =
    titlePrivate Html.h4


{-| Define a popover h5 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH5 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH5 =
    titlePrivate Html.h5


{-| Define a popover h6 title.

  - `attributes` - List of attributes
  - `children` - List of child elements

-}
titleH6 :
    List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titleH6 =
    titlePrivate Html.h6


titlePrivate :
    (List (Html.Attribute msg) -> List (Html.Html msg) -> Html.Html msg)
    -> List (Html.Attribute msg)
    -> List (Html.Html msg)
    -> Config msg
    -> Config msg
titlePrivate elemFn attributes children (Config config) =
    Config
        { config
            | title =
                elemFn (class "popover-header" :: attributes) children
                    |> Title
                    |> Just
        }


{-| Show popover to the right of the triggering element.
-}
right : Config msg -> Config msg
right (Config config) =
    Config { config | direction = Right }


{-| Show popover to the left of the triggering element.
-}
left : Config msg -> Config msg
left (Config config) =
    Config { config | direction = Left }


{-| Show popover above the triggering element.
-}
top : Config msg -> Config msg
top (Config config) =
    Config { config | direction = Top }


{-| Show popover below the triggering element.
-}
bottom : Config msg -> Config msg
bottom (Config config) =
    Config { config | direction = Bottom }


stateDecoder : Json.Decoder DOMState
stateDecoder =
    Json.map3 DOMState
        (trigger [ "target" ])
        (popper [ "target" ] DOM.offsetWidth)
        (popper [ "target" ] DOM.offsetHeight)


trigger : List String -> Json.Decoder DOM.Rectangle
trigger path =
    Json.oneOf
        [ Json.at path isTrigger
            |> Json.andThen
                (\res ->
                    if res then
                        Json.at path DOM.boundingClientRect
                    else
                        Json.fail ""
                )
        , Json.at (path ++ [ "parentElement" ]) DOM.className
            |> Json.andThen
                (\_ -> trigger (path ++ [ "parentElement" ]))
        , Json.fail "No trigger found"
        ]


isTrigger : Json.Decoder Bool
isTrigger =
    DOM.className
        |> Json.andThen
            (\class ->
                if String.contains "popover-trigger" class then
                    Json.succeed True
                else
                    Json.succeed False
            )


popper : List String -> Json.Decoder a -> Json.Decoder a
popper path decoder =
    Json.oneOf
        [ Json.at (path ++ [ "nextSibling" ]) isPopover
            |> Json.andThen
                (\res ->
                    if res then
                        Json.at (path ++ [ "nextSibling" ]) decoder
                    else
                        Json.fail ""
                )
        , Json.at (path ++ [ "parentElement" ]) DOM.className
            |> Json.andThen
                (\_ -> popper (path ++ [ "parentElement" ]) decoder)
        , Json.fail "No popover found"
        ]


isPopover : Json.Decoder Bool
isPopover =
    DOM.className
        |> Json.andThen
            (\class ->
                if String.contains "popover" class then
                    Json.succeed True
                else
                    Json.succeed False
            )


calculatePos : Position -> DOMState -> Pos
calculatePos pos { rect, offsetWidth, offsetHeight } =
    case pos of
        Left ->
            { left = -offsetWidth - 10
            , top = (rect.height / 2) - (offsetHeight / 2)
            , arrowTop = Just <| (offsetHeight / 2) - 12
            , arrowLeft = Nothing
            }

        Right ->
            { left = rect.width
            , top = (rect.height / 2) - (offsetHeight / 2)
            , arrowTop = Just <| (offsetHeight / 2) - 12
            , arrowLeft = Nothing
            }

        Top ->
            { left = (rect.width / 2) - (offsetWidth / 2)
            , top = -offsetHeight - 10
            , arrowTop = Nothing
            , arrowLeft = Just <| (offsetWidth / 2) - 12
            }

        Bottom ->
            { left = (rect.width / 2) - (offsetWidth / 2)
            , top = rect.height
            , arrowTop = Nothing
            , arrowLeft = Just <| (offsetWidth / 2) - 12
            }