From 82b59530fdcbdb9192bccd61a64b8fe66c613745 Mon Sep 17 00:00:00 2001 From: Lucas Payr Date: Fri, 10 Apr 2020 08:04:33 +0200 Subject: [PATCH] added Search, refactored scrollingNav --- example/src/Component.elm | 13 +--- example/src/Example.elm | 75 ++++++++++++-------- example/src/Icons.elm | 8 +++ example/src/Reusable.elm | 10 +++ example/src/Stateless.elm | 10 +-- src/Core/Style.elm | 30 +++++--- src/Layout.elm | 123 +++++++++++++++++++++++++------- src/Widget/ScrollingNav.elm | 137 ++++++++++++++++-------------------- 8 files changed, 249 insertions(+), 157 deletions(-) diff --git a/example/src/Component.elm b/example/src/Component.elm index 3da1e98..952beea 100644 --- a/example/src/Component.elm +++ b/example/src/Component.elm @@ -154,17 +154,6 @@ validatedInput model = } ] - -scrollingNavCard : Element msg -scrollingNavCard = - [ Element.el Heading.h3 <| Element.text "Scrolling Nav" - , Element.text "Resize the screen and open the side-menu. Then start scrolling to see the scrolling navigation in action." - |> List.singleton - |> Element.paragraph [] - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) - - view : Model -> Element Msg view model = Element.column (Grid.section ++ [ Element.centerX ]) @@ -176,6 +165,6 @@ view model = , Element.wrappedRow (Grid.simple ++ [Element.height <| Element.shrink]) <| [ filterSelect model.filterSelect , validatedInput model.validatedInput - , scrollingNavCard + ] ] diff --git a/example/src/Example.elm b/example/src/Example.elm index 78bd0a8..9b58e91 100644 --- a/example/src/Example.elm +++ b/example/src/Example.elm @@ -21,7 +21,7 @@ import Framework.Tag as Tag import Html exposing (Html) import Html.Attributes as Attributes import Icons -import Layout exposing (Direction, Layout) +import Layout exposing (Part, Layout) import Core.Style exposing (Style) import Reusable import Set exposing (Set) @@ -43,6 +43,7 @@ type alias LoadedModel = , layout : Layout , displayDialog : Bool , deviceClass : DeviceClass + , search : String } @@ -55,14 +56,16 @@ type LoadedMsg = StatelessSpecific Stateless.Msg | ReusableSpecific Reusable.Msg | ComponentSpecific Component.Msg - | ScrollingNavSpecific (ScrollingNav.Msg Section) + | UpdateScrollingNav (ScrollingNav.Model Section -> ScrollingNav.Model Section) | TimePassed Int | AddSnackbar String | ToggleDialog Bool - | ChangedSidebar (Maybe Direction) + | ChangedSidebar (Maybe Part) | Resized { width : Int, height : Int } | Load String | JumpTo Section + | ChangedSearch String + | Idle type Msg @@ -109,6 +112,11 @@ style = , moreVerticalIcon = Icons.moreVertical |> Element.html |> Element.el [] , spacing = 8 + , title = Heading.h2 + , searchIcon = + Icons.search |> Element.html |> Element.el [] + , search = + Color.simple ++ [Font.color <| Element.rgb255 0 0 0 ] } @@ -119,6 +127,12 @@ initialModel { viewport } = ScrollingNav.init { labels = Section.toString , arrangement = Section.asList + , toMsg = \result -> + case result of + Ok fun -> + UpdateScrollingNav fun + Err _ -> + Idle } in ( { component = Component.init @@ -133,8 +147,9 @@ initialModel { viewport } = } |> Element.classifyDevice |> .class + , search = "" } - , cmd |> Cmd.map ScrollingNavSpecific + , cmd ) @@ -261,16 +276,15 @@ view model = ] , onChangedSidebar = ChangedSidebar , title = - (if m.deviceClass == Phone || m.deviceClass == Tablet then - m.scrollingNav - |> ScrollingNav.current Section.fromString - |> Maybe.map Section.toString - |> Maybe.withDefault "Elm-Ui-Widgets" - else - "Elm-Ui-Widgets" - ) + "Elm-Ui-Widgets" |> Element.text |> Element.el Heading.h1 + , search = + Just + { text = m.search + , onChange = ChangedSearch + , label = "Search" + } } @@ -309,23 +323,18 @@ updateLoaded msg model = } ) (Cmd.map StatelessSpecific) - - ScrollingNavSpecific m -> - model.scrollingNav - |> ScrollingNav.update m - |> Tuple.mapBoth - (\scrollingNav -> - { model - | scrollingNav = scrollingNav - } - ) - (Cmd.map ScrollingNavSpecific) + + UpdateScrollingNav fun -> + ( { model | scrollingNav = model.scrollingNav |> fun} + , Cmd.none + ) TimePassed int -> ( { model | layout = model.layout |> Layout.timePassed int } - , Cmd.none + , ScrollingNav.getPos + |> Task.perform UpdateScrollingNav ) AddSnackbar string -> @@ -344,7 +353,7 @@ updateLoaded msg model = ) ChangedSidebar sidebar -> - ( { model | layout = model.layout |> Layout.setSidebar sidebar } + ( { model | layout = model.layout |> Layout.activate sidebar } , Cmd.none ) @@ -354,9 +363,17 @@ updateLoaded msg model = JumpTo section -> ( model , model.scrollingNav - |> ScrollingNav.jumpTo section - |> Cmd.map ScrollingNavSpecific + |> ScrollingNav.jumpTo + { section = section + , onChange = always Idle + } ) + + ChangedSearch string -> + ( { model | search = string},Cmd.none) + + Idle -> + ( model , Cmd.none) update : Msg -> Model -> ( Model, Cmd Msg ) @@ -377,9 +394,7 @@ update msg model = subscriptions : Model -> Sub Msg subscriptions model = Sub.batch - [ ScrollingNav.subscriptions - |> Sub.map ScrollingNavSpecific - , Time.every 50 (always (TimePassed 50)) + [ Time.every 50 (always (TimePassed 50)) , Events.onResize (\h w -> Resized { height = h, width = w }) ] |> Sub.map LoadedSpecific diff --git a/example/src/Icons.elm b/example/src/Icons.elm index dd3410a..c6b19d1 100644 --- a/example/src/Icons.elm +++ b/example/src/Icons.elm @@ -6,6 +6,7 @@ module Icons exposing , circle , triangle , square + , search ) import Html exposing (Html) @@ -75,4 +76,11 @@ square : Html msg square = svgFeatherIcon "square" [ Svg.rect [ Svg.Attributes.x "3", y "3", width "18", height "18", rx "2", ry "2" ] [] + ] + +search : Html msg +search = + svgFeatherIcon "search" + [ Svg.circle [ cx "11", cy "11", r "8" ] [] + , Svg.line [ x1 "21", y1 "21", x2 "16.65", y2 "16.65" ] [] ] \ No newline at end of file diff --git a/example/src/Reusable.elm b/example/src/Reusable.elm index 0b4a15f..8bf0ab5 100644 --- a/example/src/Reusable.elm +++ b/example/src/Reusable.elm @@ -142,6 +142,15 @@ sortTable model = ] |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) +scrollingNavCard : Element msg +scrollingNavCard = + [ Element.el Heading.h3 <| Element.text "Scrolling Nav" + , Element.text "Resize the screen and open the side-menu. Then start scrolling to see the scrolling navigation in action." + |> List.singleton + |> Element.paragraph [] + ] + |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) + view : { addSnackbar : String -> msg @@ -159,5 +168,6 @@ view { addSnackbar, msgMapper, model } = , Element.wrappedRow (Grid.simple ++ [Element.height <| Element.shrink]) <| [ snackbar addSnackbar , sortTable model |> Element.map msgMapper + , scrollingNavCard ] ] diff --git a/example/src/Stateless.elm b/example/src/Stateless.elm index b3334ad..528eb1f 100644 --- a/example/src/Stateless.elm +++ b/example/src/Stateless.elm @@ -18,7 +18,7 @@ import Html exposing (Html) import Html.Attributes as Attributes import Set exposing (Set) import Widget -import Layout exposing (Direction(..)) +import Layout exposing (Part(..)) type alias Model = @@ -242,7 +242,7 @@ tab model = scrim : { showDialog : msg - , changedSheet : Maybe Direction -> msg + , changedSheet : Maybe Part -> msg } -> Model -> Element msg scrim {showDialog,changedSheet} model = [ Element.el Heading.h3 <| Element.text "Scrim" @@ -251,11 +251,11 @@ scrim {showDialog,changedSheet} model = , label = Element.text <| "Show dialog" } , Input.button Button.simple - { onPress = Just <| changedSheet <| Just Left + { onPress = Just <| changedSheet <| Just LeftSheet , label = Element.text <| "show left sheet" } , Input.button Button.simple - { onPress = Just <| changedSheet <| Just Right + { onPress = Just <| changedSheet <| Just RightSheet , label = Element.text <| "show right sheet" } ] @@ -301,7 +301,7 @@ carousel model = view : { msgMapper : Msg -> msg , showDialog : msg - , changedSheet : Maybe Direction -> msg + , changedSheet : Maybe Part -> msg } -> Model -> Element msg view { msgMapper, showDialog, changedSheet } model = Element.column (Grid.section ) diff --git a/src/Core/Style.elm b/src/Core/Style.elm index e2d07a6..3d1d838 100644 --- a/src/Core/Style.elm +++ b/src/Core/Style.elm @@ -1,4 +1,4 @@ -module Core.Style exposing (Style,menuTabButtonSelected,menuTabButton, menuButton, menuButtonSelected, menuIconButton, sheetButton, sheetButtonSelected) +module Core.Style exposing (Style, menuButton, menuButtonSelected, menuIconButton, menuTabButton, menuTabButtonSelected, sheetButton, sheetButtonSelected) import Element exposing (Attribute, Element) import Element.Input as Input @@ -19,10 +19,20 @@ type alias Style msg = , menuIcon : Element Never , moreVerticalIcon : Element Never , spacing : Int + , title : List (Attribute msg) + , searchIcon : Element Never + , search : List (Attribute msg) } -menuButtonSelected : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg +type alias ButtonInfo msg = + { label : String + , icon : Element Never + , onPress : Maybe msg + } + + +menuButtonSelected : Style msg -> ButtonInfo msg -> Element msg menuButtonSelected config { label, icon, onPress } = Input.button (config.menuButton ++ config.menuButtonSelected) { onPress = onPress @@ -34,7 +44,7 @@ menuButtonSelected config { label, icon, onPress } = } -menuButton : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg +menuButton : Style msg -> ButtonInfo msg -> Element msg menuButton config { label, icon, onPress } = Input.button config.menuButton { onPress = onPress @@ -46,7 +56,7 @@ menuButton config { label, icon, onPress } = } -menuIconButton : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg +menuIconButton : Style msg -> ButtonInfo msg -> Element msg menuIconButton config { label, icon, onPress } = Input.button config.menuButton { onPress = onPress @@ -54,7 +64,7 @@ menuIconButton config { label, icon, onPress } = } -sheetButton : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg +sheetButton : Style msg -> ButtonInfo msg -> Element msg sheetButton config { label, icon, onPress } = Input.button config.sheetButton { onPress = onPress @@ -66,7 +76,7 @@ sheetButton config { label, icon, onPress } = } -sheetButtonSelected : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg +sheetButtonSelected : Style msg -> ButtonInfo msg -> Element msg sheetButtonSelected config { label, icon, onPress } = Input.button (config.sheetButton ++ config.sheetButtonSelected) { onPress = onPress @@ -77,7 +87,8 @@ sheetButtonSelected config { label, icon, onPress } = ] } -menuTabButton : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg + +menuTabButton : Style msg -> ButtonInfo msg -> Element msg menuTabButton config { label, icon, onPress } = Input.button (config.menuButton ++ config.tabButton) { onPress = onPress @@ -88,7 +99,8 @@ menuTabButton config { label, icon, onPress } = ] } -menuTabButtonSelected : Style msg -> { label : String, icon : Element Never, onPress : Maybe msg } -> Element msg + +menuTabButtonSelected : Style msg -> ButtonInfo msg -> Element msg menuTabButtonSelected config { label, icon, onPress } = Input.button (config.menuButton ++ config.tabButton ++ config.tabButtonSelected) { onPress = onPress @@ -97,4 +109,4 @@ menuTabButtonSelected config { label, icon, onPress } = [ icon |> Element.map never , Element.text label ] - } \ No newline at end of file + } diff --git a/src/Layout.elm b/src/Layout.elm index 66b5b17..ca0689f 100644 --- a/src/Layout.elm +++ b/src/Layout.elm @@ -1,5 +1,6 @@ -module Layout exposing (Direction(..), Layout, init, queueMessage, setSidebar, timePassed, view) +module Layout exposing (Layout, Part(..), activate, init, queueMessage, timePassed, view) +import Array import Browser.Dom exposing (Viewport) import Core.Style as Style exposing (Style) import Element exposing (Attribute, DeviceClass(..), Element) @@ -12,21 +13,22 @@ import Widget import Widget.Snackbar as Snackbar -type Direction - = Left - | Right +type Part + = LeftSheet + | RightSheet + | Search type alias Layout = { snackbar : Snackbar.Model String - , sheet : Maybe Direction + , active : Maybe Part } init : Layout init = { snackbar = Snackbar.init - , sheet = Nothing + , active = Nothing } @@ -37,24 +39,27 @@ queueMessage message layout = } -setSidebar : Maybe Direction -> Layout -> Layout -setSidebar direction layout = +activate : Maybe Part -> Layout -> Layout +activate part layout = { layout - | sheet = direction + | active = part } timePassed : Int -> Layout -> Layout timePassed sec layout = - case layout.sheet of - Nothing -> + case layout.active of + Just LeftSheet -> + layout + + Just RightSheet -> + layout + + _ -> { layout | snackbar = layout.snackbar |> Snackbar.timePassed sec } - _ -> - layout - view : List (Attribute msg) @@ -68,12 +73,18 @@ view : { selected : Int , items : List { label : String, icon : Element Never, onPress : Maybe msg } } + , search : + Maybe + { onChange : String -> msg + , text : String + , label : String + } , actions : List { label : String, icon : Element Never, onPress : Maybe msg } - , onChangedSidebar : Maybe Direction -> msg + , onChangedSidebar : Maybe Part -> msg , style : Style msg } -> Html msg -view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, content, style, layout } = +view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, dialog, content, style, layout } = let ( primaryActions, moreActions ) = ( if (actions |> List.length) > 4 then @@ -107,10 +118,14 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c || ((menu.items |> List.length) > 5) then [ Input.button style.menuButton - { onPress = Just <| onChangedSidebar <| Just Left + { onPress = Just <| onChangedSidebar <| Just LeftSheet , label = style.menuIcon |> Element.map never } - , title + , menu.items + |> Array.fromList + |> Array.get menu.selected + |> Maybe.map (.label >> Element.text >> Element.el style.title) + |> Maybe.withDefault title ] else @@ -133,7 +148,40 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c [ Element.width <| Element.shrink , Element.spacing 8 ] - , [ primaryActions + , if deviceClass == Phone then + Element.none + + else + search + |> Maybe.map + (\{ onChange, text, label } -> + Input.text style.search + { onChange = onChange + , text = text + , placeholder = + Just <| + Input.placeholder [] <| + Element.text label + , label = Input.labelHidden label + } + ) + |> Maybe.withDefault Element.none + , [ if deviceClass == Phone then + search + |> Maybe.map + (\{ label } -> + [ Style.menuButton style + { onPress = Just <| onChangedSidebar <| Just Search + , icon = style.searchIcon + , label = label + } + ] + ) + |> Maybe.withDefault [] + + else + [] + , primaryActions |> List.map (if deviceClass == Phone then Style.menuIconButton style @@ -146,7 +194,7 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c else [ Style.menuButton style - { onPress = Just <| onChangedSidebar <| Just Right + { onPress = Just <| onChangedSidebar <| Just RightSheet , icon = style.moreVerticalIcon , label = "" } @@ -182,10 +230,13 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c ] ) |> Maybe.withDefault Element.none + sheet = - case layout.sheet of - Just Left -> - menu.items + case layout.active of + Just LeftSheet -> + [ [ title + ] + , menu.items |> List.indexedMap (\i -> if i == menu.selected then @@ -194,6 +245,8 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c else Style.sheetButton style ) + ] + |> List.concat |> Element.column [ Element.width <| Element.fill ] |> Element.el (style.sheet @@ -202,7 +255,7 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c ] ) - Just Right -> + Just RightSheet -> moreActions |> List.map (Style.sheetButton style) |> Element.column [ Element.width <| Element.fill ] @@ -213,6 +266,26 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c ] ) + Just Search -> + case search of + Just { onChange, text, label } -> + Input.text style.search + { onChange = onChange + , text = text + , placeholder = + Just <| + Input.placeholder [] <| + Element.text label + , label = Input.labelHidden label + } + |> Element.el + [ Element.alignTop + , Element.width <| Element.fill + ] + + Nothing -> + Element.none + Nothing -> Element.none in @@ -223,7 +296,7 @@ view attributes { title, onChangedSidebar, menu, actions, deviceClass, dialog, c , [ Element.inFront nav , Element.inFront snackbar ] - , if (layout.sheet /= Nothing) || (dialog /= Nothing) then + , if (layout.active /= Nothing) || (dialog /= Nothing) then Widget.scrim { onDismiss = Just <| diff --git a/src/Widget/ScrollingNav.elm b/src/Widget/ScrollingNav.elm index 8a55cf5..dab0dff 100644 --- a/src/Widget/ScrollingNav.elm +++ b/src/Widget/ScrollingNav.elm @@ -1,7 +1,7 @@ module Widget.ScrollingNav exposing - ( Model, Msg, init, update, subscriptions, view, viewSections, current + ( Model, init, view, viewSections, current , jumpTo, syncPositions - , jumpToWithOffset + , getPos, jumpToWithOffset, setPos ) {-| The Scrolling Nav is a navigation bar thats updates while you scroll through @@ -24,8 +24,7 @@ import Element exposing (Attribute, Element) import Framework.Grid as Grid import Html.Attributes as Attributes import IntDict exposing (IntDict) -import Task -import Time +import Task exposing (Task) {-| -} @@ -37,23 +36,15 @@ type alias Model section = } -{-| -} -type Msg section - = GotHeaderPos section (Result Dom.Error Int) - | ChangedViewport (Result Dom.Error ()) - | SyncPosition Int - | JumpTo section - | TimePassed - - {-| The intial state include the labels and the arrangement of the sections -} init : { labels : section -> String , arrangement : List section + , toMsg : Result Dom.Error (Model section -> Model section) -> msg } - -> ( Model section, Cmd (Msg section) ) -init { labels, arrangement } = + -> ( Model section, Cmd msg ) +init { labels, arrangement, toMsg } = { labels = labels , positions = IntDict.empty , arrangement = arrangement @@ -62,97 +53,91 @@ init { labels, arrangement } = |> (\a -> ( a , syncPositions a + |> Task.attempt toMsg ) ) -{-| -} -update : Msg section -> Model section -> ( Model section, Cmd (Msg section) ) -update msg model = - case msg of - GotHeaderPos label result -> - ( case result of - Ok pos -> - { model - | positions = - model.positions - |> IntDict.insert pos - (label |> model.labels) - } - - Err _ -> - model - , Cmd.none - ) - - ChangedViewport _ -> - ( model, Cmd.none ) - - SyncPosition pos -> - ( { model - | scrollPos = pos - } - , Cmd.none - ) - - TimePassed -> - ( model - , Dom.getViewport - |> Task.map (.viewport >> .y >> round) - |> Task.perform SyncPosition - ) - - JumpTo elem -> - ( model - , model - |> jumpTo elem +getPos : Task x (Model selection -> Model selection) +getPos = + Dom.getViewport + |> Task.map + (\int model -> + { model + | scrollPos = int.viewport.y |> round + } ) -{-| -} -subscriptions : Sub (Msg msg) -subscriptions = - Time.every 100 (always TimePassed) +setPos : Int -> Model section -> Model section +setPos pos model = + { model | scrollPos = pos } {-| scrolls the screen to the respective section -} -jumpTo : section -> Model section -> Cmd (Msg msg) -jumpTo section { labels } = +jumpTo : + { section : section + , onChange : Result Dom.Error () -> msg + } + -> Model section + -> Cmd msg +jumpTo { section, onChange } { labels } = Dom.getElement (section |> labels) |> Task.andThen (\{ element } -> - Dom.setViewport 0 (element.y) + Dom.setViewport 0 element.y ) - |> Task.attempt ChangedViewport + |> Task.attempt onChange + {-| scrolls the screen to the respective section with some offset -} -jumpToWithOffset : Float -> section -> Model section -> Cmd (Msg msg) -jumpToWithOffset offset section { labels } = +jumpToWithOffset : + { offset : Float + , section : section + , onChange : Result Dom.Error () -> msg + } + -> Model section + -> Cmd msg +jumpToWithOffset { offset, section, onChange } { labels } = Dom.getElement (section |> labels) |> Task.andThen (\{ element } -> Dom.setViewport 0 (element.y - offset) ) - |> Task.attempt ChangedViewport + |> Task.attempt onChange + {-| -} -syncPositions : Model section -> Cmd (Msg section) +syncPositions : Model section -> Task Dom.Error (Model section -> Model section) syncPositions { labels, arrangement } = arrangement |> List.map (\label -> Dom.getElement (labels label) |> Task.map - (.element - >> .y - >> round + (\x -> + ( x.element.y |> round + , label + ) ) - |> Task.attempt - (GotHeaderPos label) ) - |> Cmd.batch + |> Task.sequence + |> Task.map + (\list m -> + list + |> List.foldl + (\( pos, label ) model -> + { model + | positions = + model.positions + |> IntDict.insert pos + (label |> model.labels) + } + ) + m + ) {-| -} @@ -170,7 +155,7 @@ current fromString { positions, scrollPos } = viewSections : { label : String -> Element msg , fromString : String -> Maybe section - , msgMapper : Msg section -> msg + , onSelect : section -> msg , attributes : Bool -> List (Attribute msg) } -> Model section @@ -181,11 +166,11 @@ viewSections : , onChange : section -> msg , attributes : Bool -> List (Attribute msg) } -viewSections { label, fromString, msgMapper, attributes } ({ arrangement, scrollPos, labels, positions } as model) = +viewSections { label, fromString, onSelect, attributes } ({ arrangement, labels } as model) = { selected = model |> current fromString , options = arrangement , label = \elem -> label (elem |> labels) - , onChange = JumpTo >> msgMapper + , onChange = onSelect , attributes = attributes }