Merge branch 'master' of gitlab.cip.ifi.lmu.de:jost/UniWorX

This commit is contained in:
SJost 2018-10-11 10:35:26 +02:00
commit d937e3d894
24 changed files with 293 additions and 214 deletions

View File

@ -59,7 +59,7 @@ colDescription = sortable Nothing (i18nCell MsgCourseDescription)
$ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity cid Course{..}, _, _, _) } ->
case courseDescription of case courseDescription of
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell $ modalStatic descr (Just descr) -> cell $ modal "Beschreibung" (Right $ toWidget descr)
colCShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colCShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort) colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
@ -73,7 +73,11 @@ colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
( case courseDescription of ( case courseDescription of
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell (Just descr) -> cell
[whamlet|<span style="float:right"> ^{modalStatic descr} |] [whamlet|
$newline never
<span style="float:right">
^{modal "Beschreibung" (Right $ toWidget descr)}
|]
) )
colTerm :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a) colTerm :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
@ -612,5 +616,3 @@ getCUserR tid ssh csh uuid = do
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCHiWisR tid ssh csh = undefined -- TODO getCHiWisR tid ssh csh = undefined -- TODO

View File

@ -185,17 +185,19 @@ getProfileDataR = do
-- TODO: move this into a Message and/or Widget-File -- TODO: move this into a Message and/or Widget-File
let delWdgt = [whamlet| let delWdgt = [whamlet|
<form .form-inline method=post action=@{ProfileDataR} enctype=#{btnEnctype}> <form .form-inline method=post action=@{ProfileDataR} enctype=#{btnEnctype}>
<div> <h2>
Sind Sie sich absolut sicher, alle Ihre in Uni2work gespeicherten Daten zu löschen?
<div .container>
Während der Testphase von Uni2work können Sie hiermit Während der Testphase von Uni2work können Sie hiermit
Ihren Account bei Uni2work vollständig löschen. Ihren Account bei Uni2work vollständig löschen.
Mit Ihrem Campus-Account können Sie sich aber danach Mit Ihrem Campus-Account können Sie sich aber danach
jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird. jederzeit erneut einloggen, wodurch wieder ein leerer Account erstellt wird.
<div> <div .container>
Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht, Hochgeladene Hausaufgaben-Dateien werden unabhhängig vom Urherber nur dann gelöscht,
wenn die Dateien ausschließlich Ihnen zugeordnet sind. wenn die Dateien ausschließlich Ihnen zugeordnet sind.
Dateien aus Gruppenabgaben werden also erst dann gelöscht, Dateien aus Gruppenabgaben werden also erst dann gelöscht,
wenn alle Gruppenmitglieder Ihren Account gelöscht haben. wenn alle Gruppenmitglieder Ihren Account gelöscht haben.
<div> <div .container>
<em>Achtung: <em>Achtung:
Auch abgegebene Hausübungen werden gelöscht! Auch abgegebene Hausübungen werden gelöscht!
Falls ein Veranstalter Informationen darüber nicht anderweitig gespeichert hat, Falls ein Veranstalter Informationen darüber nicht anderweitig gespeichert hat,
@ -203,13 +205,12 @@ getProfileDataR = do
(Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen (Verbuchte Noten sollten dadurch nicht betroffen sein, aber in einem etwaigen
Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann Streitfall konnen die per Uni2work verwalteten Hausaufgaben dann
auch nicht mehr rekonstruiert/berücksichtigt werden.) auch nicht mehr rekonstruiert/berücksichtigt werden.)
<div> <div .container>
<em>Nach der Testphase von Uni2work wird das Löschen eines Accounts etwas <em>Nach der Testphase von Uni2work wird das Löschen eines Accounts etwas
eingeschränkt werden, da z.B. Klausurnoten 5 Jahre bis nach Exmatrikulation eingeschränkt werden, da z.B. Klausurnoten 5 Jahre bis nach Exmatrikulation
aufbewahrt werden müssen. aufbewahrt werden müssen.
<div> <div .container>
Sind Sie sich absolut sicher, alle Ihre in Uni2work gespeicherten Daten zu löschen? ^{btnWdgt}
<div>^{btnWdgt}
|] |]
defaultLayout $ do defaultLayout $ do
$(widgetFile "profileData") $(widgetFile "profileData")

