Merge branch 'initial_thoughts_on_frontend' into 'master'

bootstrap-less frontend

Closes #29 and #18

See merge request !10
This commit is contained in:
Felix Hamann 2018-03-18 03:01:09 +01:00
commit 303f2163b0
45 changed files with 1736 additions and 7475 deletions

View File

@ -51,6 +51,8 @@ import Handler.Utils.StudyFeatures
import System.FilePath import System.FilePath
import Handler.Utils.Templates
-- | The foundation datatype for your application. This can be a good place to -- | The foundation datatype for your application. This can be a good place to
-- keep settings and values requiring initialization before your application -- keep settings and values requiring initialization before your application
-- starts running, such as database connections. Every handler will have -- starts running, such as database connections. Every handler will have
@ -80,6 +82,7 @@ mkYesodData "UniWorX" $(parseRoutesFile "routes")
data MenuItem = MenuItem data MenuItem = MenuItem
{ menuItemLabel :: Text { menuItemLabel :: Text
, menuItemIcon :: Maybe Text
, menuItemRoute :: Route UniWorX , menuItemRoute :: Route UniWorX
, menuItemAccessCallback :: Handler Bool , menuItemAccessCallback :: Handler Bool
} }
@ -88,6 +91,7 @@ data MenuTypes
= NavbarLeft { menuItem :: MenuItem } = NavbarLeft { menuItem :: MenuItem }
| NavbarRight { menuItem :: MenuItem } | NavbarRight { menuItem :: MenuItem }
| NavbarExtra { menuItem :: MenuItem } | NavbarExtra { menuItem :: MenuItem }
| NavbarSecondary { menuItem :: MenuItem }
-- | A convenient synonym for creating forms. -- | A convenient synonym for creating forms.
type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget) type Form x = Html -> MForm (HandlerT UniWorX IO) (FormResult x, Widget)
@ -275,33 +279,39 @@ instance YesodBreadcrumbs UniWorX where
defaultLinks :: [MenuTypes] defaultLinks :: [MenuTypes]
defaultLinks = -- Define the menu items of the header. defaultLinks = -- Define the menu items of the header.
[ NavbarLeft $ MenuItem [ NavbarRight $ MenuItem
{ menuItemLabel = "Home" { menuItemLabel = "Home"
, menuItemIcon = Just "home"
, menuItemRoute = HomeR , menuItemRoute = HomeR
, menuItemAccessCallback = return True , menuItemAccessCallback = return True
} }
, NavbarLeft $ MenuItem , NavbarLeft $ MenuItem
{ menuItemLabel = "Kurse" { menuItemLabel = "Kurse"
, menuItemIcon = Just "book"
, menuItemRoute = CourseListR , menuItemRoute = CourseListR
, menuItemAccessCallback = return True , menuItemAccessCallback = return True
} }
, NavbarRight $ MenuItem , NavbarLeft $ MenuItem
{ menuItemLabel = "Users" { menuItemLabel = "Users"
, menuItemIcon = Just "user"
, menuItemRoute = UsersR , menuItemRoute = UsersR
, menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False , menuItemAccessCallback = return True -- Creates a LOOP: (Authorized ==) <$> isAuthorized UsersR False
} }
, NavbarRight $ MenuItem , NavbarRight $ MenuItem
{ menuItemLabel = "Profile" { menuItemLabel = "Profile"
, menuItemIcon = Just "user"
, menuItemRoute = ProfileR , menuItemRoute = ProfileR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback = isJust <$> maybeAuthPair
} }
, NavbarRight $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Login" { menuItemLabel = "Login"
, menuItemIcon = Just "login"
, menuItemRoute = AuthR LoginR , menuItemRoute = AuthR LoginR
, menuItemAccessCallback = isNothing <$> maybeAuthPair , menuItemAccessCallback = isNothing <$> maybeAuthPair
} }
, NavbarRight $ MenuItem , NavbarSecondary $ MenuItem
{ menuItemLabel = "Logout" { menuItemLabel = "Logout"
, menuItemIcon = Just "logout"
, menuItemRoute = AuthR LogoutR , menuItemRoute = AuthR LogoutR
, menuItemAccessCallback = isJust <$> maybeAuthPair , menuItemAccessCallback = isJust <$> maybeAuthPair
} }
@ -328,11 +338,23 @@ defaultMenuLayout menu widget = do
-- value passed to hamletToRepHtml cannot be a widget, this allows -- value passed to hamletToRepHtml cannot be a widget, this allows
-- you to use normal widget features in default-layout. -- you to use normal widget features in default-layout.
pc <- widgetToPageContent $ do let
addStylesheet $ StaticR css_bootstrap_css navbar :: Widget
$(widgetFile "default-layout") navbar = $(widgetFile "widgets/navbar")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
breadcrumbs :: Widget
breadcrumbs = $(widgetFile "widgets/breadcrumbs")
pc <- widgetToPageContent $ do
addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900"
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_icons_css
$(widgetFile "default-layout")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/sortable")
$(widgetFile "standalone/inputs")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
-- How to run database actions. -- How to run database actions.
instance YesodPersist UniWorX where instance YesodPersist UniWorX where

View File

@ -66,15 +66,16 @@ getCourseListTermR tidini = do
let pageLinks = let pageLinks =
[ NavbarLeft $ MenuItem [ NavbarLeft $ MenuItem
{ menuItemLabel = "Neuer Kurs" { menuItemLabel = "Neuer Kurs"
, menuItemIcon = Just "book"
, menuItemRoute = CourseEditR , menuItemRoute = CourseEditR
, menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False , menuItemAccessCallback = (== Authorized) <$> isAuthorized CourseEditR False
} }
] ]
let coursesTable = encodeWidgetTable tableSortable colonnadeTerms courses
defaultLinkLayout pageLinks $ do defaultLinkLayout pageLinks $ do
-- defaultLayout $ do -- defaultLayout $ do
setTitle "Semesterkurse" setTitle "Semesterkurse"
linkButton "Neuen Kurs anlegen" BCPrimary CourseEditR $(widgetFile "courses")
encodeWidgetTable tableDefault colonnadeTerms courses -- (map entityVal courses)
getCourseShowR :: TermId -> Text -> Handler Html getCourseShowR :: TermId -> Text -> Handler Html
getCourseShowR tid csh = do getCourseShowR tid csh = do
@ -324,5 +325,3 @@ validateCourse (CourseForm{..}) =
, "Anmeldungen aktivieren oder Anmeldezeitraum löschen" , "Anmeldungen aktivieren oder Anmeldezeitraum löschen"
) )
] ] ] ]

