elm-mdl/Material/Layout.elm

421 lines
11 KiB
Elm
Raw Normal View History

2016-03-08 16:30:09 +00:00
module Material.Layout
( setupSizeChangeSignal
2016-03-13 21:47:00 +00:00
, Mode, Model, defaultLayoutModel, initState
2016-03-08 16:30:09 +00:00
, Action(SwitchTab, ToggleDrawer), update
, spacer, title, navigation, link
2016-03-13 21:47:00 +00:00
, Contents, view
2016-03-08 16:30:09 +00:00
) where
2016-03-13 21:47:00 +00:00
{-| From the
2016-03-08 16:30:09 +00:00
[Material Design Lite documentation](https://www.getmdl.io/components/index.html#layout-section):
2016-03-13 21:47:00 +00:00
2016-03-08 16:30:09 +00:00
> The Material Design Lite (MDL) layout component is a comprehensive approach to
> page layout that uses MDL development tenets, allows for efficient use of MDL
> components, and automatically adapts to different browsers, screen sizes, and
> devices.
2016-03-13 21:47:00 +00:00
>
2016-03-08 16:30:09 +00:00
> Appropriate and accessible layout is a critical feature of all user interfaces,
> regardless of a site's content or function. Page design and presentation is
> therefore an important factor in the overall user experience. See the layout
2016-03-13 21:47:00 +00:00
> component's
2016-03-08 16:30:09 +00:00
> [Material Design specifications page](https://www.google.com/design/spec/layout/structure.html#structure-system-bars)
2016-03-13 21:47:00 +00:00
> for details.
2016-03-08 16:30:09 +00:00
>
> Use of MDL layout principles simplifies the creation of scalable pages by
> providing reusable components and encourages consistency across environments by
> establishing recognizable visual elements, adhering to logical structural
> grids, and maintaining appropriate spacing across multiple platforms and screen
> sizes. MDL layout is extremely powerful and dynamic, allowing for great
> consistency in outward appearance and behavior while maintaining development
> flexibility and ease of use.
# Model & Actions
2016-03-13 21:47:00 +00:00
@docs Mode, Model, defaultLayoutModel, initState, Action, update
2016-03-08 16:30:09 +00:00
# View
2016-03-13 21:47:00 +00:00
@docs Contents, view
## Sub-views
@docs spacer, title, navigation, link
2016-03-08 16:30:09 +00:00
# Setup
@docs setupSizeChangeSignal
-}
2016-03-13 21:47:00 +00:00
import Array exposing (Array)
2016-03-08 16:30:09 +00:00
import Maybe exposing (andThen, map)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Effects exposing (Effects)
import Window
import Material.Aux exposing (..)
import Material.Ripple as Ripple
import Material.Icon as Icon
-- SETUP
{-| Setup signal for registering changes in display size. Use with StartApp
like so, supposing you have a `LayoutAction` encapsulating actions of the
layout:
inputs : List (Signal.Signal Action)
inputs =
[ Layout.setupSizeChangeSignal LayoutAction
]
-}
setupSizeChangeSignal : (Action -> a) -> Signal a
setupSizeChangeSignal f =
Window.width
|> Signal.map ((>) 1024)
|> Signal.dropRepeats
|> Signal.map (SmallScreen >> f)
-- MODEL
type alias State' =
2016-03-13 21:47:00 +00:00
{ tabs : Array Ripple.Model
2016-03-08 16:30:09 +00:00
, isSmallScreen : Bool
}
{-| Component private state. Construct with `initState`.
-}
type State = S State'
s : Model -> State'
s model = case model.state of (S state) -> state
{-| Layout model. If your layout view has tabs, any tab with the same name as
`selectedTab` will be highlighted as selected; otherwise, `selectedTab` has no
significance. `isDrawerOpen` indicates whether the drawer, if the layout has
2016-03-13 21:47:00 +00:00
such, is open; otherwise, it has no significance.
The header disappears on small devices unless
`fixedHeader` is true. The drawer opens and closes with user interactions
unless `fixedDrawer` is true, in which case it is permanently open on large
screens. Tabs scroll horisontally unless `fixedTabs` is true.
Finally, the header respects `mode`
The `state` is the opaque
layout component state; use the function `initState` to construct it. If you
change the number of tabs, you must re-initialise this state.
2016-03-08 16:30:09 +00:00
-}
type alias Model =
2016-03-13 21:47:00 +00:00
{ selectedTab : Int
2016-03-08 16:30:09 +00:00
, isDrawerOpen : Bool
2016-03-13 21:47:00 +00:00
-- Configuration
, fixedHeader : Bool
, fixedDrawer : Bool
, fixedTabs : Bool
, rippleTabs : Bool
, mode : Mode
-- State
2016-03-08 16:30:09 +00:00
, state : State
}
2016-03-13 21:47:00 +00:00
{-| Initialiser for Layout component state. Supply a number of tabs you
use in your layout. If you subsequently change the number of tabs, you
must re-initialise the state.
2016-03-08 16:30:09 +00:00
-}
2016-03-13 21:47:00 +00:00
initState : Int -> State
initState no_tabs =
S { tabs = Array.repeat no_tabs Ripple.model
, isSmallScreen = False -- TODO
}
2016-03-08 16:30:09 +00:00
2016-03-13 21:47:00 +00:00
{-| Default configuration of the layout: Fixed header, non-fixed drawer,
non-fixed tabs, tabs do not ripple, tab 0 is selected, standard header
behaviour.
-}
defaultLayoutModel : Model
defaultLayoutModel =
{ selectedTab = 0
, isDrawerOpen = False
, fixedHeader = True
, fixedDrawer = False
, fixedTabs = False
, rippleTabs = True
, mode = Standard
, state = initState 0
}
2016-03-08 16:30:09 +00:00
-- ACTIONS, UPDATE
2016-03-13 21:47:00 +00:00
{-| Component actions.
2016-03-08 16:30:09 +00:00
Use `SwitchTab` to request a switch of tabs. Use `ToggleDrawer` to toggle the
opened/closed state of the drawer.
-}
type Action
2016-03-13 21:47:00 +00:00
= SwitchTab Int
2016-03-08 16:30:09 +00:00
| ToggleDrawer
-- Private
| SmallScreen Bool -- True means small screen
| ScrollTab Int
2016-03-13 21:47:00 +00:00
| Ripple Int Ripple.Action
2016-03-08 16:30:09 +00:00
{-| Component update.
-}
update : Action -> Model -> (Model, Effects Action)
update action model =
let (S state) = model.state in
case action of
SmallScreen isSmall ->
{ model
| state = S ({ state | isSmallScreen = isSmall })
, isDrawerOpen = not isSmall && model.isDrawerOpen
}
|> pure
SwitchTab tab ->
{ model | selectedTab = tab } |> pure
ToggleDrawer ->
{ model | isDrawerOpen = not model.isDrawerOpen } |> pure
2016-03-13 21:47:00 +00:00
Ripple tabIndex action' ->
2016-03-08 16:30:09 +00:00
let
(state', effect) =
2016-03-13 21:47:00 +00:00
Array.get tabIndex (s model).tabs
2016-03-08 16:30:09 +00:00
|> Maybe.map (Ripple.update action')
|> Maybe.map (\(ripple', effect) ->
2016-03-13 21:47:00 +00:00
({ state | tabs = Array.set tabIndex ripple' (s model).tabs }
, Effects.map (Ripple tabIndex) effect))
2016-03-08 16:30:09 +00:00
|> Maybe.withDefault (pure state)
in
({ model | state = S state' }, effect)
ScrollTab tab ->
(model, Effects.none) -- TODO
-- AUXILIARY VIEWS
{-| Push subsequent elements in header row or drawer column to the right/bottom.
-}
spacer : Html
spacer = div [class "mdl-layout-spacer"] []
{-| Title in header row or drawer.
-}
title : String -> Html
title t = span [class "mdl-layout__title"] [text t]
{-| Container for links.
-}
navigation : List Html -> Html
navigation contents =
nav [class "mdl-navigation"] contents
{-| Link.
-}
link : List Attribute -> List Html -> Html
link attrs contents =
a (class "mdl-navigation__link" :: attrs) contents
-- MAIN VIEWS
{-| Mode for the header.
- A `Standard` header casts shadow, is permanently affixed to the top of the screen.
- A `Seamed` header does not cast shadow, is permanently affixed to the top of the
screen.
- A `Scroll`'ing header scrolls with contents.
-}
type Mode
= Standard
| Seamed
| Scroll
-- | Waterfall
type alias Addr = Signal.Address Action
2016-03-13 21:47:00 +00:00
tabsView : Addr -> Model -> List Html -> Html
tabsView addr model tabs =
2016-03-08 16:30:09 +00:00
let chevron direction offset =
div
[ classList
[ ("mdl-layout__tab-bar-button", True)
, ("mdl-layout__tab-bar-" ++ direction ++ "-button", True)
]
]
[ Icon.view ("chevron_" ++ direction) Icon.S
[onClick addr (ScrollTab offset)]
-- TODO: Scroll event
]
in
div
[ class "mdl-layout__tab-bar-container"]
[ chevron "left" -100
, div
[ classList
[ ("mdl-layout__tab-bar", True)
2016-03-13 21:47:00 +00:00
, ("mdl-js-ripple-effect", model.rippleTabs)
, ("mds-js-ripple-effect--ignore-events", model.rippleTabs)
2016-03-08 16:30:09 +00:00
]
]
2016-03-13 21:47:00 +00:00
(tabs |> mapWithIndex (\tabIndex tab ->
2016-03-08 16:30:09 +00:00
filter a
[ classList
[ ("mdl-layout__tab", True)
2016-03-13 21:47:00 +00:00
, ("is-active", tabIndex == model.selectedTab)
2016-03-08 16:30:09 +00:00
]
2016-03-13 21:47:00 +00:00
, onClick addr (SwitchTab tabIndex)
2016-03-08 16:30:09 +00:00
]
2016-03-13 21:47:00 +00:00
[ Just tab
, if model.rippleTabs then
Array.get tabIndex (s model).tabs |> Maybe.map (
2016-03-08 16:30:09 +00:00
Ripple.view
2016-03-13 21:47:00 +00:00
(Signal.forwardTo addr (Ripple tabIndex))
2016-03-08 16:30:09 +00:00
[ class "mdl-layout__tab-ripple-container" ]
)
else
Nothing
]
))
, chevron "right" 100
]
2016-03-13 21:47:00 +00:00
headerView : Model -> (Maybe Html, Maybe (List Html), Maybe Html) -> Html
headerView model (drawerButton, row, tabs) =
2016-03-08 16:30:09 +00:00
filter Html.header
[ classList
[ ("mdl-layout__header", True)
2016-03-13 21:47:00 +00:00
, ("is-casting-shadow", model.mode == Standard)
2016-03-08 16:30:09 +00:00
]
]
[ drawerButton
, row |> Maybe.map (div [ class "mdl-layout__header-row" ])
, tabs
]
drawerButton : Addr -> Html
drawerButton addr =
div
[ class "mdl-layout__drawer-button"
, onClick addr ToggleDrawer
]
[ Icon.i "menu" ]
obfuscator : Addr -> Model -> Html
obfuscator addr model =
div
[ classList
[ ("mdl-layout__obfuscator", True)
, ("is-visible", model.isDrawerOpen)
]
, onClick addr ToggleDrawer
]
[]
drawerView : Addr -> Model -> List Html -> Html
drawerView addr model elems =
div
[ classList
[ ("mdl-layout__drawer", True)
, ("is-visible", model.isDrawerOpen)
]
]
elems
2016-03-13 21:47:00 +00:00
{-| Content of the layout only (contents of main pane is set elsewhere). Every
part is optional. If `header` is `Nothing`, tabs will not be shown.
The `header` and `drawer` contains the contents of the header row and drawer,
respectively. Use `spacer`, `title`, `nav`, and
`link`, as well as regular Html to construct these. The `tabs` contains
the title of each tab.
-}
type alias Contents =
{ header : Maybe (List Html)
, drawer : Maybe (List Html)
, tabs : Maybe (List Html)
, main : List Html
}
2016-03-08 16:30:09 +00:00
2016-03-13 21:47:00 +00:00
{-| Main layout view.
2016-03-08 16:30:09 +00:00
-}
2016-03-13 21:47:00 +00:00
view : Addr -> Model -> Contents -> Html
view addr model { drawer, header, tabs, main } =
let
(contentDrawerButton, headerDrawerButton) =
case (drawer, header, model.fixedHeader) of
(Just _, Just _, True) ->
-- Drawer with fixedHeader: Add the button to the header
(Nothing, Just <| drawerButton addr)
(Just _, _, _) ->
-- Drawer, no or non-fixed header: Add the button before contents.
(Just <| drawerButton addr, Nothing)
_ ->
-- No drawer: no button.
(Nothing, Nothing)
mode =
case model.mode of
Standard -> ""
Scroll -> "mdl-layout__header-scroll"
-- Waterfall -> "mdl-layout__header-waterfall"
Seamed -> "mdl-layout__header-seamed"
hasHeader =
tabs /= Nothing || header /= Nothing
2016-03-08 16:30:09 +00:00
in
div
[ class "mdl-layout__container" ]
[ filter div
[ classList
[ ("mdl-layout", True)
, ("is-upgraded", True)
2016-03-13 21:47:00 +00:00
, ("is-small-screen", (s model).isSmallScreen)
2016-03-08 16:30:09 +00:00
, ("has-drawer", drawer /= Nothing)
2016-03-13 21:47:00 +00:00
, ("has-tabs", tabs /= Nothing)
2016-03-08 16:30:09 +00:00
, ("mdl-js-layout", True)
2016-03-13 21:47:00 +00:00
, ("mdl-layout--fixed-drawer", model.fixedDrawer && drawer /= Nothing)
, ("mdl-layout--fixed-header", model.fixedHeader && hasHeader)
, ("mdl-layout--fixed-tabs", model.fixedTabs && tabs /= Nothing)
2016-03-08 16:30:09 +00:00
]
]
2016-03-13 21:47:00 +00:00
[ if hasHeader then
Just <| headerView model (headerDrawerButton, header, Maybe.map (tabsView addr model) tabs)
else
Nothing
2016-03-08 16:30:09 +00:00
, drawer |> Maybe.map (\_ -> obfuscator addr model)
, drawer |> Maybe.map (drawerView addr model)
, contentDrawerButton
, Just <| main' [ class "mdl-layout__content" ] main
]
]