View File

@ -31,7 +31,7 @@ import Handler.Utils.Table.Cells
-- import Colonnade hiding (fromMaybe, singleton, bool) -- import Colonnade hiding (fromMaybe, singleton, bool)
import qualified Yesod.Colonnade as Yesod import qualified Yesod.Colonnade as Yesod
import Text.Blaze (text) import Text.Blaze (text)
-- --
-- import qualified Data.UUID.Cryptographic as UUID -- import qualified Data.UUID.Cryptographic as UUID
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
@ -451,7 +451,7 @@ handleSheetEdit tid ssh csh msId template dbAction = do
whenIsJust sfSolutionF $ insertSheetFile' sid SheetSolution whenIsJust sfSolutionF $ insertSheetFile' sid SheetSolution
whenIsJust sfMarkingF $ insertSheetFile' sid SheetMarking whenIsJust sfMarkingF $ insertSheetFile' sid SheetMarking
insert_ $ SheetEdit aid actTime sid insert_ $ SheetEdit aid actTime sid
addMessageI Info $ MsgSheetEditOk tid ssh csh sfName addMessageI Success $ MsgSheetEditOk tid ssh csh sfName
-- Sanity checks generating warnings only, but not errors! -- Sanity checks generating warnings only, but not errors!
warnTermDays tid [sfVisibleFrom, Just sfActiveFrom, Just sfActiveTo, sfHintFrom, sfSolutionFrom] warnTermDays tid [sfVisibleFrom, Just sfActiveFrom, Just sfActiveTo, sfHintFrom, sfSolutionFrom]
return True return True
@ -522,7 +522,7 @@ insertSheetFile' sid ftype fs = do
fid <- insert file fid <- insert file
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
data CorrectorForm = CorrectorForm data CorrectorForm = CorrectorForm
{ cfUserId :: UserId { cfUserId :: UserId
, cfUserName :: Text , cfUserName :: Text
@ -547,9 +547,9 @@ defaultLoads shid = do
return . E.min_ $ sheetEdit E.^. SheetEditTime return . E.min_ $ sheetEdit E.^. SheetEditTime
E.where_ $ sheet E.^. SheetCourse E.==. E.val cId E.where_ $ sheet E.^. SheetCourse E.==. E.val cId
E.orderBy [E.desc creationTime] E.orderBy [E.desc creationTime]
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState) return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
where where
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
@ -616,11 +616,11 @@ correctorForm shid = do
_ -> return loads'' _ -> return loads''
let deletions' = deletions `Set.difference` Map.keysSet loads let deletions' = deletions `Set.difference` Map.keysSet loads
names <- fmap (Map.fromList . map (\(E.Value a, E.Value b) -> (a, b))) . lift . runDB . E.select . E.from $ \user -> do names <- fmap (Map.fromList . map (\(E.Value a, E.Value b) -> (a, b))) . lift . runDB . E.select . E.from $ \user -> do
E.where_ $ user E.^. UserId `E.in_` E.valList (Map.keys loads) E.where_ $ user E.^. UserId `E.in_` E.valList (Map.keys loads)
return $ (user E.^. UserId, user E.^. UserDisplayName) return $ (user E.^. UserId, user E.^. UserDisplayName)
let let
constructFields :: (UserId, Text, (CorrectorState, Load)) -> MForm Handler CorrectorForm constructFields :: (UserId, Text, (CorrectorState, Load)) -> MForm Handler CorrectorForm
constructFields (uid, uname, (state, Load{..})) = do constructFields (uid, uname, (state, Load{..})) = do
@ -691,7 +691,7 @@ correctorForm shid = do
|] |]
} }
]) ])
-- Eingabebox für Korrektor hinzufügen -- Eingabebox für Korrektor hinzufügen
-- Eingabe für Korrekt ausgefüllt: FormMissing zurückschicken um dann Feld hinzuzufügen -- Eingabe für Korrekt ausgefüllt: FormMissing zurückschicken um dann Feld hinzuzufügen