View File

@ -55,4 +55,3 @@ postHomeR = do
(FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt" (FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt"
_other -> return () _other -> return ()
getHomeR getHomeR

View File

@ -79,7 +79,6 @@ getTermShowR = do
setTitle "Freigeschaltete Semester" setTitle "Freigeschaltete Semester"
table table
getTermEditR :: Handler Html getTermEditR :: Handler Html
getTermEditR = do getTermEditR = do
-- TODO: Defaults für Semester hier ermitteln und übergeben -- TODO: Defaults für Semester hier ermitteln und übergeben

View File

@ -41,5 +41,5 @@ getUsersR = do
-- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools -- ++ map (\school -> headed (text2widget $ schoolName $ entityVal school) (\u -> "xx")) schools
defaultLayout $ do defaultLayout $ do
setTitle "Comprehensive User List" setTitle "Comprehensive User List"
let userList = encodeWidgetTable tableDefault colonnadeUsers users let userList = encodeWidgetTable tableSortable colonnadeUsers users
$(widgetFile "users") $(widgetFile "users")

View File

@ -18,6 +18,7 @@ import Handler.Utils.Table.Pagination as Handler.Utils
import Handler.Utils.Zip as Handler.Utils import Handler.Utils.Zip as Handler.Utils
import Handler.Utils.Rating as Handler.Utils import Handler.Utils.Rating as Handler.Utils
import Handler.Utils.Submission as Handler.Utils import Handler.Utils.Submission as Handler.Utils
import Handler.Utils.Templates as Handler.Utils
import Text.Blaze (Markup, ToMarkup) import Text.Blaze (Markup, ToMarkup)

View File

@ -48,7 +48,7 @@ data FormLayout = FormStandard
renderAForm :: Monad m => FormLayout -> FormRender m a renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do renderAForm formLayout aform fragment = do
(res, (($ []) -> views)) <- aFormToForm aform (res, (($ []) -> views)) <- aFormToForm aform
let widget = $(widgetFile "form") let widget = $(widgetFile "widgets/form")
return (res, widget) return (res, widget)
---------------------------- ----------------------------
@ -344,4 +344,3 @@ setTooltip :: String -> FieldSettings site -> FieldSettings site
setTooltip tt fs setTooltip tt fs
| null tt = fs { fsTooltip = Nothing } | null tt = fs { fsTooltip = Nothing }
| otherwise = fs { fsTooltip = Just $ fromString tt } | otherwise = fs { fsTooltip = Just $ fromString tt }

View File

@ -25,6 +25,9 @@ import Data.Either
tableDefault :: Attribute tableDefault :: Attribute
tableDefault = customAttribute "class" "table table-striped table-hover" tableDefault = customAttribute "class" "table table-striped table-hover"
tableSortable :: Attribute
tableSortable = customAttribute "class" "js-sortable"
-- Colonnade Tools -- Colonnade Tools
numberColonnade :: (IsString c) => Colonnade Headed Int c numberColonnade :: (IsString c) => Colonnade Headed Int c
numberColonnade = headed "Nr" (fromString.show) numberColonnade = headed "Nr" (fromString.show)

View File

@ -0,0 +1,8 @@
{-# LANGUAGE NoImplicitPrelude, TemplateHaskell #-}
module Handler.Utils.Templates where
import Import.NoFoundation
lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum")

View File

@ -16,3 +16,6 @@ import Data.Fixed as Import
import CryptoID as Import import CryptoID as Import
import Data.UUID as Import (UUID) import Data.UUID as Import (UUID)
import Text.Lucius as Import

7022
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

9
static/css/fonts.css Normal file
View File

@ -0,0 +1,9 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('../fonts/glyphicons-halflings-regular.woff') format('woff'),
url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}

30
static/css/icons.css Normal file
View File

@ -0,0 +1,30 @@
.glyphicon {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
}
.glyphicon::before {
position: absolute;
left: 4px;
margin: 0 13px;
font-family: 'Glyphicons Halflings';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon--home::before {
content: '\e021';
}
.glyphicon--book::before {
content: '\e043';
}
.glyphicon--user::before {
content: '\e008';
}
.glyphicon--login::before {
content: '\e161';
}
.glyphicon--logout::before {
content: '\e163';
}

View File

@ -1,38 +1,34 @@
<div .masthead> <div .course-header>
<div .container> <div .course-header__info>
<div .row> <table>
<h1 .header> <tr>
#{courseName course} <th>Teilnehmer
$maybe school <- schoolMB <td>
<h4> #{participants}
#{schoolName school} $maybe capacity <- courseCapacity course
\ von #{capacity}
<tr>
<th>Anmeldezeitraum
<td>
$maybe regFrom <- courseRegisterFrom course
#{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<div>
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<div .course-header__title>
<h1>#{courseName course}
$maybe school <- schoolMB
<h4>#{schoolName school}
<div .container> <div .container>
<div .bs-docs-section> $maybe descr <- courseDescription course
<div .row> <h2 #description>Beschreibung
<div .col-lg-12> <p> #{descr}
<div .page-header> $maybe link <- courseLinkExternal course
<h4 #linl>Homepage
$maybe descr <- courseDescription course <a href=#{link}>#{link}
<h2 #description>Beschreibung
<p> #{descr}
$maybe link <- courseLinkExternal course
<h4 #linl>Homepage
<a href=#{link}>#{link}
<div .row>
<div .col-lg-12>
<h4>Teilnehmer
#{participants}
$maybe capacity <- courseCapacity course
\ von #{capacity}
<br>
$maybe regFrom <- courseRegisterFrom course
Anmeldezeitraum: #{formatTimeGerWD regFrom}
$maybe regTo <- courseRegisterTo course
\ bis #{formatTimeGerWD regTo}
<form method=post action=@{CourseShowR tid csh} enctype=#{regEnctype}>
^{regWidget}
<hr>

19
templates/course.lucius Normal file
View File

@ -0,0 +1,19 @@
.course-header {
/*display: flex;
flex-direction: row;
justify-content: space-between;*/
}
.course-header__title {
align-self: baseline;
}
.course-header__info {
border: 1px solid var(--greybase);
padding: 13px;
align-self: center;
float: right;
}
.course-header__info table {
margin: 0;
}

6
templates/courses.hamlet Normal file
View File

@ -0,0 +1,6 @@
<div .container>
<h1>Kursübersicht für Semester #{termToText $ unTermKey tidini}
^{coursesTable}
<div .container>
<a href=@{CourseEditR}>Neuen Kurs anlegen

View File

@ -19,12 +19,7 @@ $newline never
\<!--[if lt IE 9]> \<!--[if lt IE 9]>
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script> \<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
\<![endif]--> \<![endif]-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js"> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js">
\<!-- Bootstrap-3.3.7 compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">
<script> <script>
/* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */ /* The `defaultCsrfMiddleware` Middleware added in Foundation.hs adds a CSRF token to the request cookies. */
/* AJAX requests should add that token to a header to be validated by the server. */ /* AJAX requests should add that token to a header to be validated by the server. */
@ -43,8 +38,6 @@ $newline never
\ }); \ });
} }
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/,'js');
<body> <body>
^{pageBody pc} ^{pageBody pc}

