From 4d57d5dc12c928cd4320c14274599ffbcf24bfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Debois?= Date: Sun, 13 Mar 2016 22:47:00 +0100 Subject: [PATCH] Button API, demos. --- Demo.elm | 166 ++++++---------- Buttons.elm => Demo/Buttons.elm | 90 ++++----- Demo/Grid.elm | 36 ++++ Makefile | 4 +- Material/Aux.elm | 12 ++ Material/Button.elm | 216 ++++++++++++++------- Material/Grid.elm | 158 +++++++++++----- Material/Icon.elm | 25 ++- Material/Layout.elm | 273 +++++++++++++-------------- Material/Textfield.elm | 10 +- elm-mdl-demo.html => components.html | 16 +- elm-package.json | 1 + 12 files changed, 569 insertions(+), 438 deletions(-) rename Buttons.elm => Demo/Buttons.elm (60%) create mode 100644 Demo/Grid.elm rename elm-mdl-demo.html => components.html (59%) diff --git a/Demo.elm b/Demo.elm index 8c79763..21cf352 100644 --- a/Demo.elm +++ b/Demo.elm @@ -6,13 +6,14 @@ import Effects exposing (..) import Task import Signal import Task exposing (Task) -import Dict exposing (Dict) +import Array exposing (Array) -import Material.Textfield as Textfield -import Material.Grid as Grid exposing (Device(..)) -import Material.Layout as Layout +import Material.Layout as Layout exposing (defaultLayoutModel) +import Material -import Buttons +import Demo.Buttons +import Demo.Grid +import Demo.Textfields -- MODEL @@ -20,40 +21,23 @@ import Buttons type alias Model = { layout : Layout.Model - , buttons : Buttons.Model - , t0 : Textfield.Model - , t1 : Textfield.Model - , t2 : Textfield.Model - , t3 : Textfield.Model - , t4 : Textfield.Model + , buttons : Demo.Buttons.Model + , textfields : Demo.Textfields.Model } layoutModel : Layout.Model layoutModel = - { selectedTab = "Buttons" - , isDrawerOpen = False - , state = Layout.initState ["Buttons", "Grid", "Textfields"] + { defaultLayoutModel + | state = Layout.initState (List.length tabs) } model : Model model = - let t0 = Textfield.model in { layout = layoutModel - , buttons = Buttons.model - , t0 = t0 - , t1 = { t0 | label = Just { text = "Labelled", float = False } } - , t2 = { t0 | label = Just { text = "Floating label", float = True }} - , t3 = { t0 - | label = Just { text = "Disabled", float = False } - , isDisabled = True - } - , t4 = { t0 - | label = Just { text = "With error and value", float = False } - , error = Just "The input is wrong!" - , value = "Incorrect input" - } + , buttons = Demo.Buttons.model + , textfields = Demo.Textfields.model } @@ -62,12 +46,8 @@ model = type Action = LayoutAction Layout.Action - | ButtonsAction Buttons.Action - | T0 Textfield.Action - | T1 Textfield.Action - | T2 Textfield.Action - | T3 Textfield.Action - | T4 Textfield.Action + | ButtonsAction Demo.Buttons.Action + | TextfieldAction Demo.Textfields.Action update : Action -> Model -> (Model, Effects.Effects Action) @@ -80,24 +60,15 @@ update action model = ({ model | layout = l }, Effects.map LayoutAction e) ButtonsAction a -> - let (b, e) = Buttons.update a model.buttons + let + (b, e) = Demo.Buttons.update a model.buttons in ({ model | buttons = b }, Effects.map ButtonsAction e) - T0 a -> - ({ model | t0 = Textfield.update a model.t0 }, Effects.none) - - T1 a -> - ({ model | t1 = Textfield.update a model.t1 }, Effects.none) - - T2 a -> - ({ model | t2 = Textfield.update a model.t2 }, Effects.none) - - T3 a -> - ({ model | t3 = Textfield.update a model.t3 }, Effects.none) - - T4 a -> - ({ model | t4 = Textfield.update a model.t4 }, Effects.none) + TextfieldAction a -> + ({ model | textfields = Demo.Textfields.update a model.textfields } + , Effects.none + ) -- VIEW @@ -106,17 +77,17 @@ update action model = type alias Addr = Signal.Address Action -layoutConfig : Layout.Config -layoutConfig = Layout.defaultConfig - drawer : List Html drawer = - [ Layout.title "elm-mdl" + [ Layout.title "Example drawer" , Layout.navigation - [ Layout.link [] [text "Dead Link 1"] - , Layout.link [] [text "Dead Link 2"] - , Layout.link [] [text "Dead Link 3"] + [ Layout.link + [href "https://groups.google.com/forum/#!forum/elm-discuss"] + [text "Elm Discuss"] + , Layout.link + [href "http://elm-lang.org"] + [text "Elm"] ] ] @@ -129,75 +100,54 @@ header = [ Layout.link [ href "https://www.getmdl.io/components/index.html" ] [ text "MDL" ] + , Layout.link + [ href "https://www.google.com/design/spec/material-design/introduction.html"] + [ text "Material Design"] ] ] -tabGrid : Addr -> Model -> List Html -tabGrid addr model = - [ Grid.grid - [ Grid.cell [ Grid.col All 4 ] - [ h4 [] [text "Cell 1"] ] - , Grid.cell [ Grid.offset All 2, Grid.col All 4 ] - [ h4 [] [text "Cell 2"], p [] [text "This cell is offset by 2"] ] - , Grid.cell [ Grid.col All 6 ] - [ h4 [] [text "Cell 3"] ] - , Grid.cell [ Grid.col Tablet 6, Grid.col Desktop 12, Grid.col Phone 2 ] - [ h4 [] [text "Cell 4"], p [] [text "Size varies with device"] ] - ] - ] - - -tabButtons : Addr -> Model -> List Html -tabButtons addr model = - [ Buttons.view (Signal.forwardTo addr ButtonsAction) model.buttons ] - - -tabTextfields : Addr -> Model -> List Html -tabTextfields addr model = - let fwd = Signal.forwardTo addr in - [ Textfield.view (fwd T0) model.t0 - , Textfield.view (fwd T1) model.t1 - , Textfield.view (fwd T2) model.t2 - , Textfield.view (fwd T3) model.t3 - , Textfield.view (fwd T4) model.t4 - ] - |> List.map (\elem -> Grid.cell [ Grid.col All 4 ] [elem]) - |> (\content -> [Grid.grid content]) - - - -tabs : Dict String (Addr -> Model -> List Html) +tabs : List (String, Addr -> Model -> List Html) tabs = - Dict.fromList - [ ("Buttons", tabButtons) - , ("Textfields", tabTextfields) - , ("Grid", tabGrid) - ] + [ ("Buttons", \addr model -> + [Demo.Buttons.view (Signal.forwardTo addr ButtonsAction) model.buttons]) + , ("Textfields", \addr model -> + [Demo.Textfields.view (Signal.forwardTo addr TextfieldAction) model.textfields]) + , ("Grid", \addr model -> Demo.Grid.view) + ] + + +tabViews : Array (Addr -> Model -> List Html) +tabViews = List.map snd tabs |> Array.fromList + + +tabTitles : List Html +tabTitles = List.map (fst >> text) tabs view : Signal.Address Action -> Model -> Html view addr model = - let contents = - Dict.get model.layout.selectedTab tabs - |> Maybe.withDefault tabGrid - - top = + let top = div [ style [ ("margin", "auto") , ("width", "90%") ] ] - <| contents addr model - - addr' = Signal.forwardTo addr LayoutAction + ((Array.get model.layout.selectedTab tabViews + |> Maybe.withDefault (\addr model -> + [div [] [text "This can't happen."]] + ) + ) addr model) in - Layout.view addr' - layoutConfig model.layout - (Just drawer, Just header) - [ top ] + Layout.view (Signal.forwardTo addr LayoutAction) model.layout + { header = Just header + , drawer = Just drawer + , tabs = Just tabTitles + , main = [ top ] + } + |> Material.topWithColors Material.Teal Material.Red init : (Model, Effects.Effects Action) diff --git a/Buttons.elm b/Demo/Buttons.elm similarity index 60% rename from Buttons.elm rename to Demo/Buttons.elm index 7953ec3..2beac6e 100644 --- a/Buttons.elm +++ b/Demo/Buttons.elm @@ -1,11 +1,11 @@ -module Buttons where +module Demo.Buttons where import Dict import Html exposing (..) import Html.Attributes exposing (..) import Effects -import Material.Button as Button exposing (Appearance(..), Coloring(..)) +import Material.Button as Button exposing (..) import Material.Grid as Grid import Material.Icon as Icon @@ -27,16 +27,46 @@ tabulate : List a -> List (Int, a) tabulate = tabulate' 0 -row : Appearance -> Bool -> List (Int, (Bool, Button.Config)) -row appearance ripple = +type alias View = + Signal.Address Button.Action -> Button.Model -> Coloring -> List Html -> Html + +type alias View' = + Signal.Address Button.Action -> Button.Model -> Html + + +view' : View -> Coloring -> Html -> Signal.Address Button.Action -> Button.Model -> Html +view' view coloring elem addr model = + view addr model coloring [elem] + + +describe : String -> Bool -> Coloring -> String +describe kind ripple coloring = + let + c = + case coloring of + Plain -> "plain" + Colored -> "colored" + Primary -> "primary" + Accent -> "accent" + in + kind ++ ", " ++ c ++ if ripple then " w/ripple" else "" + + +row : (String, Html, View) -> Bool -> List (Int, (Bool, String, View')) +row (kind, elem, v) ripple = [ Plain, Colored, Primary, Accent ] - |> List.map (\c -> (ripple, { coloring = c, appearance = appearance })) + |> List.map (\c -> (ripple, describe kind ripple c, view' v c elem)) |> tabulate -buttons : List (List (Index, (Bool, Button.Config))) +buttons : List (List (Index, (Bool, String, View'))) buttons = - [Flat, Raised, FAB, MiniFAB, Icon] + [ ("flat", text "Flat Button", Button.flat) + , ("raised", text "Raised Button", Button.raised) + , ("FAB", Icon.i "add", Button.fab) + , ("mini-FAB", Icon.i "zoom_in", Button.minifab) + , ("icon", Icon.i "flight_land", Button.icon) + ] |> List.concatMap (\a -> [row a False, row a True]) |> tabulate |> List.map (\(i, row) -> List.map (\(j, x) -> ((i,j), x)) row) @@ -47,7 +77,7 @@ model = { clicked = "" , buttons = buttons - |> List.concatMap (List.map <| \(idx, (ripple, _)) -> (idx, Button.model ripple)) + |> List.concatMap (List.map <| \(idx, (ripple, _, _)) -> (idx, Button.model ripple)) |> Dict.fromList } @@ -79,36 +109,15 @@ update (Action idx action) model = -- VIEW -describe : Bool -> Button.Config -> String -describe ripple config = - let - appearance = - case config.appearance of - Flat -> "flat" - Raised -> "raised" - FAB -> "FAB" - MiniFAB -> "mini-FAB" - Icon -> "icon" - coloring = - case config.coloring of - Plain -> "plain" - Colored -> "colored" - Primary -> "primary" - Accent -> "accent" - in - appearance ++ ", " ++ coloring ++ if ripple then " w/ripple" else "" - - - view : Signal.Address Action -> Model -> Html view addr model = buttons |> List.concatMap (\row -> - row |> List.map (\(idx, (ripple, config)) -> + row |> List.concatMap (\(idx, (ripple, description, view)) -> let model' = Dict.get idx model.buttons |> Maybe.withDefault (Button.model False) in - Grid.cell - [ Grid.col Grid.All 3] + [ Grid.cell + [ Grid.size Grid.All 3] [ div [ style [ ("text-align", "center") @@ -116,27 +125,20 @@ view addr model = , ("margin-bottom", "1em") ] ] - [ Button.view + [ view (Signal.forwardTo addr (Action idx)) - config model' - [] - [ case config.appearance of - Flat -> text <| "Flat Button" - Raised -> text <| "Raised Button" - FAB -> Icon.i "add" - MiniFAB -> Icon.i "zoom_in" - Icon -> Icon.i "flight_land" - ] , div [ style [ ("font-size", "9pt") , ("margin-top", "1em") ] ] - [ text <| describe ripple config ] + [ text description + ] ] ] + ] ) ) - |> Grid.grid + |> Grid.grid diff --git a/Demo/Grid.elm b/Demo/Grid.elm new file mode 100644 index 0000000..0ddf04f --- /dev/null +++ b/Demo/Grid.elm @@ -0,0 +1,36 @@ +module Demo.Grid where + +import Material.Grid exposing (..) +import Html exposing (..) + + +view : List Html +view = + [ [1..12] + |> List.map (\i -> cell [size All 1] [text "1"]) + |> grid + , [1 .. 3] + |> List.map (\i -> cell [size All 4] [text <| "4"]) + |> grid + , [ cell [size All 6] [text "6"] + , cell [size All 4] [text "4"] + , cell [size All 2] [text "2"] + ] |> grid + , [ cell [size All 6, size Tablet 8] [text "6 (8 tablet)"] + , cell [size All 4, size Tablet 6] [text "4 (6 tablet)"] + , cell [size All 2, size Phone 4] [text "2 (4 phone)"] + ] |> grid + , Html.node "style" [] [text """ + .mdl-cell { + text-sizing: border-box; + background-color: #BDBDBD; + height: 200px; + padding-left: 8px; + padding-top: 4px; + color: white; + } + .mdl-grid:first-of-type .mdl-cell { + height: 50px; + } + """] + ] diff --git a/Makefile b/Makefile index 2613999..db5e6f8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ elm.js: - elm-make elm-mdl-demo.elm --output elm.js + elm-make Demo.elm --output elm.js -clean: +clean: rm -rf elm-stuff/build-artifacts elm.js .PHONY: clean elm.js diff --git a/Material/Aux.elm b/Material/Aux.elm index 338e993..0b31c77 100644 --- a/Material/Aux.elm +++ b/Material/Aux.elm @@ -12,6 +12,18 @@ filter elem attr html = elem attr (List.filterMap (\x -> x) html) +mapWithIndex : (Int -> a -> b) -> List a -> List b +mapWithIndex f xs = + let + loop k ys = + case ys of + [] -> [] + y :: ys -> f k y :: loop (k+1) ys + in + loop 0 xs + + + onClick' : Signal.Address a -> a -> Html.Attribute onClick' address x = Html.Events.onWithOptions diff --git a/Material/Button.elm b/Material/Button.elm index d84478a..e6e31d2 100644 --- a/Material/Button.elm +++ b/Material/Button.elm @@ -1,7 +1,7 @@ module Material.Button - ( model, update - , Kind(..), Coloring(..), Config - , view + ( Model, model, Action(Click), update + , Coloring(..) + , flat, raised, fab, minifab, icon ) where {-| From the [Material Design Lite documentation](http://www.getmdl.io/components/#buttons-section): @@ -11,12 +11,12 @@ module Material.Button > clearly communicates what action will occur when the user clicks or touches it. > The MDL button component provides various types of buttons, and allows you to > add both display and click effects. -> +> > Buttons are a ubiquitous feature of most user interfaces, regardless of a > site's content or function. Their design and use is therefore an important > factor in the overall user experience. See the button component's Material > Design specifications page for details. -> +> > The available button display types are flat (default), raised, fab, mini-fab, > and icon; any of these types may be plain (light gray) or colored, and may be > initially or programmatically disabled. The fab, mini-fab, and icon button @@ -26,16 +26,21 @@ See also the [Material Design Specification]([https://www.google.com/design/spec/components/buttons.html). # Component -@docs model, update +@docs Model, model, Action, update # View -@docs Kind, Coloring, Config, view +Refer to the +[Material Design Specification](https://www.google.com/design/spec/components/buttons.html) +for details about what type of buttons are appropriate for which situations. + +@docs Coloring, flat, raised, fab, minifab, icon -} import Html exposing (..) import Html.Attributes exposing (..) -import Effects exposing (Effects) +import Html.Events exposing (onClick) +import Effects exposing (Effects, none) import Material.Aux as Aux import Material.Ripple as Ripple @@ -47,8 +52,8 @@ import Material.Ripple as Ripple -- MODEL -{-| Model of the button. Determines if the button will ripple when clicked; -use `initState` to initalise it. +{-| Model of the button; common to all kinds of button. +Use `model` to initalise it. -} type Model = S (Maybe Ripple.Model) @@ -67,42 +72,34 @@ model shouldRipple = -- ACTION, UPDATE -{-| Component action. This exists exclusively to support ripple-animations. -To repsond to clicks, disable the button etc., supply event-handler attributes -to `view` as you would a regular button. +{-| Component action. The `Click` action fires when the button is clicked. -} -type alias Action = Ripple.Action +type Action + = Ripple Ripple.Action + | Click {-| Component update. -} update : Action -> Model -> (Model, Effects Action) update action model = - case model of - S (Just ripple) -> - let (ripple', e) = Ripple.update action ripple - in - (S (Just ripple'), e) - S Nothing -> - (model, Effects.none) + case action of + Click -> + (model, none) + + Ripple action' -> + case model of + S (Just ripple) -> + let (ripple', e) = Ripple.update action' ripple + in + (S (Just ripple'), Effects.map Ripple e) + S Nothing -> + (model, none) -- VIEW -{-| Type of button. Refer to the -[Material Design Specification](https://www.google.com/design/spec/components/buttons.html) -for what these look like and what they -are supposed to be used for. --} -type Kind - = Flat - | Raised - | FAB - | MiniFAB - | Icon - - {-| Coloring of a button. `Plain` respectively `Colored` is the button's uncolored respectively colored defaults. `Primary` respectively `Accent` chooses a colored button with the indicated @@ -115,48 +112,123 @@ type Coloring | Accent -{-| Button configuration: Its `Kind` and `Coloring`. --} -type alias Config = - { kind : Kind - , coloring : Coloring - } - - -{-| Construct a button view. Kind and coloring is given by -`Config`. To interact with the button, supply the usual -event-handler attributes, e.g., `onClick`. To disable the button, add the -standard HTML `disabled` attribute. - -NB! This implementation will override the properties `class`, `onmouseup`, -and `onmouseleave` even if you specify them as part of `List Attributes`. --} -view : Signal.Address Action -> Config -> Model -> List Attribute -> List Html -> Html -view addr config model attrs html = +view : String -> Signal.Address Action -> Model -> Coloring -> List Html -> Html +view kind addr model coloring html = button - (classList + [ classList [ ("mdl-button", True) , ("mdl-js-button", True) , ("mdl-js-ripple-effect", model /= S Nothing) -- Color effect. - , ("mdl-button--colored", config.coloring == Colored) - , ("mdl-button--primary", config.coloring == Primary) - , ("mdl-button--accent", config.coloring == Accent) + , ("mdl-button--colored", coloring == Colored) + , ("mdl-button--primary", coloring == Primary) + , ("mdl-button--accent", coloring == Accent) -- Kind. - , ("mdl-button--raised", config.kind == Raised) - , ("mdl-button--fab", config.kind == FAB || config.kind == MiniFAB) - , ("mdl-button--mini-fab", config.kind == MiniFAB) - , ("mdl-button--icon", config.kind == Icon) + , (kind, kind /= "") ] - :: Aux.blurOn "mouseup" - :: Aux.blurOn "mouseleave" - :: attrs) - (case model of - S (Just ripple) -> - Ripple.view - addr - [ class "mdl-button__ripple-container" - , Aux.blurOn "mouseup" ] - ripple - :: html - _ -> html) + , Aux.blurOn "mouseup" + , Aux.blurOn "mouseleave" + , onClick addr Click + ] + (case model of + S (Just ripple) -> + Ripple.view + (Signal.forwardTo addr Ripple) + [ class "mdl-button__ripple-container" + , Aux.blurOn "mouseup" ] + ripple + :: html + _ -> html) + + +{-| From the +[Material Design Specification](https://www.google.com/design/spec/components/buttons.html#buttons-flat-buttons): + +> Flat buttons are printed on material. They do not lift, but fill with color on +> press. +> +> Use flat buttons in the following locations: +> +> - On toolbars +> - In dialogs, to unify the button action with the dialog content +> - Inline, with padding, so the user can easily find them + +Example use (uncolored flat button, assuming properly setup model): + + import Material.Button as Button + + flatButton : Html + flatButton = Button.flat addr model Button.Plain [text "Click me!"] + +-} +flat : Signal.Address Action -> Model -> Coloring -> List Html -> Html +flat = view "" + + +{-| From the +[Material Design Specification](https://www.google.com/design/spec/components/buttons.html#buttons-raised-buttons): + +> Raised buttons add dimension to mostly flat layouts. They emphasize functions +> on busy or wide spaces. +> +> Raised buttons behave like a piece of material resting on another sheet – +> they lift and fill with color on press. + +Example use (colored raised button, assuming properly setup model): + + import Material.Button as Button + + raisedButton : Html + raisedButton = Button.raised addr model Button.Colored [text "Click me!"] + +-} +raised : Signal.Address Action -> Model -> Coloring -> List Html -> Html +raised = view "mdl-button--raised" + + +{-| Floating Action Button. From the +[Material Design Specification](https://www.google.com/design/spec/components/buttons-floating-action-button.html): + +> Floating action buttons are used for a promoted action. They are distinguished +> by a circled icon floating above the UI and have motion behaviors that include +> morphing, launching, and a transferring anchor point. +> +> Floating action buttons come in two sizes: +> +> - Default size: For most use cases +> - Mini size: Only used to create visual continuity with other screen elements + +This constructor produces the default size, use `minifab` to get the mini-size. + +Example use (colored with a '+' icon): + + import Material.Button as Button + import Material.Icon as Icon + + fabButton : Html + fabButton = fab addr model Colored [Icon.i "add"] +-} +fab : Signal.Address Action -> Model -> Coloring -> List Html -> Html +fab = view "mdl-button--fab" + + +{-| Mini-sized variant of a Floating Action Button; refer to `fab`. +-} +minifab : Signal.Address Action -> Model -> Coloring -> List Html -> Html +minifab = view "mdl-button--mini-fab" + + +{-| The [Material Design Lite implementation](https://www.getmdl.io/components/index.html#buttons-section) +also offers an "icon button", which we +re-implement here. See also +[Material Design Specification](http://www.google.com/design/spec/components/buttons.html#buttons-toggle-buttons). +Example use (no color, displaying a '+' icon): + + import Material.Button as Button + import Material.Icon as Icon + + iconButton : Html + iconButton = icon addr model Plain [Icon.i "add"] +-} +icon : Signal.Address Action -> Model -> Coloring -> List Html -> Html +icon = view "mdl-button--icon" diff --git a/Material/Grid.elm b/Material/Grid.elm index 4fe8e47..408e27a 100644 --- a/Material/Grid.elm +++ b/Material/Grid.elm @@ -1,25 +1,28 @@ module Material.Grid - ( grid - , size - , offset - , align + ( grid, gridWithOptions, Options , cell , Device(..) , Align(..) + , size + , offset + , align + , hide + , order ) where -{-| From the [Material Design Lite documentation](http://www.getmdl.io/components/#layout-section/grid): +{-| From the +[Material Design Lite documentation](http://www.getmdl.io/components/#layout-section/grid): > The Material Design Lite (MDL) grid component is a simplified method for laying > out content for multiple screen sizes. It reduces the usual coding burden > required to correctly display blocks of content in a variety of display > conditions. -> +> > The MDL grid is defined and enclosed by a container element. A grid has 12 > columns in the desktop screen size, 8 in the tablet size, and 4 in the phone > size, each size having predefined margins and gutters. Cells are laid out > sequentially in a row, in the order they are defined, with some exceptions: -> +> > - If a cell doesn't fit in the row in one of the screen sizes, it flows > into the following line. > - If a cell has a specified column size equal to or larger than the number @@ -30,28 +33,35 @@ Example use: import Material.Grid exposing (grid, cell, size, Device(..)) - grid - [ cell [ size All 4 ] - [ h4 [] [text "Cell 1"] - ] - , cell [ offset All 2, size All 4 ] - [ h4 [] [text "Cell 2"] - , p [] [text "This cell is offset by 2"] - ] - , cell [ size All 6 ] - [ h4 [] [text "Cell 3"] - ] - , cell [ size Tablet 6, size Desktop 12, size Phone 2 ] - [ h4 [] [text "Cell 4"] - , p [] [text "Size varies with device"] - ] - ] + top : Html + top = + grid + [ cell [ size All 4 ] + [ h4 [] [text "Cell 1"] + ] + , cell [ offset All 2, size All 4 ] + [ h4 [] [text "Cell 2"] + , p [] [text "This cell is offset by 2"] + ] + , cell [ size All 6 ] + [ h4 [] [text "Cell 3"] + ] + , cell [ size Tablet 6, size Desktop 12, size Phone 2 ] + [ h4 [] [text "Cell 4"] + , p [] [text "Size varies with device"] + ] + ] -# Views -@docs grid, cell +# Grid container +@docs grid, Options, gridWithOptions -# Cell configuration -@docs Device, size, offset, Align, align +# Cells + +Cells are configured with a `List CellConfig`; this configuration dictates the +size, offset, and alignment behaviour of the cell. Construct +individual `CellConfig` elements using `size`, `offset`, and `align`. + +@docs cell, Device, size, offset, Align, align, hide, order -} @@ -62,33 +72,77 @@ Example use: "You can set a maximum grid width, after which the grid stays centered with padding on either side, by setting its max-width CSS property." -2. mdl-grid--no-spacing -3. mdl-cell--stretch -4. mdl-cell--hide-* +2. mdl-cell--stretch -} import Html exposing (..) import Html.Attributes exposing (..) import String -import Material.Aux exposing (clip) +import Material.Aux exposing (clip, filter) -{-| Construct a grid. Use `cell` some number of times to construct the argument list. +{-| The `spacing` parameter indicates whether or not the grid should have +spacing between cells. The `maxWidth` parameter, which must be a valid CSS +dimension, indicates the maximum +width of the grid; if the grid is in a larger container, it stays centered with +padding on either side. -} -grid : List Html -> Html -grid elms = - div [class "mdl-grid"] elms +type alias Options = + { spacing : Bool + , maxWidth : Maybe String + } -{-| Device specifiers, used with `size` and `offset`. +{-| By default, a grid has spacing between columns, but no maximum width. +-} +defaultOptions : Options +defaultOptions = + { spacing = True + , maxWidth = Nothing + } + + +{-| Construct a grid with options. +-} +gridWithOptions : Options -> List Cell -> Html +gridWithOptions options elms = + div + [ classList + [ ("mdl-grid", True) + , ("mdl-grid--no-spacing", not options.spacing) + ] + , style ( + options.maxWidth + |> Maybe.map (\maxwidth -> [("max-width", maxwidth)]) + |> Maybe.withDefault [] + ) + ] + (List.map (\(Cell elm) -> elm) elms) + + +{-| Construct a grid with default options (i.e., default spacing, no +maximum width.) Use `cell` some number of times to construct the argument +list. +-} +grid : List Cell -> Html +grid = gridWithOptions defaultOptions + + +{-| Device specifiers, used with `size` and `offset`. (A `Device` really +encapsulates a screen size.) -} type Device = All | Desktop | Tablet | Phone {- Cell configuration. Construct with `size`, `offset`, and `align`. -} -type CellConfig = C String +type CellConfig = Config String + + +{- Opaque cell type. +-} +type Cell = Cell Html suffix : Device -> String @@ -112,7 +166,7 @@ size device k = Tablet -> clip 1 8 k Phone -> clip 1 4 k in - "mdl-cell--" ++ toString c ++ "-col" ++ suffix device |> C + "mdl-cell--" ++ toString c ++ "-col" ++ suffix device |> Config {-| Specify cell offset, i.e., empty number of empty cells before the present @@ -128,7 +182,7 @@ offset device k = Tablet -> clip 1 7 k Phone -> clip 1 3 k in - "mdl-cell--" ++ toString c ++ "-offset" ++ suffix device |> C + "mdl-cell--" ++ toString c ++ "-offset" ++ suffix device |> Config {-| Vertical alignment of cells; use with `align`. @@ -140,16 +194,34 @@ type Align = Top | Middle | Bottom -} align : Align -> CellConfig align a = - C <| case a of + Config <| case a of Top -> "mdl-cell--top" Middle -> "mdl-cell--middle" Bottom -> "mdl-cell--bottom" +{-| Specify that a cell should be hidden on given `Device`. +-} +hide : Device -> CellConfig +hide device = + Config <| case device of + All -> "" + _ -> "mdl-cell--hide-" ++ suffix device + + +{-| Specify that a cell should re-order itself to position 'Int' on `Device`. +-} +order : Device -> Int -> CellConfig +order device n = + Config <| "mdl-cell--order-" ++ (toString <| clip 1 12 n) ++ suffix device + + {-| Construct a cell for use in the argument list for `grid`. Construct the cell configuration (first argument) using `size`, `offset`, and `align`. Supply contents for the cell as the second argument. -} -cell : List CellConfig -> List Html -> Html -cell extents elms = - div [class <| String.join " " ("mdl-cell" :: (List.map (\(C s) -> s) extents))] elms +cell : List CellConfig -> List Html -> Cell +cell configs elms = + Cell <| div + [class <| String.join " " ("mdl-cell" :: (List.map (\(Config s) -> s) configs))] + elms diff --git a/Material/Icon.elm b/Material/Icon.elm index f627ac0..7b52263 100644 --- a/Material/Icon.elm +++ b/Material/Icon.elm @@ -31,9 +31,21 @@ type Size {-| View function for icons. Supply the -(Material Icons Library)[https://design.google.com/icons/] name as +[Material Icons Library](https://design.google.com/icons/) name as the first argument (replace spaces with underscores); and the size of the icon -as the second. +as the second. Do not use this function to produce clickable icons; use +icon buttons in Material.Button for that. + +I.e., to produce a 48px +["trending flat"](https://design.google.com/icons/#ic_trending_flat) icon with +no attributes: + + import Material.Icon as Icon + + icon : Html + icon = Icon.view "trending_flat" Icon.S48 [] + +This function will override any `class` set in `List Attribute`. -} view : String -> Size -> List Attribute -> Html view name size attrs = @@ -51,7 +63,14 @@ view name size attrs = {-| Render a default-sized icon with no behaviour. The `String` argument must be the name of a [Material Icon](https://design.google.com/icons/) -(replace spaces with underscores). +(replace spaces with underscores). + +I.e., to produce a default size (24xp) "trending flat" icon: + + import Material.Icon as Icon + + icon : Html + icon = Icon.i "trending_flat" -} i : String -> Html i name = view name S [] diff --git a/Material/Layout.elm b/Material/Layout.elm index 1d9d9f6..1bd795e 100644 --- a/Material/Layout.elm +++ b/Material/Layout.elm @@ -1,25 +1,25 @@ module Material.Layout ( setupSizeChangeSignal - , Model, initState + , Mode, Model, defaultLayoutModel, initState , Action(SwitchTab, ToggleDrawer), update , spacer, title, navigation, link - , Mode, Config, config, view + , Contents, view ) where -{-| From the +{-| From the [Material Design Lite documentation](https://www.getmdl.io/components/index.html#layout-section): - + > 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. -> +> > 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 -> component's +> component's > [Material Design specifications page](https://www.google.com/design/spec/layout/structure.html#structure-system-bars) -> for details. +> for details. > > Use of MDL layout principles simplifies the creation of scalable pages by > providing reusable components and encourages consistency across environments by @@ -30,20 +30,20 @@ module Material.Layout > flexibility and ease of use. # Model & Actions -@docs Model, initState, Action, update - -# Sub-components -@docs spacer, title, navigation, link +@docs Mode, Model, defaultLayoutModel, initState, Action, update # View -@docs Mode, Config, config, view +@docs Contents, view + +## Sub-views +@docs spacer, title, navigation, link # Setup @docs setupSizeChangeSignal -} -import Dict exposing (Dict) +import Array exposing (Array) import Maybe exposing (andThen, map) import Html exposing (..) import Html.Attributes exposing (..) @@ -79,13 +79,8 @@ setupSizeChangeSignal f = -- MODEL -type alias TabState = - { titles : List String - , ripples : Dict String Ripple.Model - } - type alias State' = - { tabs : TabState + { tabs : Array Ripple.Model , isSmallScreen : Bool } @@ -102,60 +97,74 @@ 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 -such, is open; otherwise, it has no significance. The `state` is the opaque -layout component state; use the function `initState` to construct it. (The names -of your tabs lives in this state; so you must use `initState` to set those -names.) +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. -} type alias Model = - { selectedTab : String + { selectedTab : Int , isDrawerOpen : Bool + -- Configuration + , fixedHeader : Bool + , fixedDrawer : Bool + , fixedTabs : Bool + , rippleTabs : Bool + , mode : Mode + -- State , state : State } -{-| Initialiser for Layout component state. Supply a list of tab titles -or the empty list if your layout should have no tabs. E.g., - - initState ["About", "Main", "Contact"] +{-| 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. -} -initState : List String -> State -initState titles = - let ripples = - titles - |> List.map (\title -> (title, Ripple.model)) - |> Dict.fromList - in - S { tabs = - { titles = titles - , ripples = ripples - } - , isSmallScreen = False -- TODO - } +initState : Int -> State +initState no_tabs = + S { tabs = Array.repeat no_tabs Ripple.model + , isSmallScreen = False -- TODO + } -hasTabs : Model -> Bool -hasTabs model = - case (s model).tabs.titles of - [] -> False - [x] -> False -- MDL spec says tabs should come in at least pairs. - _ -> True +{-| 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 + } -- ACTIONS, UPDATE -{-| Component actions. +{-| Component actions. Use `SwitchTab` to request a switch of tabs. Use `ToggleDrawer` to toggle the opened/closed state of the drawer. -} type Action - = SwitchTab String + = SwitchTab Int | ToggleDrawer -- Private | SmallScreen Bool -- True means small screen | ScrollTab Int - | Ripple String Ripple.Action + | Ripple Int Ripple.Action {-| Component update. @@ -177,19 +186,14 @@ update action model = ToggleDrawer -> { model | isDrawerOpen = not model.isDrawerOpen } |> pure - Ripple tab action' -> + Ripple tabIndex action' -> let - tabs = state.tabs (state', effect) = - Dict.get tab tabs.ripples + Array.get tabIndex (s model).tabs |> Maybe.map (Ripple.update action') |> Maybe.map (\(ripple', effect) -> - ({ state - | tabs = - { tabs - | ripples = Dict.insert tab ripple' tabs.ripples - } - }, Effects.map (Ripple tab) effect)) + ({ state | tabs = Array.set tabIndex ripple' (s model).tabs } + , Effects.map (Ripple tabIndex) effect)) |> Maybe.withDefault (pure state) in ({ model | state = S state' }, effect) @@ -230,7 +234,6 @@ link attrs contents = - -- MAIN VIEWS @@ -248,40 +251,12 @@ type Mode -- | Waterfall -{-| Layout view configuration. 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. Tabs have a -ripple-animation when clicked if `rippleTabs` is true. Finally, the header -respects `mode` - -} -type alias Config = - { fixedHeader : Bool - , fixedDrawer : Bool - , fixedTabs : Bool - , rippleTabs : Bool - , mode : Mode - } - - -{-| Default configuration of the layout: Fixed header, non-fixed drawer, -non-fixed tabs, tabs ripple, standard header behaviour. --} -config : Config -config = - { fixedHeader = True - , fixedDrawer = False - , fixedTabs = False - , rippleTabs = True - , mode = Standard - } - type alias Addr = Signal.Address Action -tabsView : Addr -> Config -> Model -> Html -tabsView addr config model = +tabsView : Addr -> Model -> List Html -> Html +tabsView addr model tabs = let chevron direction offset = div [ classList @@ -300,24 +275,23 @@ tabsView addr config model = , div [ classList [ ("mdl-layout__tab-bar", True) - , ("mdl-js-ripple-effect", config.rippleTabs) - , ("mds-js-ripple-effect--ignore-events", config.rippleTabs) + , ("mdl-js-ripple-effect", model.rippleTabs) + , ("mds-js-ripple-effect--ignore-events", model.rippleTabs) ] ] - (let (S state) = model.state in - state.tabs.titles |> List.map (\tab -> + (tabs |> mapWithIndex (\tabIndex tab -> filter a [ classList [ ("mdl-layout__tab", True) - , ("is-active", tab == model.selectedTab) + , ("is-active", tabIndex == model.selectedTab) ] - , onClick addr (SwitchTab tab) + , onClick addr (SwitchTab tabIndex) ] - [ text tab |> Just - , if config.rippleTabs then - Dict.get tab state.tabs.ripples |> Maybe.map ( + [ Just tab + , if model.rippleTabs then + Array.get tabIndex (s model).tabs |> Maybe.map ( Ripple.view - (Signal.forwardTo addr (Ripple tab)) + (Signal.forwardTo addr (Ripple tabIndex)) [ class "mdl-layout__tab-ripple-container" ] ) else @@ -328,12 +302,12 @@ tabsView addr config model = ] -headerView : Config -> Model -> (Maybe Html, Maybe (List Html), Maybe Html) -> Html -headerView config model (drawerButton, row, tabs) = +headerView : Model -> (Maybe Html, Maybe (List Html), Maybe Html) -> Html +headerView model (drawerButton, row, tabs) = filter Html.header [ classList [ ("mdl-layout__header", True) - , ("is-casting-shadow", config.mode == Standard) + , ("is-casting-shadow", model.mode == Standard) ] ] [ drawerButton @@ -342,15 +316,6 @@ headerView config model (drawerButton, row, tabs) = ] -{-} -visibilityClasses : Visibility -> List (String, Bool) -visibilityClasses v = - [ ("mdl-layout--large-screen-only", v == LargeScreenOnly) - , ("mdl-layout--small-screen-only", v == SmallScreenOnly) - ] --} - - drawerButton : Addr -> Html drawerButton addr = div @@ -383,39 +348,50 @@ drawerView addr model elems = elems -type alias Content = (Maybe (List Html), Maybe (List Html)) +{-| 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. - -{-| Main layout view. The `Content` argument contains the body -of the drawer and header (or `Nothing`). The final argument is -the contents of the main pane. +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. -} -view : Addr -> Config -> Model -> Content -> List Html -> Html -view addr config model (drawer, header) main = - let (contentDrawerButton, headerDrawerButton) = - case (drawer, header, config.fixedHeader) of - (Just _, Just _, True) -> - -- Drawer with fixedHeader: Add the button to the header - (Nothing, Just <| drawerButton addr) +type alias Contents = + { header : Maybe (List Html) + , drawer : Maybe (List Html) + , tabs : Maybe (List Html) + , main : List Html + } - (Just _, _, _) -> - -- Drawer, no or non-fixed header: Add the button before contents. - (Just <| drawerButton addr, Nothing) - _ -> - -- No drawer: no button. - (Nothing, Nothing) - mode = - case config.mode of - Standard -> "" - Scroll -> "mdl-layout__header-scroll" - -- Waterfall -> "mdl-layout__header-waterfall" - Seamed -> "mdl-layout__header-seamed" - tabs = - if hasTabs model then - tabsView addr config model |> Just - else - Nothing +{-| Main layout view. +-} +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 in div [ class "mdl-layout__container" ] @@ -423,16 +399,19 @@ view addr config model (drawer, header) main = [ classList [ ("mdl-layout", True) , ("is-upgraded", True) - , ("is-small-screen", let (S state) = model.state in state.isSmallScreen) + , ("is-small-screen", (s model).isSmallScreen) , ("has-drawer", drawer /= Nothing) - , ("has-tabs", hasTabs model) + , ("has-tabs", tabs /= Nothing) , ("mdl-js-layout", True) - , ("mdl-layout--fixed-drawer", config.fixedDrawer && drawer /= Nothing) - , ("mdl-layout--fixed-header", config.fixedHeader && header /= Nothing) - , ("mdl-layout--fixed-tabs", config.fixedTabs && hasTabs model) + , ("mdl-layout--fixed-drawer", model.fixedDrawer && drawer /= Nothing) + , ("mdl-layout--fixed-header", model.fixedHeader && hasHeader) + , ("mdl-layout--fixed-tabs", model.fixedTabs && tabs /= Nothing) ] ] - [ header |> Maybe.map (\_ -> headerView config model (headerDrawerButton, header, tabs)) + [ if hasHeader then + Just <| headerView model (headerDrawerButton, header, Maybe.map (tabsView addr model) tabs) + else + Nothing , drawer |> Maybe.map (\_ -> obfuscator addr model) , drawer |> Maybe.map (drawerView addr model) , contentDrawerButton diff --git a/Material/Textfield.elm b/Material/Textfield.elm index 49fb855..a897528 100644 --- a/Material/Textfield.elm +++ b/Material/Textfield.elm @@ -8,19 +8,19 @@ module Material.Textfield where > can occur and, typically, text that clearly communicates the intended > contents of the text field. The MDL text field component provides various > types of text fields, and allows you to add both display and click effects. -> +> > Text fields are a common feature of most user interfaces, regardless of a > site's content or function. Their design and use is therefore an important -> factor in the overall user experience. See the text field component's -> [Material Design specifications page](https://www.google.com/design/spec/components/text-fields.html) +> factor in the overall user experience. See the text field component's +> [Material Design specifications page](https://www.google.com/design/spec/components/text-fields.html) > for details. -> +> > The enhanced text field component has a more vivid visual look than a standard > text field, and may be initially or programmatically disabled. There are three > main types of text fields in the text field component, each with its own basic > coding requirements. The types are single-line, multi-line, and expandable. -This implementation provides only single-line. +This implementation provides only single-line. # Configuration diff --git a/elm-mdl-demo.html b/components.html similarity index 59% rename from elm-mdl-demo.html rename to components.html index 5de94d4..56dc12b 100644 --- a/elm-mdl-demo.html +++ b/components.html @@ -14,20 +14,8 @@ - + diff --git a/elm-package.json b/elm-package.json index 334eeae..c6e7c02 100644 --- a/elm-package.json +++ b/elm-package.json @@ -7,6 +7,7 @@ "." ], "exposed-modules": [ + "Material", "Material.Icon", "Material.Button", "Material.Textfield",