View File

@ -76,7 +76,11 @@ courseCell (Course {..}) = anchorCell link name `mappend` desc
name = citext2widget courseName name = citext2widget courseName
desc = case courseDescription of desc = case courseDescription of
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell [whamlet| <span style="float:right"> ^{modalStatic descr} |] (Just descr) -> cell [whamlet|
$newline never
<span style="float:right">
^{modal "Beschreibung" (Right $ toWidget descr)}
|]
sheetCell :: IsDBTable m a => CourseLink -> SheetName -> DBCell m a sheetCell :: IsDBTable m a => CourseLink -> SheetName -> DBCell m a
sheetCell crse shn = sheetCell crse shn =

View File

@ -2,39 +2,19 @@
module Handler.Utils.Templates where module Handler.Utils.Templates where
import Data.Either (isLeft)
import Import.NoFoundation import Import.NoFoundation
lipsum :: WidgetT site IO () lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum") lipsum = $(widgetFile "widgets/lipsum")
modalStatic :: Html -> WidgetT site IO () modal :: WidgetT site IO () -> Either (Route site) (WidgetT site IO ()) -> WidgetT site IO ()
modalStatic modalContent = do modal modalTrigger modalContent = do
uniqueId <- newIdent let modalDynamic = isLeft modalContent
let modalTrigger = cons '#' uniqueId -- SJ: I am confused why this is needed here? modalId <- newIdent
modalId :: Int32 triggerId <- newIdent
modalId = 13
$(widgetFile "widgets/modalStatic")
[whamlet|<div .tooltip__handle ##{uniqueId}>?|] -- SJ: confused why ## is needed here either?
modalWidget :: Html -> WidgetT site IO () -> WidgetT site IO ()
modalWidget modalTrigger modalContent = do
uniqueId <- newIdent
let modalTriggerId = cons '#' uniqueId -- SJ: I am confused why this is needed here?
modalId :: Int32
modalId = 13
$(widgetFile "widgets/modalWidget")
[whamlet|<div .btn ##{uniqueId}>#{modalTrigger}|] -- SJ: confused why ## is needed here either?
modal :: Text -> Maybe [Char] -> WidgetT site IO ()
modal modalTrigger (Just modalContent) = do -- WARNING: ModalContent should not have length 11. SJ: This is possibly bad. See Template!
let
modalId :: Int32
modalId = 13
$(widgetFile "widgets/modal")
modal modalTrigger Nothing = do
let
modalId :: Int32
modalId = 13
modalContent :: [Char]
modalContent = "placeholder"
$(widgetFile "widgets/modal") $(widgetFile "widgets/modal")
case modalContent of
Left route -> [whamlet|<a .btn ##{triggerId} href=@{route}>^{modalTrigger}|]
Right content -> [whamlet|<div .btn ##{triggerId}>^{modalTrigger}|]

View File

@ -44,6 +44,8 @@ splitCamel = map fromList . reverse . helper (error "hasChange undefined at star
| sameCategory w c | sameCategory w c
, null ws' = helper (Char.isLower w) items (c:ws) cs , null ws' = helper (Char.isLower w) items (c:ws) cs
| sameCategory w c = helper hadChange items (c:ws) cs | sameCategory w c = helper hadChange items (c:ws) cs
| Char.isLower w
, Char.isUpper c = helper True (reverse ws :items) [c] cs
| null ws' = helper True items (c:ws) cs | null ws' = helper True items (c:ws) cs
| not hadChange = helper True (reverse ws':items) [c,w] cs | not hadChange = helper True (reverse ws':items) [c,w] cs
| otherwise = helper True (reverse ws :items) [c] cs | otherwise = helper True (reverse ws :items) [c] cs

View File

@ -22,7 +22,6 @@
<li .list-group-item> <li .list-group-item>
<a href=@{CourseNewR}>Kurse anlegen <a href=@{CourseNewR}>Kurse anlegen
<hr>
<div .container> <div .container>
<h2>Funktionen zum Testen <h2>Funktionen zum Testen
@ -33,9 +32,5 @@
^{btnWdgt} ^{btnWdgt}
<li><br> <li><br>
Modals: Modals:
^{modal ".toggler1" Nothing} ^{modal "Klick mich für Ajax-Test" (Left UsersR)}
<a href=@{UsersR} .btn.toggler1>Klick mich für Ajax-Test ^{modal "Klick mich für Content-Test" (Right "Test Inhalt für Modal")}
<noscript>(Für Modals bitte JS aktivieren)</noscript>
^{modal ".toggler2" (Just "Test Inhalt für Modal")}
<div .btn.toggler2>Klick mich für Content-Test
<noscript>(Für Modals bitte JS aktivieren)</noscript>

View File

@ -7,38 +7,11 @@ $newline never
<html class="no-js" lang="en"> <!--<![endif]--> <html class="no-js" lang="en"> <!--<![endif]-->
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>#{pageTitle pc} <title>#{pageTitle pc}
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
^{pageHead pc} ^{pageHead pc}
\<!--[if lt IE 9]>
\<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
\<![endif]-->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.0.3/js.cookie.min.js">
<script>
/* 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. */
/* See the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package for details. */
var csrfHeaderName = "#{TE.decodeUtf8 $ CI.foldedCase defaultCsrfHeaderName}";
var csrfCookieName = "#{TE.decodeUtf8 defaultCsrfCookieName}";
var csrfToken = Cookies.get(csrfCookieName);
if (csrfToken) {
\ $.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
\ if (!options.crossDomain) {
\ jqXHR.setRequestHeader(csrfHeaderName, csrfToken);
\ }
\ });
}
<body .no-js .theme--#{toPathPiece currentTheme} :isAuth:.logged-in> <body .no-js .theme--#{toPathPiece currentTheme} :isAuth:.logged-in>
<!-- removes no-js class from body if client supports javascript --> <!-- removes no-js class from body if client supports javascript -->
<script> <script>

View File

@ -113,7 +113,7 @@ a:hover {
ul { ul {
list-style-type: none; margin-left: 20px;
} }
h1, h2, h3, h4, h5 { h1, h2, h3, h4, h5 {
@ -424,8 +424,17 @@ input[type="button"].btn-info:hover,
} }
/* LIST MODIFIERS */ /* LIST MODIFIERS */
.list--inline li { .list--iconless {
display: inline-block; list-style-type: none;
margin-left: 0;
}
.list--inline {
margin-left: 0;
li {
display: inline-block;
}
} }
.list--comma-separated li { .list--comma-separated li {

View File

@ -16,5 +16,5 @@ $# new files
<label for=#{fieldId}_zip>ZIPs automatisch entpacken <label for=#{fieldId}_zip>ZIPs automatisch entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}> <input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
<div class="js-tooltip"> <div class="js-tooltip">
<div class="tooltip__handle">? <div class="tooltip__handle">
<div class="tooltip__content">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu. <div class="tooltip__content">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.

View File

@ -51,7 +51,7 @@
. .
<h2> <h2>
^{modalWidget "Alle Benutzerbezogenen Daten löschen" delWdgt} ^{modal "Alle Benutzerbezogenen Daten löschen" (Right delWdgt)}
<p> <p>
<h4>Hinweise: <h4>Hinweise:
<ul> <ul>

View File

@ -3,25 +3,75 @@
window.utils = window.utils || {}; window.utils = window.utils || {};
window.utils.alert = function(alertEl) { var ALERT_INVISIBLE_CLASS = 'alert--invisible';
var closeEl = document.createElement('DIV'); var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible';
var dataDecay = alertEl.dataset.decay;
var autoDecay = 30;
if (dataDecay) {
autoDecay = parseInt(dataDecay, 10);
}
closeEl.classList.add('alert__close');
closeEl.addEventListener('click', function(event) {
alertEl.classList.add('alert--invisible');
});
alertEl.insertBefore(closeEl, alertEl.children[0]);
// auto-hide info and success-alerts after 3 seconds window.utils.alerts = function(alertsEl) {
if (autoDecay > 0 && !alertEl.matches('.alert-danger, .alert-warning')) {
window.setTimeout(function() { var alerts = Array.from(alertsEl.querySelectorAll('.alert'));
alertEl.classList.add('alert--invisible'); var toggler;
}, autoDecay * 1000); var showingToggler = false;
function makeToggler() {
toggler = document.createElement('DIV');
toggler.classList.add('alerts__toggler');
toggler.addEventListener('click', function() {
alerts.forEach(function(alert) {
alert.classList.remove(ALERT_INVISIBLE_CLASS);
toggler.classList.remove(TOGGLER_INVISIBLE_CLASS);
});
checkToggler();
});
alertsEl.appendChild(toggler);
} }
function makeAlert(alertEl) {
var iconEl = document.createElement('DIV');
var closeEl = document.createElement('DIV');
var dataDecay = alertEl.dataset.decay;
var autoDecay = 30;
if (dataDecay) {
autoDecay = parseInt(dataDecay, 10);
}
iconEl.classList.add('alert__icon');
closeEl.classList.add('alert__close');
closeEl.addEventListener('click', function(event) {
closeAlert(alertEl);
});
alertEl.insertBefore(iconEl, alertEl.children[0]);
alertEl.insertBefore(closeEl, alertEl.children[0]);
// auto-hide info and success-alerts after 3 seconds
if (autoDecay > 0 && !alertEl.matches('.alert-warning, .alert-error')) {
window.setTimeout(function() {
closeAlert(alertEl);
}, autoDecay * 1000);
}
}
function closeAlert(alertEl) {
alertEl.classList.add(ALERT_INVISIBLE_CLASS);
checkToggler();
}
function checkToggler() {
var hidden = true;
alerts.forEach(function(alert) {
if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) {
hidden = false;
}
});
if (!showingToggler) {
showingToggler = true;
window.setTimeout(function() {
toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden);
showingToggler = false;
}, 120);
}
}
makeToggler();
alerts.map(makeAlert);
} }
})(); })();
@ -29,7 +79,5 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// setup alerts // setup alerts
Array.from(document.querySelectorAll('.alert')).forEach(function(alertEl) { window.utils.alerts(document.querySelector('.alerts'));
window.utils.alert(alertEl);
});
}); });