View File

@ -1,60 +1,17 @@
<!-- navigation -->
^{navbar}
<!-- Static navbar --> <div .main>
<nav .navbar.navbar-default.navbar-static-top>
<div .container>
<div .navbar-header>
<button type="button" .navbar-toggle.collapsed data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<div #navbar .collapse.navbar-collapse> <!-- secondary navigation at the side -->
<ul .nav.navbar-nav> ^{asidenav}
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of NavbarExtra (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<ul .nav.navbar-nav.navbar-right> <div .main__content>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label route _)
<li :Just route == mcurrentRoute:.active>
<a href=@{route}>#{label}
$of _
<!-- Page Contents -->
<div .container>
$if not $ Just HomeR == mcurrentRoute
<ul .breadcrumb>
$forall bc <- parents
<li>
<a href="@{fst bc}">#{snd bc}
<li .active>#{title}
<!-- alerts -->
$forall (status, msg) <- mmsgs $forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "") $with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">#{msg} <div class="alert alert-#{status2}">#{msg}
<!-- actual content -->
$if (Just HomeR == mcurrentRoute)
^{widget} ^{widget}
$else
<div .container>
<div .row>
<div .col-md-12>
^{widget}
<!-- Footer -->
<footer .footer>
<div .container>
<p .text-muted>
#{appCopyright $ appSettings master}

View File

@ -1,73 +1,200 @@
.masthead, :root {
.navbar { /* THEME 1 */
background-color: rgb(27, 28, 29); --base00: #72a85b;
} --base-bg-color: #1d1c1d;
--base-font-color: #fff;
.navbar-default .navbar-nav > .active > a { --sec-font-color: #fff;
background-color: transparent; --box-bg-color: #3c3c3c;
border-bottom: 2px solid white; /* THEME 2 */
} --base00: #38428a;
--base-bg-color: #ffffff;
.navbar-nav { --base-font-color: rgb(53, 53, 53);
padding-bottom: 1em; --sec-font-color: #eaf2ff;
} --box-bg-color: #dddddd;
/* THEME 3 */
.masthead { --darkbase: #364B60;
margin-top: -21px; --lightbase: #2490E8;
color: white; --lighterbase: #60C2FF;
text-align: center; --whitebase: #FCFFFA;
min-height: 500px; --greybase: #B1B5C0;
} --fontbase: #34303a;
--fontsec: #5b5861;
.masthead .header { /* THEME 4 */
max-width: 700px; --darkbase: #263C4C;
margin: 0 auto; --lightbase: #598EB5;
font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; --lighterbase: #5F98C2;
} --whitebase: #FCFFFA;
--greybase: #B1B5C0;
.masthead h1.header { --blackbase: #1A2A36;
margin-top: 1em; --fontbase: #34303a;
margin-bottom: 0em; --fontsec: #5b5861;
font-size: 4.5em; --primarybase: #4C7A9C;
line-height: 1.2em;
font-weight: normal;
}
.masthead h2 {
font-size: 1.7em;
font-weight: normal;
}
.masthead .btn {
margin: 1em 0;
}
/* Common styles for all types */ /* THEME INDEPENDENT COLORS */
.bs-callout { --errorbase: red;
padding: 20px; --warningbase: #fe7700;
margin: 20px 0; --validbase: #2dcc35;
border: 1px solid #eee; --infobase: var(--darkbase);
border-left-width: 5px;
border-radius: 3px;
/* FONTS */
--fontfamilybase: "Source Sans Pro", Helvetica, sans-serif;
/* DIMENSIONS */
--header-height: 80px;
--header-height-collapsed: 50px;
} }
.bs-callout p:last-child { * {
margin-bottom: 0; box-sizing: border-box;
padding: 0;
margin: 0;
} }
.bs-callout-info { body {
border-left-color: #1b809e; background-color: white;
color: var(--fontbase);
font-family: var(--fontfamilybase);
font-weight: 400;
font-size: 16px;
overflow-y: scroll;
} }
/* Space things out */ a,
.bs-docs-section { a:visited {
margin-bottom: 60px; color: var(--darkbase);
text-decoration: none;
font-weight: 600;
transition: color .2s ease, background-color .2s ease;
} }
.bs-docs-section:last-child { a:hover {
margin-bottom: 0; color: var(--lightbase);
} }
#message { ul {
margin-bottom: 40px; list-style-type: none;
}
.list--inline > li {
display: inline-block;
}
h1, h2, h3, h4, h5 {
font-weight: 600;
}
h1 {
font-size: 32px;
margin: 20px 0 10px;
}
h2 {
font-size: 24px;
margin: 10px 0 5px;
}
h3 {
font-size: 20px;
margin: 5px 0;
}
h4 {
font-size: 16px;
margin: 0;
}
table {
margin: 21px 0;
/*width: 100%;*/
}
th, td {
text-align: left;
padding: 0 13px 0 7px;
vertical-align: baseline;
}
th:first-child,
td:first-child {
padding-left: 0;
border-left: 0;
}
th {
border-left: 2px solid var(--greybase);
}
/* LAYOUT */
.main {
display: flex;
padding-right: 5vw;
min-height: calc(100vh - var(--header-height));
}
.main__content {
position: relative;
background-color: white;
padding: 0 40px;
padding-right: 0;
flex: 1;
z-index: 0;
> .container {
margin: 20px 0;
}
p {
margin: 10px 0;
}
}
.pseudo-focus {
outline: 5px auto var(--lightbase);
outline: 5px auto -webkit-focus-ring-color;
}
/* GENERAL BUTTON STYLES */
input[type="submit"],
input[type="button"],
button,
.btn {
outline: 0;
border: 0;
box-shadow: 0;
background-color: var(--lightbase);
color: white;
padding: 10px 17px;
min-width: 100px;
transition: all .1s;
font-size: 16px;
cursor: pointer;
border-radius: 4px;
display: inline-block;
}
input.btn-primary,
button.btn-primary,
.btn.btn-primary {
background-color: var(--primarybase);
}
input.btn-info,
button.btn-info,
.btn.btn-info {
background-color: var(--infobase)
}
input[type="submit"][disabled],
input[type="button"][disabled],
button[disabled],
.btn[disabled] {
opacity: 0.3;
background-color: var(--greybase);
cursor: default;
}
input[type="submit"]:not([disabled]):hover,
input[type="button"]:not([disabled]):hover,
button:not([disabled]):hover,
.btn:not([disabled]):hover {
background-color: var(--lighterbase);
text-decoration: underline;
}
input[type="submit"].btn-info:hover,
input[type="button"].btn-info:hover,
button.btn-info:hover,
.btn.btn-info:hover {
background-color: var(--greybase)
} }

