diff --git a/ui/src/Layout.elm b/ui/src/Layout.elm index 3b0196f..e4ff701 100644 --- a/ui/src/Layout.elm +++ b/ui/src/Layout.elm @@ -38,7 +38,10 @@ import Html.Attributes ) import Html.Events as Events import Modal +import Process import Route exposing (Route) +import Task +import Timer exposing (Timer) {-| Used to highlight current page in navbar. @@ -52,8 +55,9 @@ type Page type alias Model msg = { mapMsg : Msg -> msg - , menuVisible : Bool - , recentVisible : Bool + , mainMenuVisible : Bool + , recentMenuVisible : Bool + , recentMenuTimer : Timer , mailboxName : String } @@ -61,8 +65,9 @@ type alias Model msg = init : (Msg -> msg) -> Model msg init mapMsg = { mapMsg = mapMsg - , menuVisible = False - , recentVisible = False + , mainMenuVisible = False + , recentMenuVisible = False + , recentMenuTimer = Timer.empty , mailboxName = "" } @@ -72,20 +77,24 @@ init mapMsg = reset : Model msg -> Model msg reset model = { model - | menuVisible = False - , recentVisible = False + | mainMenuVisible = False + , recentMenuVisible = False + , recentMenuTimer = Timer.cancel model.recentMenuTimer , mailboxName = "" } type Msg = ClearFlash + | MainMenuToggled | ModalFocused Modal.Msg | ModalUnfocused | OnMailboxNameInput String | OpenMailbox - | ShowRecent Bool - | ToggleMenu + | RecentMenuMouseOver + | RecentMenuMouseOut + | RecentMenuTimeout Timer + | RecentMenuToggled update : Msg -> Model msg -> Session -> ( Model msg, Session, Cmd msg ) @@ -97,6 +106,12 @@ update msg model session = , Cmd.none ) + MainMenuToggled -> + ( { model | mainMenuVisible = not model.mainMenuVisible } + , session + , Cmd.none + ) + ModalFocused message -> ( model , Modal.updateSession message session @@ -122,14 +137,43 @@ update msg model session = , Route.pushUrl session.key (Route.Mailbox model.mailboxName) ) - ShowRecent visible -> - ( { model | recentVisible = visible } + RecentMenuMouseOver -> + ( { model + | recentMenuVisible = True + , recentMenuTimer = Timer.cancel model.recentMenuTimer + } , session , Cmd.none ) - ToggleMenu -> - ( { model | menuVisible = not model.menuVisible } + RecentMenuMouseOut -> + let + newTimer = + Timer.replace model.recentMenuTimer + in + ( { model + | recentMenuTimer = newTimer + } + , session + , Timer.schedule (RecentMenuTimeout >> model.mapMsg) newTimer 400 + ) + + RecentMenuTimeout timer -> + if timer == model.recentMenuTimer then + ( { model + | recentMenuVisible = False + , recentMenuTimer = Timer.cancel timer + } + , session + , Cmd.none + ) + + else + -- Timer was no longer valid. + ( model, session, Cmd.none ) + + RecentMenuToggled -> + ( { model | recentMenuVisible = not model.recentMenuVisible } , session , Cmd.none ) @@ -150,11 +194,11 @@ frame { model, session, activePage, activeMailbox, modal, content } = div [ class "app" ] [ header [] [ nav [ class "navbar" ] - [ button [ class "navbar-toggle", Events.onClick (ToggleMenu |> model.mapMsg) ] + [ button [ class "navbar-toggle", Events.onClick (MainMenuToggled |> model.mapMsg) ] [ i [ class "fas fa-bars" ] [] ] , span [ class "navbar-brand" ] [ a [ Route.href Route.Home ] [ text "@ inbucket" ] ] - , ul [ class "main-nav", classList [ ( "active", model.menuVisible ) ] ] + , ul [ class "main-nav", classList [ ( "active", model.mainMenuVisible ) ] ] [ if session.config.monitorVisible then navbarLink Monitor Route.Monitor [ text "Monitor" ] activePage @@ -256,15 +300,15 @@ navbarRecent page activeMailbox model session = [ class "navbar-dropdown-container" , classList [ ( "navbar-active", active ) ] , attribute "aria-haspopup" "true" - , ariaExpanded model.recentVisible - , Events.onMouseOver (ShowRecent True |> model.mapMsg) - , Events.onMouseOut (ShowRecent False |> model.mapMsg) + , ariaExpanded model.recentMenuVisible + , Events.onMouseOver (RecentMenuMouseOver |> model.mapMsg) + , Events.onMouseOut (RecentMenuMouseOut |> model.mapMsg) ] [ span [ class "navbar-dropdown" ] [ text title , button [ class "navbar-dropdown-button" - , Events.onClick (ShowRecent (not model.recentVisible) |> model.mapMsg) + , Events.onClick (RecentMenuToggled |> model.mapMsg) ] [ i [ class "fas fa-chevron-down" ] [] ] ] diff --git a/ui/src/Timer.elm b/ui/src/Timer.elm new file mode 100644 index 0000000..fc82d44 --- /dev/null +++ b/ui/src/Timer.elm @@ -0,0 +1,58 @@ +module Timer exposing (Timer, cancel, empty, replace, schedule) + +import Process +import Task + + +{-| Implements an identity to track an asynchronous timer. +-} +type Timer + = Empty + | Idle Int + | Timer Int + + +empty : Timer +empty = + Empty + + +schedule : (Timer -> msg) -> Timer -> Float -> Cmd msg +schedule message timer millis = + Task.perform (always (message timer)) (Process.sleep millis) + + +{-| Replaces the provided timer with a newly created one. +-} +replace : Timer -> Timer +replace previous = + case previous of + Empty -> + Timer 0 + + Idle index -> + Timer (next index) + + Timer index -> + Timer (next index) + + +{-| Cancels the provided timer without creating a replacement. +-} +cancel : Timer -> Timer +cancel previous = + case previous of + Timer index -> + Idle index + + _ -> + previous + + +next : Int -> Int +next index = + if index > 2 ^ 30 then + 0 + + else + index + 1