View File

@ -3,100 +3,152 @@
.alert .alert
Regular Info Alert Regular Info Alert
Disappears automatically after 30 seconds Disappears automatically after 30 seconds
Disappears after x seconds if explicitly specified via data-decay='x' on html element Disappears after x seconds if explicitly specified via data-decay='x'
Can be told not to disappear with data-decay='0' Can be told not to disappear with data-decay='0'
.alert-warning, .alert-error .alert-success
Warning or Error alert Disappears automatically after 30 seconds
These don't disappear, only difference is color
.alert-warning is orange regardless of user's selected theme .alert-warning
.alert-error is red regardless of user's selected theme Does not disappear
Orange regardless of user's selected theme
.alert-error
Does not disappear
Red regardless of user's selected theme
*/ */
.alerts { .alerts {
position: fixed; position: fixed;
bottom: 5%; bottom: 0;
right: 0; right: 5%;
z-index: 20; z-index: 20;
text-align: right; text-align: right;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.alerts__toggler {
width: 40px;
height: 40px;
position: absolute;
top: 400px;
left: 50%;
transform: translateX(-50%);
cursor: pointer;
&::before {
content: '\f077';
position: absolute;
font-family: "Font Awesome 5 Free";
left: 50%;
top: 0;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
color: var(--color-lightblack);
font-size: 30px;
transform: translateX(-50%);
}
}
.alerts__toggler--visible {
top: -40px;
opacity: 1;
transition: top .5s cubic-bezier(0.73, 1.25, 0.61, 1),
opacity .5s cubic-bezier(0.73, 1.25, 0.61, 1);
}
@media (max-width: 425px) {
.alerts {
left: 5%;
}
}
.alert { .alert {
position: relative; position: relative;
display: inline-block; display: block;
background-color: var(--color-dark); background-color: var(--color-lightblack);
font-size: 1rem; font-size: 1rem;
color: var(--color-lightwhite); color: var(--color-lightwhite);
z-index: 0; z-index: 0;
max-height: 200px; padding: 0 50px;
transition: all .3s ease-in-out; padding-right: 60px;
padding-left: 20px;
margin-left: 20px;
animation: slide-in-alert .2s ease-out forwards; animation: slide-in-alert .2s ease-out forwards;
margin-bottom: 20px; margin-bottom: 10px;
transition: margin-bottom .2s ease-out;
&:hover {
.alert__content {
&::after {
opacity: 1;
}
}
}
} }
@keyframes slide-in-alert { @keyframes slide-in-alert {
from { from {
left: 120%; transform: translateY(120%);
} }
to { to {
left: 0; transform: translateY(0);
}
}
@keyframes slide-out-alert {
from {
transform: translateY(0);
max-height: 200px;
}
to {
transform: translateY(250%);
opacity: 0;
max-height: 0;
overflow: hidden;
} }
} }
@media (min-width: 425px) { @media (min-width: 425px) {
.alert { .alert {
margin-left: 80px; max-width: 400px;
max-width: 420px;
} }
} }
@media (min-width: 768px) { .alert--invisible {
animation: slide-out-alert .2s ease-out forwards;
.alert { margin-bottom: 0;
padding-left: 30px;
margin-left: 40px;
min-width: 400px;
}
}
@media (min-width: 1024px) {
.alert {
min-width: 350px;
}
} }
.alert__content { .alert__content {
padding: 8px 1.5em; padding: 8px 0;
min-height: 40px; min-height: 40px;
position: relative; position: relative;
display: flex; display: flex;
font-weight: 600; font-weight: 600;
justify-content: flex-end;
align-items: center; align-items: center;
text-align: left; text-align: left;
} }
@media (max-width: 768px) { .alert__icon {
text-align: right;
position: absolute;
left: 0px;
top: 0;
width: 50px;
height: 100%;
z-index: 40;
.alert__content { &::before {
padding: 4px 7px; content: '\f05a';
padding-left: 25px; position: absolute;
font-family: "Font Awesome 5 Free";
font-size: 24px;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
border-radius: 50%;
width: 30px;
height: 30px;
} }
} }
@ -104,7 +156,7 @@
cursor: pointer; cursor: pointer;
text-align: right; text-align: right;
position: absolute; position: absolute;
left: 0px; right: 0px;
top: 0; top: 0;
width: 60px; width: 60px;
height: 100%; height: 100%;
@ -145,18 +197,26 @@
} }
} }
.alert-success {
background-color: var(--color-success);
.alert__icon::before {
content: '\f058';
}
}
.alert-warning { .alert-warning {
background-color: var(--color-warning); background-color: var(--color-warning);
.alert__icon::before {
content: '\f06a';
}
} }
.alert-danger,
.alert-error { .alert-error {
background-color: var(--color-error); background-color: var(--color-error);
}
.alert--invisible { .alert__icon::before {
max-height: 0; content: '\f071';
transform: translateX(120%); }
margin-bottom: 0;
overflow: hidden;
} }