View File

@ -1,9 +0,0 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional :isJust $ fvErrors view:.has-error>
$if not (Blaze.null $ fvLabel view)
<label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -1,82 +1,69 @@
<div .masthead>
<div .container>
<div .row>
<h1 .header>
ReWorX - Demo
<h3>
Testumgebung für die Re-Implementierung von
<a href="https://uniworx.ifi.lmu.de/">
UniWorX
<div .container> <div .container>
<!-- Starting <h1>ReWorX - Demo
================================================== --> <h3>
<div .bs-docs-section> Testumgebung für die Re-Implementierung von <a href="https://uniworx.ifi.lmu.de/">UniWorX</a>
<div .row> <p>
<div .col-lg-12> Die Reimplementierung von
<div .page-header> UniWorX ist noch nicht abgeschlossen.
<h2 #start>Übersicht
<p> <p .alert .alert-danger>Das System ist noch nicht produktiv einsetzbar
Die Reimplementierung von
UniWorX ist noch nicht abgeschlossen.
<div .alert .alert-danger> <hr>
Das System ist noch nicht produktiv <div .container>
einsetzbar <h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
<div .bs-docs-section> <ul>
<div .row> <li .list-group-item>
<div .col-lg-12> <a href=@{UsersR}>Benutzer Verwaltung
<div .page-header>
<h2 #design>Design
<p> <li .list-group-item>
Wir konzentrieren uns derzeit <a href=@{TermShowR}>Semester Verwaltung
ausschließlich auf die Funktionalität. <a href=@{TermEditR}>Neues Semester anlegen
<p>
Insbesondere Formulare zeigen
alle Eingabefelder und Knöpfe
ohne eine gezielte Anordnung
und Reihenfolge.
Dies läßt sich leicht nachträglich einstellen.
<p>
Momentan werden noch keine speziellen Grafiken oder CSS verwendet;
sondern nur gewöhnliches Bootstrap3.
<div .bs-docs-section> <li .list-group-item>
<div .row> <a href=@{CourseEditR}>Kurse anlegen, editieren und anzeigen
<div .col-lg-12>
<div .page-header>
<h3 #design>Teilweise funktionierende Abschnitte
<ul .list-group> <li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<li .list-group-item> <hr>
<a href=@{UsersR}>Benutzer Verwaltung <div .container>
<h2 .js-show-hide__toggle data-collapsed=true>Tabellen
<table .js-sortable>
<thead>
<tr>
<th .sorted-asc>ID
<th>TH1
<th>TH2
<th>TH3
<tbody>
<tr>
<td>0
<td>14
<td>CON2
<td>3
<tr>
<td>1
<td>5
<td>ONT2
<td>13
<tr>
<td>2
<td>CONT1
<td>NT2
<td>43
<tr>
<td>3
<td>43
<td>T2C2
<td>35
<li .list-group-item> <hr>
<a href=@{TermShowR}>Semester Verwaltung <div .container>
<a href=@{TermEditR}>Neues Semester anlegen <h2>Funktionen zum Testen
<li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen
editieren und anzeigen
<li .list-group-item>
<a href=@{SubmissionListR}>Dateien hochladen und abrufen
<h3 #design>Funktionen zum Testen
<ul .list-group>
<li .list-group-item>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}
<li .list-group-item>
<a href=@{CourseEditR}>Kurse anlegen
editieren und anzeigen
<ul>
<li>
Knopf-Test:
<form .form-inline method=post action=@{HomeR} enctype=#{btnEnctype}>
^{btnWdgt}

0
templates/home.julius Normal file
View File

View File

@ -0,0 +1 @@
<!-- only here to be able to include inputs using `toWidget` -->

View File

