1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 18:17:03 +00:00

Merge branch 'feature/keybd-nav' into develop

This commit is contained in:
James Hillyerd
2019-02-17 14:05:19 -08:00
7 changed files with 276 additions and 128 deletions

View File

@@ -1,4 +1,4 @@
module Layout exposing (Page(..), frame)
module Layout exposing (Model, Msg, Page(..), frame, init, reset, update)
import Data.Session as Session exposing (Session)
import Html exposing (..)
@@ -29,56 +29,124 @@ type Page
| Status
type alias FrameControls msg =
{ viewMailbox : String -> msg
, mailboxOnInput : String -> msg
, mailboxValue : String
, recentOptions : List String
, recentActive : String
, clearFlash : msg
, showMenu : Bool
, toggleMenu : msg
type alias Model msg =
{ mapMsg : Msg -> msg
, menuVisible : Bool
, recentVisible : Bool
, mailboxName : String
}
frame : FrameControls msg -> Session -> Page -> Maybe (Html msg) -> List (Html msg) -> Html msg
frame controls session activePage modal content =
init : (Msg -> msg) -> Model msg
init mapMsg =
{ mapMsg = mapMsg
, menuVisible = False
, recentVisible = False
, mailboxName = ""
}
{-| Resets layout state, used when navigating to a new page.
-}
reset : Model msg -> Model msg
reset model =
{ model
| menuVisible = False
, recentVisible = False
, mailboxName = ""
}
type Msg
= ClearFlash
| OnMailboxNameInput String
| OpenMailbox
| ShowRecent Bool
| ToggleMenu
update : Msg -> Model msg -> Session -> ( Model msg, Session, Cmd msg )
update msg model session =
case msg of
ClearFlash ->
( model
, Session.clearFlash session
, Cmd.none
)
OnMailboxNameInput name ->
( { model | mailboxName = name }
, session
, Cmd.none
)
OpenMailbox ->
( model
, session
, Route.pushUrl session.key (Route.Mailbox model.mailboxName)
)
ShowRecent visible ->
( { model | recentVisible = visible }
, session
, Cmd.none
)
ToggleMenu ->
( { model | menuVisible = not model.menuVisible }
, session
, Cmd.none
)
type alias State msg =
{ model : Model msg
, session : Session
, activePage : Page
, activeMailbox : String
, modal : Maybe (Html msg)
, content : List (Html msg)
}
frame : State msg -> Html msg
frame { model, session, activePage, activeMailbox, modal, content } =
div [ class "app" ]
[ header []
[ nav [ class "navbar" ]
[ span [ class "navbar-toggle", Events.onClick controls.toggleMenu ]
[ button [ class "navbar-toggle", Events.onClick (ToggleMenu |> model.mapMsg) ]
[ i [ class "fas fa-bars" ] [] ]
, span [ class "navbar-brand" ]
[ a [ Route.href Route.Home ] [ text "@ inbucket" ] ]
, ul [ classList [ ( "main-nav", True ), ( "active", controls.showMenu ) ] ]
[ li [ class "navbar-mailbox" ]
[ form [ Events.onSubmit (controls.viewMailbox controls.mailboxValue) ]
[ input
[ type_ "text"
, placeholder "mailbox"
, value controls.mailboxValue
, Events.onInput controls.mailboxOnInput
]
[]
]
]
, if session.config.monitorVisible then
, ul [ class "main-nav", classList [ ( "active", model.menuVisible ) ] ]
[ if session.config.monitorVisible then
navbarLink Monitor Route.Monitor [ text "Monitor" ] activePage
else
text ""
, navbarLink Status Route.Status [ text "Status" ] activePage
, navbarRecent activePage controls
, navbarRecent activePage activeMailbox model session
, li [ class "navbar-mailbox" ]
[ form [ Events.onSubmit (OpenMailbox |> model.mapMsg) ]
[ input
[ type_ "text"
, placeholder "mailbox"
, value model.mailboxName
, Events.onInput (OnMailboxNameInput >> model.mapMsg)
]
[]
]
]
]
]
]
, div [ class "navbar-bg" ] [ text "" ]
, frameModal modal
, div [ class "page" ] ([ errorFlash controls session.flash ] ++ content)
, div [ class "page" ] ([ errorFlash model session.flash ] ++ content)
, footer []
[ div [ class "footer" ]
[ externalLink "https://www.inbucket.org" "Inbucket"
, text " is an open source projected hosted at "
, text " is an open source project hosted on "
, externalLink "https://github.com/jhillyerd/inbucket" "GitHub"
, text "."
]
@@ -98,8 +166,8 @@ frameModal maybeModal =
text ""
errorFlash : FrameControls msg -> Maybe Session.Flash -> Html msg
errorFlash controls maybeFlash =
errorFlash : Model msg -> Maybe Session.Flash -> Html msg
errorFlash model maybeFlash =
let
row ( heading, message ) =
tr []
@@ -115,7 +183,7 @@ errorFlash controls maybeFlash =
div [ class "well well-error" ]
[ div [ class "flash-header" ]
[ h2 [] [ text flash.title ]
, a [ href "#", Events.onClick controls.clearFlash ] [ text "Close" ]
, a [ href "#", Events.onClick (ClearFlash |> model.mapMsg) ] [ text "Close" ]
]
, div [ class "flash-table" ] (List.map row flash.table)
]
@@ -129,21 +197,22 @@ externalLink url title =
navbarLink : Page -> Route -> List (Html a) -> Page -> Html a
navbarLink page route linkContent activePage =
li [ classList [ ( "navbar-active", page == activePage ) ] ]
[ a [ class "navbar-active-bg", Route.href route ] linkContent ]
[ a [ Route.href route ] linkContent ]
{-| Renders list of recent mailboxes, selecting the currently active mailbox.
-}
navbarRecent : Page -> FrameControls msg -> Html msg
navbarRecent page controls =
navbarRecent : Page -> String -> Model msg -> Session -> Html msg
navbarRecent page activeMailbox model session =
let
-- Active means we are viewing a specific mailbox.
active =
page == Mailbox
-- Recent tab title is the name of the current mailbox when active.
title =
if active then
controls.recentActive
activeMailbox
else
"Recent Mailboxes"
@@ -151,18 +220,46 @@ navbarRecent page controls =
-- Mailboxes to show in recent list, doesn't include active mailbox.
recentMailboxes =
if active then
List.tail controls.recentOptions |> Maybe.withDefault []
List.tail session.persistent.recentMailboxes |> Maybe.withDefault []
else
controls.recentOptions
session.persistent.recentMailboxes
dropdownExpanded =
if model.recentVisible then
"true"
else
"false"
recentLink mailbox =
a [ Route.href (Route.Mailbox mailbox) ] [ text mailbox ]
in
li
[ class "navbar-recent"
, classList [ ( "navbar-dropdown", True ), ( "navbar-active", active ) ]
[ 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)
]
[ span [ class "navbar-active-bg" ] [ text title ]
[ span [ class "navbar-dropdown" ]
[ text title
, button
[ class "navbar-dropdown-button"
, Events.onClick (ShowRecent (not model.recentVisible) |> model.mapMsg)
]
[ i [ class "fas fa-chevron-down" ] [] ]
]
, div [ class "navbar-dropdown-content" ] (List.map recentLink recentMailboxes)
]
ariaExpanded : Bool -> Attribute msg
ariaExpanded value =
attribute "aria-expanded" <|
if value then
"true"
else
"false"

View File

@@ -23,9 +23,8 @@ import Url exposing (Url)
type alias Model =
{ page : PageModel
, mailboxName : String
, showMenu : Bool
{ layout : Layout.Model Msg
, page : PageModel
}
@@ -62,9 +61,8 @@ init configValue location key =
Home.init session
initModel =
{ page = Home subModel
, mailboxName = ""
, showMenu = False
{ layout = Layout.init LayoutMsg
, page = Home subModel
}
route =
@@ -81,10 +79,7 @@ type Msg
| LinkClicked UrlRequest
| SessionUpdated (Result D.Error Session.Persistent)
| TimeZoneLoaded Time.Zone
| ClearFlash
| OnMailboxNameInput String
| ViewMailbox String
| ToggleMenu
| LayoutMsg Layout.Msg
| HomeMsg Home.Msg
| MailboxMsg Mailbox.Msg
| MonitorMsg Monitor.Msg
@@ -182,11 +177,6 @@ updateMain msg model session =
, Cmd.none
)
ClearFlash ->
( applyToModelSession Session.clearFlash model
, Cmd.none
)
SessionUpdated (Ok persistent) ->
( updateSession model { session | persistent = persistent }
, Cmd.none
@@ -208,17 +198,15 @@ updateMain msg model session =
, Cmd.none
)
OnMailboxNameInput name ->
( { model | mailboxName = name }, Cmd.none )
ViewMailbox name ->
( applyToModelSession Session.clearFlash { model | mailboxName = "" }
, Route.pushUrl session.key (Route.Mailbox name)
LayoutMsg subMsg ->
let
( layout, newSession, cmd ) =
Layout.update subMsg model.layout session
in
( updateSession { model | layout = layout } newSession
, cmd
)
ToggleMenu ->
( { model | showMenu = not model.showMenu }, Cmd.none )
_ ->
updatePage msg model
@@ -256,7 +244,7 @@ changeRouteTo route model =
getSession model
newModel =
{ model | showMenu = False }
{ model | layout = Layout.reset model.layout }
in
case route of
Route.Unknown path ->
@@ -372,17 +360,6 @@ view model =
_ ->
""
controls =
{ viewMailbox = ViewMailbox
, mailboxOnInput = OnMailboxNameInput
, mailboxValue = model.mailboxName
, recentOptions = session.persistent.recentMailboxes
, recentActive = mailbox
, clearFlash = ClearFlash
, showMenu = model.showMenu
, toggleMenu = ToggleMenu
}
framePage :
Layout.Page
-> (msg -> Msg)
@@ -391,11 +368,13 @@ view model =
framePage page toMsg { title, modal, content } =
Document title
[ Layout.frame
controls
session
page
(Maybe.map (Html.map toMsg) modal)
(List.map (Html.map toMsg) content)
{ model = model.layout
, session = session
, activePage = page
, activeMailbox = mailbox
, modal = Maybe.map (Html.map toMsg) modal
, content = List.map (Html.map toMsg) content
}
]
in
case model.page of

View File

@@ -17,15 +17,16 @@ import Html.Attributes
, id
, placeholder
, property
, tabindex
, target
, type_
, value
)
import Html.Events exposing (..)
import Html.Events as Events
import Http exposing (Error)
import HttpUtil
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import Json.Decode as D
import Json.Encode as E
import Ports
import Route
import Task
@@ -135,6 +136,7 @@ subscriptions model =
type Msg
= ListLoaded (Result HttpUtil.Error (List MessageHeader))
| ClickMessage MessageID
| ListKeyPress String Int
| OpenMessage MessageID
| CloseMessage
| MessageLoaded (Result HttpUtil.Error Message)
@@ -165,7 +167,7 @@ update msg model =
)
OpenMessage id ->
updateOpenMessage model.session model id
updateOpenMessage model id
CloseMessage ->
case model.state of
@@ -186,6 +188,14 @@ update msg model =
, Cmd.none
)
ListKeyPress id keyCode ->
case keyCode of
13 ->
updateOpenMessage model id
_ ->
( model, Cmd.none )
ListLoaded (Ok headers) ->
case model.state of
LoadingList selection ->
@@ -197,7 +207,7 @@ update msg model =
in
case selection of
Just id ->
updateOpenMessage model.session newModel id
updateOpenMessage newModel id
Nothing ->
( { newModel
@@ -458,8 +468,8 @@ updateMarkMessageSeen model message =
( model, Cmd.none )
updateOpenMessage : Session -> Model -> String -> ( Model, Cmd Msg )
updateOpenMessage session model id =
updateOpenMessage : Model -> String -> ( Model, Cmd Msg )
updateOpenMessage model id =
let
newModel =
{ model | session = Session.addRecent model.mailboxName model.session }
@@ -492,12 +502,12 @@ view model =
[ input
[ type_ "search"
, placeholder "search"
, onInput OnSearchInput
, Events.onInput OnSearchInput
, value model.searchInput
]
[]
, button
[ onClick PurgeMailboxPrompt
[ Events.onClick PurgeMailboxPrompt
, alt "Purge Mailbox"
]
[ i [ class "fas fa-trash" ] [] ]
@@ -533,8 +543,8 @@ viewModal promptPurge =
div []
[ p [] [ text "Are you sure you want to delete all messages in this mailbox?" ]
, div [ class "button-bar" ]
[ button [ onClick PurgeMailboxConfirmed, class "danger" ] [ text "Yes" ]
, button [ onClick PurgeMailboxCanceled ] [ text "Cancel" ]
[ button [ Events.onClick PurgeMailboxConfirmed, class "danger" ] [ text "Yes" ]
, button [ Events.onClick PurgeMailboxCanceled ] [ text "Cancel" ]
]
]
@@ -559,12 +569,14 @@ viewMessageList session model =
messageChip : Model -> Maybe MessageID -> MessageHeader -> Html Msg
messageChip model selected message =
div
[ classList
[ ( "message-list-entry", True )
, ( "selected", selected == Just message.id )
[ class "message-list-entry"
, classList
[ ( "selected", selected == Just message.id )
, ( "unseen", not message.seen )
]
, onClick (ClickMessage message.id)
, Events.onClick (ClickMessage message.id)
, onKeyUp (ListKeyPress message.id)
, tabindex 0
]
[ div [ class "subject" ] [ text message.subject ]
, div [ class "from" ] [ text message.from ]
@@ -586,18 +598,16 @@ viewMessage zone message bodyMode =
text ""
else
a
[ href htmlUrl, target "_blank" ]
[ button [] [ text "Raw HTML" ] ]
a [ href htmlUrl, target "_blank" ]
[ button [ tabindex -1 ] [ text "Raw HTML" ] ]
in
div []
[ div [ class "button-bar" ]
[ button [ class "message-close light", onClick CloseMessage ]
[ button [ class "message-close light", Events.onClick CloseMessage ]
[ i [ class "fas fa-arrow-left" ] [] ]
, button [ class "danger", onClick (DeleteMessage message) ] [ text "Delete" ]
, a
[ href sourceUrl, target "_blank" ]
[ button [] [ text "Source" ] ]
, button [ class "danger", Events.onClick (DeleteMessage message) ] [ text "Delete" ]
, a [ href sourceUrl, target "_blank" ]
[ button [ tabindex -1 ] [ text "Source" ] ]
, htmlButton
]
, dl [ class "message-header" ]
@@ -644,7 +654,7 @@ messageBody message bodyMode =
bodyModeTab mode label =
a
[ classList [ ( "active", bodyMode == mode ) ]
, onClick (MessageBody mode)
, Events.onClick (MessageBody mode)
, href "#"
]
[ text label ]
@@ -667,10 +677,10 @@ messageBody message bodyMode =
, article [ class "message-body" ]
[ case bodyMode of
SafeHtmlBody ->
Html.node "rendered-html" [ property "content" (Encode.string message.html) ] []
Html.node "rendered-html" [ property "content" (E.string message.html) ] []
TextBody ->
Html.node "rendered-html" [ property "content" (Encode.string message.text) ] []
Html.node "rendered-html" [ property "content" (E.string message.text) ] []
]
]
@@ -750,3 +760,8 @@ filterMessageList list =
|| String.contains list.searchFilter (String.toLower header.from)
in
List.filter matches list.headers
onKeyUp : (Int -> msg) -> Attribute msg
onKeyUp tagger =
Events.on "keyup" (D.map tagger Events.keyCode)

View File

@@ -36,6 +36,7 @@ type Msg
| MessageReceived D.Value
| Clear
| OpenMessage MessageHeader
| MessageKeyPress MessageHeader Int
update : Msg -> Model -> ( Model, Cmd Msg )
@@ -69,9 +70,22 @@ update msg model =
( { model | messages = [] }, Cmd.none )
OpenMessage header ->
( model
, Route.pushUrl model.session.key (Route.Message header.mailbox header.id)
)
openMessage header model
MessageKeyPress header keyCode ->
case keyCode of
13 ->
openMessage header model
_ ->
( model, Cmd.none )
openMessage : MessageHeader -> Model -> ( Model, Cmd Msg )
openMessage header model =
( model
, Route.pushUrl model.session.key (Route.Message header.mailbox header.id)
)
@@ -121,7 +135,11 @@ view model =
viewMessage : Time.Zone -> MessageHeader -> Html Msg
viewMessage zone message =
tr [ Events.onClick (OpenMessage message) ]
tr
[ tabindex 0
, Events.onClick (OpenMessage message)
, onKeyUp (MessageKeyPress message)
]
[ td [] [ shortDate zone message.date ]
, td [ class "desktop" ] [ text message.from ]
, td [] [ text message.mailbox ]
@@ -147,3 +165,8 @@ shortDate zone date =
zone
date
|> text
onKeyUp : (Int -> msg) -> Attribute msg
onKeyUp tagger =
Events.on "keyup" (D.map tagger Events.keyCode)

View File

@@ -18,6 +18,18 @@
display: flex;
}
.message-list-controls button,
.message-list-controls input[type="search"] {
border: 1px solid var(--border-color);
border-radius: 3px;
}
.message-list-controls button {
color: var(--low-color);
margin-left: 1px;
padding: 0 6px;
}
.message-list-controls input[type="search"] {
flex: 1 1 auto;
padding: 2px 4px;

View File

@@ -15,7 +15,7 @@ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
b, u, i, center, button,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
@@ -44,13 +44,17 @@ body {
background-color: var(--bg-color);
}
body, input, table {
body, button, input, table {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.43;
color: var(--primary-color);
}
button {
background: none;
}
h1, h2, h3, h4, h5, h6, p {
margin-bottom: 10px;
}

View File

@@ -30,29 +30,35 @@
.navbar-toggle {
color: var(--navbar-color);
font-size: 24px;
padding: 0 6px;
position: absolute;
top: 10px;
right: 20px;
right: 14px;
}
.navbar a,
.navbar-dropdown span {
.navbar-dropdown {
color: var(--navbar-color);
display: block;
padding: 15px;
text-decoration: none;
}
/* This takes precendence over .navbar a above */
.navbar-brand a {
display: inline-block;
padding: 12px 15px;
}
.navbar-dropdown-button {
display: none;
}
.navbar li {
color: var(--navbar-color);
}
li.navbar-active .navbar-active-bg {
li.navbar-active > *:first-child {
background-color: #080808;
}
@@ -83,8 +89,7 @@ li.navbar-active span,
}
@media screen and (min-width: 1000px) {
.main-nav,
.navbar-bg {
.main-nav {
height: var(--navbar-height);
}
@@ -110,16 +115,17 @@ li.navbar-active span,
background-image: var(--navbar-image);
grid-column: 1 / 4;
grid-row: 1;
height: var(--navbar-height);
width: 100%;
z-index: -1;
}
.navbar-brand {
margin-left: -15px;
.navbar-toggle {
display: none;
}
.navbar-recent {
margin: 0 auto;
.navbar-brand {
margin-left: -15px;
}
.navbar-mailbox {
@@ -131,6 +137,22 @@ li.navbar-active span,
margin-top: 1px;
}
.navbar-dropdown-container {
margin: 0 auto;
}
.navbar-dropdown {
padding: 15px 19px 15px 25px;
}
.navbar-dropdown-button {
background: none;
border: none;
color: var(--navbar-color);
display: inline;
margin-left: 3px;
}
.navbar-dropdown-content {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
@@ -143,7 +165,7 @@ li.navbar-active span,
z-index: 1;
}
.navbar-dropdown:hover .navbar-dropdown-content {
.navbar-dropdown-container[aria-expanded="true"] .navbar-dropdown-content {
display: block;
}
@@ -156,8 +178,4 @@ li.navbar-active span,
color: var(--primary-color) !important;
background-color: var(--selected-color);
}
.navbar-toggle {
display: none;
}
}