View File

@ -30,6 +30,16 @@
} }
} }
.form-group--submit .form-group__input {
grid-column: 2;
}
@media (max-width: 768px) {
.form-group--submit .form-group__input {
grid-column: 1;
}
}
.form-group--has-error { .form-group--has-error {
background-color: rgba(255, 0, 0, 0.1); background-color: rgba(255, 0, 0, 0.1);
@ -187,6 +197,7 @@ input[type="checkbox"]:checked::after {
.checkbox, .checkbox,
.radio { .radio {
position: relative; position: relative;
display: inline-block;
[type="checkbox"], [type="checkbox"],
[type="radio"] { [type="radio"] {

View File

@ -6,12 +6,11 @@
window.utils.modal = function(modal) { window.utils.modal = function(modal) {
var overlay = document.createElement('div'); var overlay = document.createElement('div');
var closer = document.createElement('div'); var closer = document.createElement('div');
var trigger = document.querySelector(modal.dataset.trigger); var trigger = document.querySelector('#' + modal.dataset.trigger);
var origParent = modal.parentNode; var origParent = modal.parentNode;
function open(event) { function open(event) {
// disable modals for narrow screens // disable modals for narrow screens
if (window.innerWidth < 768) return true;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
@ -20,7 +19,6 @@
document.body.insertBefore(modal, null); document.body.insertBefore(modal, null);
document.body.insertBefore(overlay, modal); document.body.insertBefore(overlay, modal);
overlay.classList.add('modal__overlay--open'); overlay.classList.add('modal__overlay--open');
toggleScroll(false);
if (modal.dataset.closeable === 'true') { if (modal.dataset.closeable === 'true') {
closer.classList.add('modal__closer'); closer.classList.add('modal__closer');
@ -30,8 +28,8 @@
} }
} }
// open this modal with an event: // you can open this modal via event
// document.dispatchEvent(new CustomEvent('modal-open', { dateils: {for: 'modal-13'}})) // example: document.dispatchEvent(new CustomEvent('modal-open', { details: { for: 'modal-[id]' }}))
function openOnEvent(event) { function openOnEvent(event) {
if (event.detail.for === modal.getAttribute('id')) { if (event.detail.for === modal.getAttribute('id')) {
open(); open();
@ -43,7 +41,6 @@
overlay.remove(); overlay.remove();
origParent.insertBefore(modal, null); origParent.insertBefore(modal, null);
modal.classList.remove('modal--open'); modal.classList.remove('modal--open');
toggleScroll(true);
closer.removeEventListener('click', close, false); closer.removeEventListener('click', close, false);
} }
}; };
@ -56,27 +53,20 @@
trigger.classList.add('modal__trigger'); trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false); trigger.addEventListener('click', open, false);
} }
// if there is no content specified for the modal we assume that
// the content is supposed to be the page the trigger links to. if (modal.dataset.dynamic === 'True') {
// so we check if the trigger has a href-attribute, fetch that page var dynamicContentURL = trigger.getAttribute('href');
// and replace the modal content with the response if (dynamicContentURL.length > 0) {
var replaceMe = modal.querySelector('.replace-me'); fetch(dynamicContentURL, {
var replaceWith = trigger ? trigger.getAttribute('href') : ''; credentials: 'same-origin',
if (replaceMe) {
replaceMe.classList.remove('replace-me');
replaceMe.innerText = '...loading';
if (replaceWith.length > 0) {
fetch(replaceWith, {
credentials: 'same-origin'
}).then(function(response) { }).then(function(response) {
return response.text(); return response.text();
}).then(function(body) { }).then(function(body) {
var modalContent = document.createElement('div'); var modalContent = document.createElement('div');
modalContent.innerHTML = body; modalContent.innerHTML = body;
var main = modalContent.querySelector('.main__content'); var main = modalContent.querySelector('.main__content-body');
if (main) { if (main) {
replaceMe.innerText = ''; modal.appendChild(main);
replaceMe.insertBefore(main, null);
} else { } else {
replaceMe.innerHTML = body; replaceMe.innerHTML = body;
} }
@ -88,11 +78,6 @@
} }
setup(); setup();
}; };
// make sure document doesn't scroll when modal is active
function toggleScroll(scrollable) {
document.body.classList.toggle('no-scroll', !scrollable);
}
})(); })();
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View File

@ -4,14 +4,15 @@
top: 50%; top: 50%;
transform: translate(-50%, -50%) scale(0.8, 0.8); transform: translate(-50%, -50%) scale(0.8, 0.8);
display: block; display: block;
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.99);
min-width: 60vw; min-width: 60vw;
min-height: 100px; min-height: 100px;
max-height: calc(100vh - 30px); max-height: calc(100vh - 30px);
border-radius: 7px; border-radius: 2px;
z-index: -1; z-index: -1;
color: var(--color-font); color: var(--color-font);
padding: 20px; padding: 20px;
padding-right: 65px;
overflow: auto; overflow: auto;
opacity: 0; opacity: 0;
transition: all .15s ease; transition: all .15s ease;
@ -81,7 +82,3 @@
color: white; color: white;
} }
} }
.no-scroll {
overflow: hidden;
}