@ -0,0 +1,177 @@
(function() {
'use strict';
window.utils = window.utils || {};
// makes <label> smaller if <input> is focussed
window.utils.reactiveInputLabel = function(input, label) {
// updates to dom
if (input.value.length > 0) {
label.classList.add('reactive-label--small');
}
// add event listeners
input.addEventListener('focus', function() {
label.classList.add('reactive-label--small');
});
label.addEventListener('click', function() {
label.classList.add('reactive-label--small');
input.focus();
});
input.addEventListener('blur', function() {
if (input.value.length < 1) {
label.classList.remove('reactive-label--small');
}
});
};
window.utils.reactiveFileUpload = function(input, parent) {
var currValidInputCount = 0;
var addMore = false;
var inputName = input.getAttribute('name');
// FileInput PseudoClass
function FileInput(container, input, label, remover) {
this.container = container;
this.input = input;
this.label = label;
this.remover = remover;
addListener(this);
this.addTo = function(parentElement) {
parentElement.appendChild(this.container);
}
this.remove = function() {
this.container.remove();
}
this.isValid = function() {
return this.container.classList.contains('file-input__container--valid');
}
}
function addNextInput() {
var inputs = parent.querySelectorAll('.file-input__container');
if (inputs[inputs.length - 1].classList.contains('file-input__container--valid')) {
makeInput(inputName).addTo(parent);
}
}
// updates submitbutton and form-group-stripe
function updateForm() {
var submitBtn = parent.parentElement.querySelector('[type=submit]');
parent.classList.remove('form-group--has-error');
if (currValidInputCount > 0) {
if (parent.classList.contains('form-group')) {
parent.classList.add('form-group--valid')
}
submitBtn.removeAttribute('disabled');
addNextInput();
} else {
if (parent.classList.contains('form-group')) {
parent.classList.remove('form-group--valid')
}
submitBtn.setAttribute('disabled', 'disabled');
}
}
// addseventlistener destInput
function addListener(fileInput) {
fileInput.input.addEventListener('change', function(event) {
if (fileInput.input.value.length > 0) {
// update label
var filePath = fileInput.input.value.replace(/\\/g, '/').split('/');
var fileName = filePath[filePath.length - 1];
fileInput.label.innerHTML = fileName;
// increase count if this field was empty previously
if (!fileInput.isValid()) {
currValidInputCount++;
}
fileInput.container.classList.add('file-input__container--valid')
// show next input
} else {
currValidInputCount--;
fileInput.remove();
}
updateForm();
});
fileInput.input.addEventListener('focus', function() {
fileInput.container.classList.add('pseudo-focus');
});
fileInput.input.addEventListener('blur', function() {
fileInput.container.classList.remove('pseudo-focus');
});
fileInput.label.addEventListener('click', function() {
fileInput.input.click();
});
fileInput.remover.addEventListener('click', function() {
if (fileInput.isValid()) {
currValidInputCount--;
}
fileInput.remove();
updateForm();
});
}
// create new wrapped input element with name name
function makeInput(name) {
var cont = document.createElement('div');
var desc = document.createElement('span');
var nextInput = document.createElement('input');
var remover = document.createElement('div');
cont.classList.add('file-input__container');
desc.classList.add('file-input__label', 'btn');
remover.classList.add('file-input__remover');
nextInput.setAttribute('name', name);
nextInput.setAttribute('type', 'file');
cont.appendChild(nextInput);
cont.appendChild(desc);
cont.appendChild(remover);
return new FileInput(cont, nextInput, desc, remover);
}
// initial setup
function setup() {
var newInput = makeInput(inputName);
input.remove();
newInput.addTo(parent);
updateForm();
}
setup();
}
window.utils.reactiveFormGroup = function(formGroup, input) {
// updates to dom
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
input.addEventListener('input', function() {
formGroup.classList.remove('form-group--has-error');
if (input.value.length > 0) {
formGroup.classList.add('form-group--valid');
} else {
formGroup.classList.remove('form-group--valid');
}
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
// setup reactive labels
Array.from(document.querySelectorAll('.reactive-label')).forEach(function(label) {
var input = document.querySelector('#' + label.getAttribute('for'));
var parent = label.parentElement;
var type = input.getAttribute('type');
var isFileInput = /file/i.test(type);
var isListening = !RegExp(['date', 'checkbox', 'radio', 'hidden', 'file'].join('|')).test(type);
var isInFormGroup = parent.classList.contains('form-group') && parent.classList.contains('form-group--required');
if (isInFormGroup) {
window.utils.reactiveFormGroup(parent, input);
}
if (isFileInput) {
window.utils.reactiveFileUpload(input, parent);
}
if (isListening) {
window.utils.reactiveInputLabel(input, label);
} else {
label.classList.remove('reactive-label');
}
});
});

View File

@ -0,0 +1,306 @@
/* GENERAL STYLES FOR FORMS */
form {
margin: 20px 0;
}
/* TEXT INPUTS */
input[type="text"],
input[type="password"],
input[type="url"],
input[type="number"],
input[type="email"] {
background-color: rgba(0, 0, 0, 0.05);
padding: 7px 3px 7px;
outline: 0;
border: 0;
border-bottom: 2px solid var(--darkbase);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
color: var(--fontbase);
transition: all .1s;
font-size: 16px;
min-width: 300px;
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="email"]:focus {
border-bottom-color: var(--lightbase);
background-color: transparent;
}
/* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */
textarea {
outline: 0;
border: 0;
padding: 7px 4px;
min-width: 300px;
min-height: 100px;
font-family: var(--fontfamilybase);
font-size: 16px;
color: var(--fontbase);
background-color: rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 13px rgba(0, 0, 0, 0.05);
border-bottom: 2px solid var(--darkbase);
}
textarea:focus {
background-color: transparent;
border-bottom-color: var(--lightbase);
}
/* FORM GROUPS */
.form-group {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(150px, max-content));
grid-auto-columns: minmax(150px, max-content);
grid-gap: 5px;
align-items: center;
margin: 10px 0;
padding-left: 10px;
border-left: 8px solid transparent;
}
.form-group--required {
border-left: 8px solid var(--lighterbase);
}
.form-group--valid {
border-left: 8px solid var(--validbase);
}
.form-group--has-error {
border-left: 8px solid var(--errorbase) !important;
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
position: relative;
height: 20px;
width: 20px;
-webkit-appearance: none;
cursor: pointer;
}
input[type="checkbox"]::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background-color: var(--lighterbase);
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
input[type="checkbox"]:checked::before {
background-color: var(--lightbase);
}
input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
/* CUSTOM CHECKBOXES AND RADIO BOXES */
/* Completely replaces legacy checkbox and radiobox */
.checkbox,
.radio {
position: relative;
margin: 3px;
> [type="checkbox"],
> [type="radio"] {
display: none;
}
> label {
display: block;
padding: 7px 13px 7px 30px;
background-color: var(--darkbase);
border-radius: 4px;
color: white;
cursor: pointer;
}
> label::before,
> label::after {
content: '';
position: absolute;
top: 15px;
left: 4px;
display: block;
width: 20px;
height: 20px;
background-color: white;
transition: all .2s;
}
> label::before {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> label::after {
width: 20px;
height: 2px;
transform: scale(0.8, 0.1);
}
> :checked + label {
background-color: var(--lightbase);
text-decoration: underline;
}
&:hover > label::before,
> :checked + label::before {
transform: scale(1, 1) rotate(45deg);
}
&:hover > label::after,
> :checked + label::after {
transform: scale(1, 1) rotate(-45deg);
}
}
.radio > label::before {
transform: scale(0.01, 0.01) rotate(45deg);
}
.radio > label::after {
transform: scale(0.01, 0.01) rotate(-45deg);
}
.radio::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
border: 2px solid white;
z-index: -1;
}
/* REACTIVE LABELS */
.reactive-label {
cursor: text;
color: var(--fontsec);
transform: translate(0, 0);
transition: all .1s;
}
.reactive-label--small {
cursor: default;
color: var(--fontbase);
}
@media (max-width: 999px) {
.form-group {
grid-template-rows: 30px;
grid-template-columns: 1fr;
align-items: baseline;
margin-top: 17px;
}
.reactive-label {
position: relative;
transform: translate(2px, 30px);
}
.reactive-label--small {
transform: translate(2px, 0px);
color: var(--fontsec);
/*font-size: 14px;*/
}
}
/* CUSTOM FILE INPUT */
input[type="file"] {
color: white;
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
outline: 0;
border: 0;
}
.file-input__container {
grid-column-start: 2;
display: flex;
justify-content: space-between;
}
.file-input__label,
.file-input__remover {
display: block;
border-radius: 2px;
padding: 5px 13px;
color: var(--whitebase);
cursor: pointer;
}
.file-input__label {
text-align: left;
position: relative;
height: 30px;
}
.file-input__label.btn {
padding: 5px 13px;
}
.file-input__label::after,
.file-input__label::before {
position: absolute;
content: '';
background-color: white;
width: 16px;
height: 2px;
top: 14px;
top: 50%;
left: 12px;
left: 50%;
}
.file-input__label::after {
transform: translate(-50%, -50%) rotate(90deg);
}
.file-input__label::before {
transform: translate(-50%, -50%);
}
.file-input__remover {
display: none;
width: 40px;
height: 30px;
text-align: center;
background-color: var(--warningbase);
position: relative;
margin-left: 10px;
}
.file-input__remover::before {
position: absolute;
content: '';
width: 16px;
height: 2px;
top: 14px;
left: 12px;
background-color: white;
}
.file-input__container--valid > .file-input__label {
background-color: var(--lightbase);
}
.file-input__container--valid > .file-input__label::before,
.file-input__container--valid > .file-input__label::after {
content: none;
}
.file-input__container--valid > .file-input__remover {
display: block;
}
@media (max-width: 999px) {
.file-input__container {
grid-column-start: 1;
}
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include showHide using `toWidget` -->

View File

@ -0,0 +1,59 @@
/**
* div.js-show-hide
* div.js-show-hide-toggle
* toggle here
* div
* content here
*/
document.addEventListener('DOMContentLoaded', function() {
var elements = Array.from(document.querySelectorAll('.js-show-hide__toggle')),
toggles = [],
path = new URL(window.location.href).pathname;
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var toggle = toggles[el.dataset.index];
toggle.collapsed = !toggle.collapsed;
toggle.parent.classList.toggle('js-show-hide--collapsed', toggle.collapsed);
updateLocalStorage();
});
}
function updateLocalStorage(id) {
var lsData = getStateFromLocalStorage();
lsData[path] = toggles.map(t => {
return {id: t.index, collapsed: t.collapsed};
});
window.localStorage.setItem('showHidesToggles', JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(id, fallBack) {
var lsData = getStateFromLocalStorage();
if (lsData[path] && lsData[path][id] && lsData[path][id].id === id) {
return lsData[path][id].collapsed;
}
return fallBack;
}
function getStateFromLocalStorage() {
return JSON.parse(window.localStorage.getItem('showHidesToggles')) || {};
}
elements.forEach(function(el, i) {
el.dataset.index = i;
var coll = collapsedStateInLocalStorage(i, el.dataset.collapsed === 'true');
if (coll) {
el.parentElement.classList.add('js-show-hide--collapsed')
}
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
toggles.push({index: i, collapsed: coll, parent: el.parentElement});
addEventHandler(el);
});
});

View File

@ -0,0 +1,44 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
}
.js-show-hide__toggle:hover {
cursor: pointer;
}
.js-show-hide__toggle::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-right: 8px solid transparent;
border-bottom: 8px solid transparent;
}
.js-show-hide__toggle::before,
.js-show-hide--collapsed .js-show-hide__toggle:hover::before {
left: -28px;
top: 10px;
border-left: 8px solid transparent;
border-top: 8px solid var(--lightbase);
}
.js-show-hide__toggle:hover::before,
.js-show-hide--collapsed .js-show-hide__toggle::before {
border-left: 8px solid var(--lightbase);
border-top: 8px solid transparent;
top: 5px;
left: -22px;
}
.js-show-hide__target {
}
.js-show-hide--collapsed > .js-show-hide__target {
display: none;
}

View File

@ -0,0 +1 @@
<!-- only here to be able to include sortable using `toWidget` -->

View File

@ -0,0 +1,88 @@
/**
* delcare a table as sortable by adding class 'js-sortable'
*/
document.addEventListener('DOMContentLoaded', function() {
var tables = [];
var ASC = 1;
var DESC = -1;
function initTable(table, tableIndex) {
var trs = table.querySelectorAll('tr');
var ths = table.querySelectorAll('th');
var trContents = [];
Array.from(trs).forEach(function(tr, rowIndex) {
if (rowIndex === 0) {
// register table headers as sort-listener
Array.from(tr.querySelectorAll('th')).forEach(function(th, thIndex) {
th.addEventListener('click', function(el) {
sortTableBy(tableIndex, thIndex);
});
});
} else {
// register table rows
trContents.push(Array.from(tr.querySelectorAll('td')).map(function(td) {
return td.innerHTML;
}));
}
});
tables.push({
el: table,
ths: ths,
sortBy: 0,
sortDir: ASC,
trContents,
});
}
function updateThs(tableIndex, thIndex, sortOrder) {
Array.from(tables[tableIndex].ths).forEach(function (th) {
th.classList.remove('sorted-asc', 'sorted-desc');
});
var suffix = sortOrder > 0 ? 'asc' : 'desc';
tables[tableIndex].ths[thIndex].classList.add('sorted-' + suffix);
}
function sortTableBy(tableIndex, thIndex) {
var table = tables[tableIndex];
var sortKey = thIndex;
var sortOrder = ASC;
if (table.sortBy === sortKey) {
sortOrder = table.sortDir === ASC ? DESC : ASC;
}
table.trContents.sort(dynamicSort(sortKey, sortOrder));
tables[tableIndex].sortBy = thIndex;
tables[tableIndex].sortDir = sortOrder;
updateThs(tableIndex, thIndex, sortOrder);
Array.from(table.el.querySelectorAll('tr')).forEach(function(tr, trIndex) {
if (trIndex > 0) {
Array.from(tr.querySelectorAll('td')).forEach(function (td, tdIndex) {
td.innerHTML = table.trContents[trIndex - 1][tdIndex];
});
}
});
}
function dynamicSort(key, order) {
return function (a,b) {
var aVal = parseInt(a[key]);
var bVal = parseInt(b[key]);
if ((isNaN(aVal) && !isNaN(bVal)) || (!isNaN(aVal) && isNaN(bVal))) {
console.error('trying to sort table by row with mixed content: "%s", "%s"', a[key], b[key]);
}
aVal = isNaN(aVal) ? a[key] : aVal;
bVal = isNaN(bVal) ? b[key] : bVal;
var result = (aVal < bVal) ? -1 : (aVal > bVal) ? 1 : 0;
return result * order;
}
}
var rawTables = document.querySelectorAll('.js-sortable');
Array.from(rawTables).forEach(function(table, i) {
initTable(table, i);
});
});