View File

@ -19,6 +19,23 @@
text-align: center; text-align: center;
margin: 0 10px; margin: 0 10px;
cursor: default; cursor: default;
position: relative;
&::before {
content: '\f128';
position: absolute;
top: 0;
left: 0;
font-family: "Font Awesome 5 Free";
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 15px;
}
&:hover {
background-color: var(--color-light);
}
} }
.tooltip__content { .tooltip__content {

View File

@ -9,13 +9,13 @@ $newline never
_{MsgWinterTermShort year} _{MsgWinterTermShort year}
$of Summer $of Summer
_{MsgSummerTermShort year} _{MsgSummerTermShort year}
<ul .asidenav__list.js-show-hide__target> <ul .asidenav__list.js-show-hide__target.list--iconless>
$forall (Course{..}, courseRoute, pageActions) <- favouriteTerm tid $forall (Course{..}, courseRoute, pageActions) <- favouriteTerm tid
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active> <li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{courseRoute}> <a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand} <div .asidenav__link-shorthand>#{courseShorthand}
<div .asidenav__link-label>#{courseName} <div .asidenav__link-label>#{courseName}
<ul .asidenav__nested-list> <ul .asidenav__nested-list.list--iconless>
$forall action <- pageActions $forall action <- pageActions
$case action $case action
$of PageActionPrime (MenuItem{..}) $of PageActionPrime (MenuItem{..})

View File

@ -3,6 +3,7 @@ $newline never
$case formLayout $case formLayout
$of FormStandard $of FormStandard
$forall view <- views $forall view <- views
$# TODO: add class 'form-group--submit' if this is the submit-button view
<div .form-group :fvRequired view:.form-group--required :not $ fvRequired view:.form-group--optional :isJust $ fvErrors view:.form-group--has-error> <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) $if not (Blaze.null $ fvLabel view)
<label .form-group__label for=#{fvId view}>#{fvLabel view} <label .form-group__label for=#{fvId view}>#{fvLabel view}
@ -10,5 +11,5 @@ $case formLayout
^{fvInput view} ^{fvInput view}
$maybe tooltip <- fvTooltip view $maybe tooltip <- fvTooltip view
<div .js-tooltip> <div .js-tooltip>
<div .tooltip__handle>? <div .tooltip__handle>
<div .tooltip__content>^{tooltip} <div .tooltip__content>^{tooltip}