View File

@ -0,0 +1,31 @@
table.js-sortable th {
cursor: pointer;
position: relative;
padding-right: 20px;
}
table.js-sortable th.sorted-asc,
table.js-sortable th.sorted-desc {
color: var(--darkbase);
}
table.js-sortable th.sorted-asc::after,
table.js-sortable th.sorted-desc::after {
content: '';
position: absolute;
right: 0;
top: 15px;
width: 0;
height: 0;
transform: translateY(-100%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
table.js-sortable th.sorted-asc::after {
border-top: 8px solid var(--lightbase);
}
table.js-sortable th.sorted-desc::after {
border-bottom: 8px solid var(--lightbase);
}

View File

@ -1,6 +1,8 @@
<form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}> <div .container>
^{uploadWidget} <form method=POST enctype=#{uploadEnctype} action=@{SubmissionListR}>
^{uploadWidget}
<form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}> <div .container>
^{subTable} <form method=POST enctype=#{selectEncoding} target=_blank action=@{SubmissionDownloadMultiArchiveR}>
<button .btn .btn-default type=submit >Markierte herunterladen ^{subTable}
<button .btn .btn-default type=submit >Markierte herunterladen

View File

@ -0,0 +1,20 @@
<aside .main__aside>
<div .asidenav>
<div .asidenav__box--dont-hide>
<ul .asidenav__list>
$forall menuType <- menuTypes
$case menuType
$of NavbarLeft (MenuItem label mIcon route _)
<li .asidenav__list-item :Just route == mcurrentRoute:.asidenav__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .asidenav__link href=@{route}>#{label}
$of _
<div .asidenav__box>
<h3 .asidenav__box-title>WiSe 17/18
<ul .asidenav__list.asidenav__list--padded>
<li>Vorlesung 1
<li>Vorlesung 2
<li>Vorlesung 3
<li>Vorlesung 4

View File