View File

@ -1,8 +1,5 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true> <div .modal.js-modal #modal-#{modalId} data-trigger=#{triggerId} data-closeable=true data-dynamic=#{modalDynamic}>
$# primitive way of checking if this is supposed to be add a placeholder for async data. $case modalContent
$# modalContent is 'placeholder' if there should be a placeholder only. $of Right content
$# 'placeholder' has length 11. ^{content}
$if 11 == length modalContent $of Left _
<div .replace-me>
$else
#{modalContent}

View File

@ -1,2 +0,0 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTrigger} data-closeable=true>
#{modalContent}

View File

@ -1,2 +0,0 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{modalTriggerId} data-closeable=true>
^{modalContent}

View File

@ -41,7 +41,7 @@
background: linear-gradient(to top, var(--color-dark) 0%,var(--color-darker) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ background: linear-gradient(to top, var(--color-dark) 0%,var(--color-darker) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
color: white; color: white;
margin-right: 40px; margin-right: 40px;
z-index: 10; z-index: 20;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
overflow: auto; overflow: auto;
transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1); transition: all .2s cubic-bezier(0.03, 0.43, 0.58, 1);

View File

@ -6,6 +6,7 @@
.pagenav__list { .pagenav__list {
display: block; display: block;
margin-left: 0;
} }
.pagenav__list-item { .pagenav__list-item {