@ -0,0 +1,55 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.aside = function(el) {
var asideEl = el;
var collapsed = false;
var collClass = 'main__aside--collapsed';
var animClass = 'main__aside--transitioning';
init();
function init() {
var collLS = window.localStorage.getItem('asidenavCollapsed') === 'true';
if (document.body.getBoundingClientRect().width < 999 || collLS) {
asideEl.classList.add(collClass);
collapsed = true;
}
}
function check() {
if (collapsed && !hasClass() || !collapsed && hasClass()) {
asideEl.classList.add(animClass);
asideEl.classList.toggle(collClass, collapsed);
window.localStorage.setItem('asidenavCollapsed', collapsed);
}
}
function hasClass() {
return asideEl.classList.contains(collClass);
}
asideEl.addEventListener('click', function(event) {
if (event.target === asideEl) {
collapsed = !collapsed;
check();
}
});
asideEl.addEventListener('transitionend', function(event) {
if (event.propertyName === 'opacity') {
asideEl.classList.remove(animClass);
}
});
window.addEventListener('resize', function() {
collapsed = document.body.getBoundingClientRect().width < 999;
check();
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
utils.aside(document.querySelector('.main__aside'));
});

View File

@ -0,0 +1,94 @@
.main__aside {
flex-shrink: 0;
background-color: var(--darkbase);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
width: 300px;
overflow: hidden;
}
.main__aside--transitioning {
transition: width .2s ease;
}
.main__aside--transitioning .asidenav__box{
transition: opacity .2s ease;
}
.main__aside--collapsed:hover {
overflow: visible;
}
.main__aside--collapsed {
width: 50px;
}
.main__aside--collapsed .asidenav__box {
opacity: 0;
}
.main__aside--collapsed .asidenav__box--dont-hide {
opacity: 1;
}
.main__aside--collapsed:not(.main__aside--transitioning) .asidenav__box {
height: 0;
padding: 0;
margin: 0;
visibility: hidden;
}
.asidenav {
width: 300px;
margin-top: 20px;
color: white;
}
.asidenav__box {
margin: 10px 0;
padding: 10px 0;
border-bottom: 4px solid var(--whitebase);
background-color: var(--darkbase);
}
.asidenav__box-title {
padding: 7px 13px;
}
.asidenav__list--padded {
padding: 0 13px;
}
.asidenav__list-item {
position: relative;
background-color: white;
color: var(--darkbase);
.glyphicon {
position: absolute;
z-index: 1;
top: 5px;
}
&:not(.asidenav__list-item--active):hover {
color: white;
background-color: var(--darkbase);
.asidenav__link {
color: white;
}
}
}
.asidenav__list-item--active {
background-color: var(--darkbase);
color: white;
.asidenav__link {
pointer-events: none;
color: white;
}
}
.asidenav__link {
position: relative;
display: block;
line-height: 50px;
margin: 4px 0;
padding-left: 54px;
color: var(--darkbase);
z-index: 1;
}

View File

@ -0,0 +1,7 @@
<div .breadcrumbs__container>
<ul .breadcrumbs__list.list--inline>
$forall bc <- parents
<li .breadcrumbs__item>
<a .breadcrumbs__link href="@{fst bc}">#{snd bc}
&gt;
<li .breadcrumbs__item--active>#{title}

View File

@ -0,0 +1,17 @@
.breadcrumbs__container {
position: absolute;
color: white;
left: 340px;
top: 7px;
z-index: 100;
transition: left .2s ease;
}
.breadcrumbs__container .breadcrumbs__link {
color: white;
}
@media (max-width: 999px) {
.breadcrumbs__container {
left: 90px;
}
}

View File

@ -0,0 +1,9 @@
$newline never
#{fragment}
$case formLayout
$of FormStandard
$forall view <- views
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error>
$if not (Blaze.null $ fvLabel view)
<label .form-group__label .reactive-label for=#{fvId view}>#{fvLabel view}
^{fvInput view}

View File

@ -0,0 +1,51 @@
(function() {
'use strict';
window.utils = window.utils || {};
// registers input-listener for each element in <elements> (array) and
// enables <button> if <fn> for these elements returns true
window.utils.reactiveButton = function(elements, button, fn) {
if (elements.length == 0) {
return false;
}
var checkboxes = elements[0].getAttribute('type') === 'checkbox';
var eventType = checkboxes ? 'change' : 'input';
updateButtonState();
elements.forEach(function(el) {
el.addEventListener(eventType, function() {
updateButtonState();
});
});
function updateButtonState() {
if (fn.call(null, elements)) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
// auto reactiveButton submit-buttons with required fields
var forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
var requireds = form.querySelectorAll('[required]');
var submitBtn = form.querySelector('[type=submit]');
if (submitBtn && requireds) {
window.utils.reactiveButton(Array.from(requireds), submitBtn, function(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
});
}
});
});

View File

View File

@ -0,0 +1,10 @@
<div>
<h2>A little lorem ipsum to make the page scrollable
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.
Aute velit consectetur consequat excepteur ut in qui do reprehenderit sit consequat occaecat incididunt sint eu. Nulla cillum quis et sit labore aliquip et ad excepteur duis elit deserunt aute. Anim esse reprehenderit et ipsum et sit quis minim enim sint et pariatur duis nostrud. Do minim dolore adipisicing ullamco ad aute veniam id magna est proident tempor labore non. Mollit et laboris duis esse commodo ex tempor.
Elit labore qui dolor ea non ut anim occaecat do aliqua cillum qui esse. Aliqua nisi non veniam labore esse irure velit qui veniam labore consectetur ut. Tempor laboris anim officia veniam nostrud cupidatat reprehenderit aute incididunt nostrud dolore occaecat deserunt pariatur in non dolore.
Proident anim pariatur eu laborum Lorem duis cillum est anim magna sit pariatur eu. Nulla consectetur quis ea sunt proident sint laborum. Esse est ea Lorem mollit aute excepteur fugiat ipsum ipsum ad irure do reprehenderit voluptate. Sint tempor nulla mollit voluptate pariatur aliquip culpa amet ullamco fugiat incididunt minim sint ipsum exercitation eiusmod ad. Nostrud ipsum nulla cillum ex aute elit voluptate proident proident magna ut. Ipsum officia cillum officia nostrud enim fugiat ut. Voluptate laboris aute dolore incididunt aliquip sit sit. Ea ut laboris Lorem ea ad non nostrud aute tempor non nisi.
Qui cupidatat nisi id et excepteur sint aliquip fugiat sint reprehenderit aliquip enim anim aliqua sint dolore proident. Voluptate labore tempor laboris nisi eiusmod sunt occaecat deserunt adipisicing. Consectetur fugiat quis non ad laboris aliqua voluptate in eu id. Duis velit cillum aliquip dolor non ea mollit incididunt elit ex excepteur aute consequat amet.

View File

@ -0,0 +1,23 @@
<div .navbar-container>
<nav .navbar.js-sticky-navbar>
<ul .navbar__list.list--inline>
$forall menuType <- menuTypes
$case menuType
$of NavbarRight (MenuItem label mIcon route _)
<li .navbar__list-item :Just route == mcurrentRoute:.navbar__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .navbar__link href=@{route}>#{label}
$of NavbarSecondary (MenuItem label mIcon route _)
<li .navbar__list-item.navbar__list-item--secondary :Just route == mcurrentRoute:.navbar__list-item--active>
$if isJust mIcon
<div .glyphicon.glyphicon--#{fromMaybe "" mIcon}>
<a .navbar__link href=@{route}>#{label}
$of _
<!-- breadcrumbs -->
$if not $ Just HomeR == mcurrentRoute
^{breadcrumbs}
<div .navbar__pushdown>

View File

@ -0,0 +1,28 @@
/**
* .js-sticky-navbar
* ul
* li Item 1
* li Item 2
*/
document.addEventListener('DOMContentLoaded', function() {
var ticking = false;
var nav = document.querySelector('.js-sticky-navbar');
window.addEventListener('scroll', function(e) {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
}, false);
// checks scroll direction and shows/hides navbar accordingly
function checkScroll() {
var sticky = window.scrollY > 0;
nav.classList.toggle('navbar--sticky', sticky);
ticking = false;
}
checkScroll();
});

View File

@ -0,0 +1,111 @@
.navbar {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
width: 100%;
height: var(--header-height);
line-height: var(--header-height);
padding-right: 5vw;
background: var(--darkbase); /* Old browsers */
background: -moz-linear-gradient(bottom, var(--darkbase) 0%, #425d79 100%); /* FF3.6-15 */
background: -webkit-linear-gradient(bottom, var(--darkbase) 0%,#425d79 100%); /* Chrome10-25,Safari5.1-6 */
background: linear-gradient(to top, var(--darkbase) 0%,#425d79 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white;
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
z-index: 10;
overflow: hidden;
}
.navbar .navbar__link {
position: relative;
display: inline-block;
height: 100%;
padding: 0 13px;
color: white;
text-transform: uppercase;
}
.navbar .navbar__link.glyphicon::before {
left: 50%;
top: -20px;
transform: translateX(-50%);
margin: 0;
}
.navbar__list-item {
position: relative;
padding-top: 13px;
> .glyphicon {
position: absolute;
width: 100%;
}
> .glyphicon::before {
height: 40px;
margin: 0;
left: 50%;
transform: translateX(-50%);
}
}
.navbar__list-item--secondary {
margin-left: 20px;
color: var(--greybase);
}
.navbar__list-item--secondary + .navbar__list-item--secondary {
margin-left: 0;
border-left: 0;
}
.navbar__list-item--active {
background-color: white;
color: var(--darkbase);
}
.navbar__list-item--active > .navbar__link {
color: var(--darkbase);
pointer-events: none;
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover {
background-color: var(--darkbase);
color: var(--whitebase);
}
.navbar .navbar__list-item:not(.navbar__list-item--active):hover > .navbar__link {
color: var(--whitebase);
}
.navbar__list-item--secondary > .navbar__link {
color: var(--greybase);
}
.navbar__link {
transition: opacity .2s ease;
}
.navbar--sticky {
position: fixed;
top: 0;
left: 0;
height: var(--header-height-collapsed);
line-height: var(--header-height-collapsed);
z-index: 100;
transition: height 0.2s ease, line-height 0.2s ease;
.navbar__link {
opacity: 0;
}
.breadcrumbs__container {
top: 0px;
}
}
.navbar__pushdown {
display: none;
background-color: var(--darkbase);
height: var(--header-height);
}
.navbar--sticky + .navbar__pushdown {
display: block;
}