merge master
This commit is contained in:
commit
23029abec6
2
build.sh
2
build.sh
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec -- stack build --fast --flag uniworx:library-only --flag uniworx:dev
|
||||
exec -- stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
||||
echo Build task completed.
|
||||
|
||||
2
db.sh
2
db.sh
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Options: see /test/Database.hs (Main)
|
||||
stack build --fast --flag uniworx:library-only --flag uniworx:dev
|
||||
stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
||||
stack exec uniworxdb -- $@
|
||||
|
||||
3
hlint.sh
Executable file
3
hlint.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec -- ./test.sh uniworx:test:hlint
|
||||
4
messages/frontend/de.msg
Normal file
4
messages/frontend/de.msg
Normal file
@ -0,0 +1,4 @@
|
||||
FilesSelected: Dateien ausgewählt
|
||||
SelectFile: Datei auswählen
|
||||
SelectFiles: Datei(en) auswählen
|
||||
AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für deine Hilfe!
|
||||
@ -159,21 +159,21 @@ SheetInvisibleUntil date@Text: Dieses Übungsblatt ist für Teilnehmer momentan
|
||||
SheetName: Name
|
||||
SheetDescription: Hinweise für Teilnehmer
|
||||
SheetGroup: Gruppenabgabe
|
||||
SheetVisibleFrom: Sichtbar ab
|
||||
SheetVisibleFromTip: Ohne Datum wird das Blatt nie sichtbar, z.B. weil es noch nicht fertig ist
|
||||
SheetActiveFrom: Aktiv ab
|
||||
SheetActiveFromTip: Abgabe und Download der Aufgabenstellung ist erst ab diesem Datum möglich
|
||||
SheetActiveTo: Abgabefrist
|
||||
SheetVisibleFrom: Sichtbar für Teilnehmer ab
|
||||
SheetVisibleFromTip: Ohne Datum nie sichtbar und keine Abgabe möglich; nur für unfertige Blätter leer lassen, deren Fristen/Bewertung sich noch ändern kann
|
||||
SheetActiveFrom: Beginn Abgabezeitraum
|
||||
SheetActiveFromTip: Download der Aufgabenstellung erst ab diesem Datum möglich
|
||||
SheetActiveTo: Ende Abgabezeitraum
|
||||
SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
|
||||
SheetPseudonym: Persönliches Abgabe-Pseudonym
|
||||
SheetGeneratePseudonym: Generieren
|
||||
|
||||
SheetErrVisibility: Sichtbarkeit muss vor Beginn der Abgabefrist liegen
|
||||
SheetErrDeadlineEarly: Ende der Abgabefrist muss nach deren Beginn liegen
|
||||
SheetErrHintEarly: Hinweise dürfen erst nach Beginn der Abgabefrist herausgegeben werden
|
||||
SheetErrSolutionEarly: Die Lösung sollte erst nach Ende der Abgabefrist herausgegeben werden
|
||||
SheetErrVisibility: "Beginn Abgabezeitraum" muss nach "Sichbar für Teilnehmer ab" liegen
|
||||
SheetErrDeadlineEarly: "Ende Abgabezeitraum" muss nach "Beginn Abzeitraum" liegen
|
||||
SheetErrHintEarly: Hinweise dürfen erst nach Beginn des Abgabezeitraums herausgegeben werden
|
||||
SheetErrSolutionEarly: Lösungen dürfen erst nach Ende der Abgabezeitraums herausgegeben werden
|
||||
|
||||
|
||||
Deadline: Abgabe
|
||||
@ -244,7 +244,7 @@ AddCorrector: Zusätzlicher Korrektor
|
||||
CorrectorExists email@UserEmail: #{email} ist bereits als Korrektor eingetragen
|
||||
SheetCorrectorsTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{csh} #{sheetName}
|
||||
CountTutProp: Tutorien zählen gegen Proportion
|
||||
AutoAssignCorrs: Korrekturen am Ende der Abgabefrist automatisch zuteilen
|
||||
AutoAssignCorrs: Korrekturen nach Ablauf des Abgabezeitraums automatisch zuteilen
|
||||
Corrector: Korrektor
|
||||
Correctors: Korrektoren
|
||||
CorState: Status
|
||||
@ -254,7 +254,7 @@ CorByProportionOnly proportion@Rational: #{display proportion} Anteile
|
||||
CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Anteile - Tutorium
|
||||
CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium
|
||||
|
||||
RowCount count@Int64: #{display count} #{pluralDE count "Eintrag" "Einträge"} insgesamt
|
||||
RowCount count@Int64: #{display count} #{pluralDE count "Eintrag" "Einträge"} nach Filter
|
||||
DeleteRow: Zeile entfernen
|
||||
ProportionNegative: Anteile dürfen nicht negativ sein
|
||||
CorrectorUpdated: Korrektor erfolgreich aktualisiert
|
||||
@ -294,7 +294,10 @@ Settings: Individuelle Benutzereinstellungen
|
||||
SettingsUpdate: Einstellungen wurden gespeichert.
|
||||
Never: Nie
|
||||
|
||||
PreviouslyUploadedInfo: Bereits hochgeladene Dateien:
|
||||
PreviouslyUploadedDeletionInfo: (Nicht ausgewählte Dateien werden gelöscht)
|
||||
MultiFileUploadInfo: (Mehrere Dateien mit Shift oder Strg auswählen)
|
||||
AddMoreFiles: Weitere Dateien hinzufügen:
|
||||
|
||||
NrColumn: Nr
|
||||
SelectColumn: Auswahl
|
||||
@ -496,7 +499,7 @@ MailSubmissionsUnassignedIntro n@Int courseName@Text termDesc@Text sheetName@She
|
||||
|
||||
MailSubjectSheetSoonInactive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} kann nur noch kurze Zeit abgegeben werden
|
||||
MailSheetSoonInactiveIntro courseName@Text termDesc@Text sheetName@SheetName: Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) endet in Kürze.
|
||||
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabfrist für #{sheetName} in #{csh} abgelaufen
|
||||
MailSubjectSheetInactive csh@CourseShorthand sheetName@SheetName: Abgabezeitraum für #{sheetName} in #{csh} abgelaufen
|
||||
MailSheetInactiveIntro courseName@Text termDesc@Text sheetName@SheetName n@Int num@Int64: Die Abgabefirst für #{sheetName} im Kurs #{courseName} (#{termDesc}) beendet. Es gab #{noneOneMoreDE n "Keine Abgaben" "Nur eine Abgabe von " (display n <> " Abgaben von ")}#{noneOneMoreDE num "" "einem Teilnehmer" (display num <> " Teilnehmern")}.
|
||||
|
||||
MailSubjectCorrectionsAssigned csh@CourseShorthand sheetName@SheetName: Ihnen wurden Korrekturen zu #{sheetName} in #{csh} zugeteilt
|
||||
@ -510,6 +513,7 @@ MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{plu
|
||||
|
||||
MailEditNotifications: Benachrichtigungen ein-/ausschalten
|
||||
MailSubjectSupport: Supportanfrage
|
||||
MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject}
|
||||
|
||||
SheetGrading: Bewertung
|
||||
SheetGradingPoints maxPoints@Points: #{tshow maxPoints} Punkte
|
||||
@ -553,7 +557,7 @@ NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übun
|
||||
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
|
||||
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
|
||||
NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben
|
||||
NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen
|
||||
NotificationTriggerSheetInactive: Abgabezeitraum eines meiner Übungsblätter ist abgelaufen
|
||||
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
||||
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
|
||||
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
||||
@ -584,6 +588,7 @@ HelpAnswer: Antworten an
|
||||
HelpUser: Meinen Benutzeraccount
|
||||
HelpAnonymous: Keine Antwort (Anonym)
|
||||
HelpEmail: E-Mail
|
||||
HelpSubject: Betreff
|
||||
HelpRequest: Supportanfrage / Verbesserungsvorschlag
|
||||
HelpProblemPage: Problematische Seite
|
||||
HelpIntroduction: Wenn Ihnen die Benutzung dieser Webseite Schwierigkeiten bereitet oder Sie einen verbesserbaren Umstand entdecken bitten wir Sie uns das zu melden, auch wenn Sie Ihr Problem bereits selbst lösen konnten. Wir passen die Seite ständig an und versuchen sie auch für zukünftige Benutzer so einsichtig wie möglich zu halten.
|
||||
@ -739,4 +744,4 @@ AddRecipientCustom: Weitere Empfänger
|
||||
|
||||
RGCourseParticipants: Kursteilnehmer
|
||||
RGCourseLecturers: Kursverwalter
|
||||
RGCourseCorrectors: Korrektoren
|
||||
RGCourseCorrectors: Korrektoren
|
||||
|
||||
@ -218,6 +218,9 @@ executables:
|
||||
dependencies:
|
||||
- uniworx
|
||||
other-modules: []
|
||||
when:
|
||||
- condition: flag(library-only)
|
||||
buildable: false
|
||||
|
||||
# Test suite
|
||||
tests:
|
||||
|
||||
@ -66,7 +66,7 @@ campusForm :: ( RenderMessage site FormMessage
|
||||
, Button site ButtonSubmit
|
||||
) => AForm (HandlerT site IO) CampusLogin
|
||||
campusForm = CampusLogin
|
||||
<$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing
|
||||
<$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote & addAttr "autofocus" "") Nothing
|
||||
<*> areq passwordField (fslI MsgCampusPassword) Nothing
|
||||
|
||||
campusLogin :: forall site.
|
||||
|
||||
@ -5,8 +5,9 @@ module Database.Esqueleto.Utils
|
||||
, isInfixOf, hasInfix
|
||||
, any, all
|
||||
, SqlIn(..)
|
||||
, mkExactFilter, mkContainsFilter
|
||||
, anyFilter
|
||||
, mkExactFilter, mkExactFilterWith
|
||||
, mkContainsFilter
|
||||
, anyFilter, allFilter
|
||||
) where
|
||||
|
||||
import ClassyPrelude.Yesod hiding (isInfixOf, any, all)
|
||||
@ -74,13 +75,22 @@ _queryFeaturesDegree = $(sqlIJproj 3 2)
|
||||
-- Given a lens-like function, make filter for exact matches in a collection
|
||||
-- (Generalizing from Set to Foldable ok here, but gives ambigouus types elsewhere)
|
||||
mkExactFilter :: (PersistField a)
|
||||
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
||||
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
||||
-> t -- ^ query row
|
||||
-> Set.Set a -- ^ needle collection
|
||||
-> E.SqlExpr (E.Value Bool)
|
||||
mkExactFilter lenslike row criterias
|
||||
mkExactFilter = mkExactFilterWith id
|
||||
|
||||
-- | like @mkExactFiler@ but allows for conversion; convenient in conjunction with @anyFilter@ and @allFilter@
|
||||
mkExactFilterWith :: (PersistField b)
|
||||
=> (a -> b) -- ^ type conversion
|
||||
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
||||
-> t -- ^ query row
|
||||
-> Set.Set a -- ^ needle collection
|
||||
-> E.SqlExpr (E.Value Bool)
|
||||
mkExactFilterWith cast lenslike row criterias
|
||||
| Set.null criterias = true
|
||||
| otherwise = lenslike row `E.in_` E.valList (Set.toList criterias)
|
||||
| otherwise = lenslike row `E.in_` E.valList (cast <$> Set.toList criterias)
|
||||
|
||||
-- | generic filter creation for dbTable
|
||||
-- Given a lens-like function, make filter searching for needles in String-like elements
|
||||
@ -94,9 +104,22 @@ mkContainsFilter lenslike row criterias
|
||||
| Set.null criterias = true
|
||||
| otherwise = any (hasInfix $ lenslike row) criterias
|
||||
|
||||
|
||||
anyFilter :: (Foldable f) => f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
||||
-> t -> Set.Set Text-> E.SqlExpr (E.Value Bool)
|
||||
-- | Combine several filters, using logical or
|
||||
anyFilter :: (Foldable f)
|
||||
=> f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
||||
-> t
|
||||
-> Set.Set Text
|
||||
-> E.SqlExpr (E.Value Bool)
|
||||
anyFilter fltrs needle criterias = F.foldr aux false fltrs
|
||||
where
|
||||
aux fltr acc = fltr needle criterias E.||. acc
|
||||
aux fltr acc = fltr needle criterias E.||. acc
|
||||
|
||||
-- | Combine several filters, using logical and
|
||||
allFilter :: (Foldable f)
|
||||
=> f (t -> Set.Set Text-> E.SqlExpr (E.Value Bool))
|
||||
-> t
|
||||
-> Set.Set Text
|
||||
-> E.SqlExpr (E.Value Bool)
|
||||
allFilter fltrs needle criterias = F.foldr aux true fltrs
|
||||
where
|
||||
aux fltr acc = fltr needle criterias E.&&. acc
|
||||
@ -187,6 +187,7 @@ mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
|
||||
mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de"
|
||||
mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de"
|
||||
mkMessageVariant "UniWorX" "Button" "messages/button" "de"
|
||||
mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de"
|
||||
|
||||
-- This instance is required to use forms. You can modify renderMessage to
|
||||
-- achieve customized and internationalized form validation messages.
|
||||
@ -1053,6 +1054,11 @@ siteLayout' headingOverride widget = do
|
||||
hasSecondaryPageActions = any (is _PageActionSecondary) $ toListOf (traverse . _1 . _menuItemType) menuTypes
|
||||
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
|
||||
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
let
|
||||
-- See Utils.Frontend.I18n and files in messages/frontend for message definitions
|
||||
frontendI18n = toJSON (mr :: FrontendMessage -> Text)
|
||||
|
||||
pc <- widgetToPageContent $ do
|
||||
-- 3rd party
|
||||
addScript $ StaticR js_vendor_flatpickr_js
|
||||
@ -1064,20 +1070,21 @@ siteLayout' headingOverride widget = do
|
||||
-- polyfills
|
||||
addScript $ StaticR js_polyfills_fetchPolyfill_js
|
||||
addScript $ StaticR js_polyfills_urlPolyfill_js
|
||||
-- JavaScript services
|
||||
addScript $ StaticR js_services_utilRegistry_js
|
||||
addScript $ StaticR js_services_httpClient_js
|
||||
addScript $ StaticR js_services_i18n_js
|
||||
-- JavaScript utils
|
||||
addScript $ StaticR js_utils_alerts_js
|
||||
addScript $ StaticR js_utils_asidenav_js
|
||||
addScript $ StaticR js_utils_asyncForm_js
|
||||
addScript $ StaticR js_utils_asyncTable_js
|
||||
addScript $ StaticR js_utils_asyncTableFilter_js
|
||||
addScript $ StaticR js_utils_checkAll_js
|
||||
addScript $ StaticR js_utils_httpClient_js
|
||||
addScript $ StaticR js_utils_form_js
|
||||
addScript $ StaticR js_utils_inputs_js
|
||||
addScript $ StaticR js_utils_modal_js
|
||||
addScript $ StaticR js_utils_setup_js
|
||||
addScript $ StaticR js_utils_showHide_js
|
||||
addScript $ StaticR js_utils_tabber_js
|
||||
-- addScript $ StaticR js_utils_tabber_js
|
||||
addStylesheet $ StaticR css_utils_alerts_scss
|
||||
addStylesheet $ StaticR css_utils_asidenav_scss
|
||||
addStylesheet $ StaticR css_utils_asyncForm_scss
|
||||
@ -2221,12 +2228,9 @@ instance YesodMail UniWorX where
|
||||
mailT ctx mail = defMailT ctx $ do
|
||||
void setMailObjectId
|
||||
setDateCurrent
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
replaceMailHeader "Sender" . Just . addressEmail =<< getsYesod (appMailFrom . appSettings)
|
||||
|
||||
ret <- mail
|
||||
|
||||
setMailSmtpData
|
||||
return ret
|
||||
mail <* setMailSmtpData
|
||||
|
||||
|
||||
instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where
|
||||
|
||||
@ -121,7 +121,7 @@ postAdminTestR = do
|
||||
let emailWidget' = wrapForm emailWidget def
|
||||
{ formAction = Just . SomeRoute $ AdminTestR
|
||||
, formEncoding = emailEnctype
|
||||
, formAttrs = [("data-ajax-submit", "")]
|
||||
, formAttrs = [("uw-async-form", "")]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -161,6 +161,7 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
|
||||
mkRoute = do
|
||||
cid <- encrypt subId
|
||||
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
|
||||
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
||||
in mconcat
|
||||
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||
, writerCell $ do
|
||||
|
||||
@ -862,15 +862,28 @@ makeCourseUserTable cid colChoices psValidator = do
|
||||
, fltrUserEmail queryUser
|
||||
, fltrUserMatriclenr queryUser
|
||||
, fltrUserNameEmail queryUser
|
||||
-- , ("course-user-degree", error "TODO") -- TODO
|
||||
-- , ("field" , FilterColumn $ queryFeaturesField error "TODO") -- TODO
|
||||
, ("semesternr", FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
||||
, ("field-name" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName))
|
||||
, ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
||||
, ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey))
|
||||
, ("field" , FilterColumn $ E.anyFilter
|
||||
[ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName)
|
||||
, E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand)
|
||||
, E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey)
|
||||
] )
|
||||
, ("degree" , FilterColumn $ E.anyFilter
|
||||
[ E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeName)
|
||||
, E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand)
|
||||
, E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey)
|
||||
] )
|
||||
, ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
||||
-- , ("course-registration", error "TODO") -- TODO
|
||||
-- , ("course-user-note", error "TODO") -- TODO
|
||||
]
|
||||
dbtFilterUI mPrev = mconcat
|
||||
[ fltrUserNameEmailUI mPrev
|
||||
, fltrUserMatriclenrUI mPrev
|
||||
, prismAForm (singletonFilter "degree") mPrev $ aopt (searchField False) (fslI MsgStudyFeatureDegree)
|
||||
, prismAForm (singletonFilter "field") mPrev $ aopt (searchField False) (fslI MsgCourseStudyFeature)
|
||||
]
|
||||
dbtParams = DBParamsForm
|
||||
{ dbParamsFormMethod = POST
|
||||
|
||||
@ -16,16 +16,18 @@ nullaryPathPiece ''HelpIdentOptions (camelToPathPiece' 1)
|
||||
embedRenderMessage ''UniWorX ''HelpIdentOptions (("Help" <>) . dropPrefix "HI")
|
||||
|
||||
data HelpForm = HelpForm
|
||||
{ hfReferer:: Maybe (Route UniWorX)
|
||||
, hfUserId :: Either (Maybe Address) UserId
|
||||
, hfRequest:: Text
|
||||
{ hfReferer :: Maybe (Route UniWorX)
|
||||
, hfUserId :: Either (Maybe Address) UserId
|
||||
, hfSubject :: Maybe Text
|
||||
, hfRequest :: Text
|
||||
}
|
||||
|
||||
helpForm :: Maybe (Route UniWorX) -> Maybe UserId -> AForm _ HelpForm
|
||||
helpForm mReferer mUid = HelpForm
|
||||
helpForm :: (forall msg. RenderMessage UniWorX msg => msg -> Text) -> Maybe (Route UniWorX) -> Maybe UserId -> AForm _ HelpForm
|
||||
helpForm mr mReferer mUid = HelpForm
|
||||
<$> aopt routeField (fslI MsgHelpProblemPage & inputReadonly) (Just <$> mReferer)
|
||||
<*> multiActionA (fslI MsgHelpAnswer) identActions (HIUser <$ mUid)
|
||||
<*> (unTextarea <$> areq textareaField (fslI MsgHelpRequest) Nothing)
|
||||
<*> aopt textField (fslpI MsgHelpSubject $ mr MsgHelpSubject) Nothing
|
||||
<*> (unTextarea <$> areq textareaField (fslpI MsgHelpRequest $ mr MsgHelpRequest) Nothing)
|
||||
where
|
||||
identActions :: Map _ (AForm _ (Either (Maybe Address) UserId))
|
||||
identActions = Map.fromList $ case mUid of
|
||||
@ -33,7 +35,7 @@ helpForm mReferer mUid = HelpForm
|
||||
Nothing -> defaultActions
|
||||
|
||||
defaultActions =
|
||||
[ (HIEmail, Left . Just <$> (Address <$> aopt textField (fslI MsgName) Nothing <*> apreq emailField (fslI MsgEMail) Nothing))
|
||||
[ (HIEmail, Left . Just <$> (Address <$> aopt textField (fslpI MsgName $ mr MsgName) Nothing <*> apreq emailField (fslpI MsgEMail $ mr MsgEMail) Nothing))
|
||||
, (HIAnonymous, pure $ Left Nothing)
|
||||
]
|
||||
|
||||
@ -43,19 +45,16 @@ postHelpR = do
|
||||
mUid <- maybeAuthId
|
||||
mReferer <- flip formResultMaybe return <=< runInputGetResult $ iopt routeField (toPathPiece GetReferer)
|
||||
isModal <- hasCustomHeader HeaderIsModal
|
||||
MsgRenderer mr <- getMsgRenderer
|
||||
|
||||
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid
|
||||
let form = wrapForm formWidget def
|
||||
{ formAction = Just $ SomeRoute HelpR
|
||||
, formEncoding = formEnctype
|
||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
||||
}
|
||||
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mr mReferer mUid
|
||||
|
||||
formResultModal res HelpR $ \HelpForm{..} -> do
|
||||
now <- liftIO getCurrentTime
|
||||
hfReferer' <- traverse toTextUrl hfReferer
|
||||
queueJob' JobHelpRequest
|
||||
{ jSender = hfUserId
|
||||
, jHelpSubject = hfSubject
|
||||
, jHelpRequest = hfRequest
|
||||
, jRequestTime = now
|
||||
, jReferer = hfReferer'
|
||||
@ -64,4 +63,8 @@ postHelpR = do
|
||||
|
||||
defaultLayout $ do
|
||||
setTitleI MsgHelpTitle
|
||||
$(widgetFile "help")
|
||||
wrapForm $(widgetFile "help") def
|
||||
{ formAction = Just $ SomeRoute HelpR
|
||||
, formEncoding = formEnctype
|
||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
||||
}
|
||||
|
||||
@ -170,4 +170,4 @@ homeUpcomingSheets uid = do
|
||||
, dbtParams = def
|
||||
, dbtIdent = "upcoming-sheets" :: Text
|
||||
}
|
||||
$(widgetFile "home/upcomingSheets")
|
||||
$(widgetFile "home/upcomingSheets")
|
||||
@ -116,10 +116,11 @@ postProfileR = do
|
||||
|
||||
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
||||
setTitle . toHtml $ "Profil " <> userIdent
|
||||
wrapForm formWidget def
|
||||
{ formAction = Just $ SomeRoute ProfileR
|
||||
, formEncoding = formEnctype
|
||||
}
|
||||
let settingsForm = wrapForm formWidget def
|
||||
{ formAction = Just $ SomeRoute ProfileR
|
||||
, formEncoding = formEnctype
|
||||
}
|
||||
$(widgetFile "profile")
|
||||
|
||||
|
||||
getProfileDataR :: Handler Html
|
||||
|
||||
@ -199,11 +199,12 @@ getSheetListR tid ssh csh = do
|
||||
let stats = sheetTypeSum sheetType in -- for statistics over all shown rows
|
||||
case mbSub of
|
||||
Nothing -> cellTell mempty $ stats Nothing
|
||||
(Just (Entity sid Submission{..})) ->
|
||||
(Just (Entity sid sub@Submission{..})) ->
|
||||
let mkCid = encrypt sid
|
||||
mkRoute = do
|
||||
cid' <- mkCid
|
||||
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
||||
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
||||
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||
in cellTell acell $ stats submissionRatingPoints
|
||||
|
||||
|
||||
@ -462,10 +462,10 @@ sinkSubmission userId mExists isUpdate = do
|
||||
case isUpdate of
|
||||
False -> lift . insert_ $ SubmissionEdit userId now submissionId
|
||||
True -> do
|
||||
Submission{submissionRatingTime} <- lift $ getJust submissionId
|
||||
when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True }
|
||||
lift $ update submissionId [ SubmissionRatingBy =. Just userId, SubmissionRatingTime =. Just now ]
|
||||
-- TODO: Should submissionRatingAssigned change here if userId changes?
|
||||
Submission{submissionRatingTime, submissionRatingBy} <- lift $ getJust submissionId
|
||||
when (submissionRatingBy == Just userId) $ do
|
||||
when (isNothing submissionRatingTime) $ tellSt mempty { sinkSubmissionNotifyRating = Any True }
|
||||
lift $ update submissionId [ SubmissionRatingTime =. Just now ]
|
||||
tellSt $ mempty{ sinkSubmissionTouched = Any True }
|
||||
|
||||
finalize :: SubmissionSinkState -> YesodJobDB UniWorX ()
|
||||
|
||||
@ -63,6 +63,7 @@ headedRowSelector toExternal fromExternal attrs colonnade tdata = do
|
||||
-> return Nothing
|
||||
|
||||
view _ name attributes val _ =
|
||||
-- TODO: move this to a *.hamlet file
|
||||
[whamlet|
|
||||
<label style="display: block">
|
||||
<input type=checkbox name=#{name} value=#{toPathPiece extId} *{attributes} :isRight val:checked>
|
||||
|
||||
@ -595,7 +595,7 @@ instance Monoid x => Monoid (DBCell (RWST (Maybe (Env, FileEnv), UniWorX, [Lang]
|
||||
instance IsDBTable m a => IsString (DBCell m a) where
|
||||
fromString = cell . fromString
|
||||
|
||||
|
||||
-- | DB-backed tables with pagination, may short-circuit a handler
|
||||
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> DB (DBResult m x)
|
||||
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
||||
let
|
||||
|
||||
@ -15,7 +15,8 @@ import Yesod.Auth as Import
|
||||
import Yesod.Core.Types as Import (loggerSet)
|
||||
import Yesod.Default.Config2 as Import
|
||||
import Utils as Import
|
||||
import Utils.Modal as Import
|
||||
import Utils.Frontend.Modal as Import
|
||||
import Utils.Frontend.I18n as Import
|
||||
import Yesod.Core.Json as Import (provideJson)
|
||||
import Yesod.Core.Types.Instances as Import ()
|
||||
|
||||
|
||||
@ -16,10 +16,11 @@ import Data.Bitraversable
|
||||
|
||||
dispatchJobHelpRequest :: Either (Maybe Address) UserId
|
||||
-> UTCTime
|
||||
-> Maybe Text -- ^ Help Subject
|
||||
-> Text -- ^ Help Request
|
||||
-> Maybe Text -- ^ Referer
|
||||
-> Handler ()
|
||||
dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do
|
||||
dispatchJobHelpRequest jSender jRequestTime jHelpSubject jHelpRequest jReferer = do
|
||||
supportAddress <- getsYesod $ appMailSupport . appSettings
|
||||
userInfo <- bitraverse return (runDB . getEntity) jSender
|
||||
let userAddress = either
|
||||
@ -28,8 +29,9 @@ dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do
|
||||
userInfo
|
||||
mailT def $ do
|
||||
_mailTo .= [supportAddress]
|
||||
whenIsJust userAddress $ addMailHeader "Reply-To" . renderAddress
|
||||
setSubjectI MsgMailSubjectSupport
|
||||
whenIsJust userAddress (_mailFrom .=)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "no"
|
||||
setSubjectI $ maybe MsgMailSubjectSupport MsgMailSubjectSupportCustom jHelpSubject
|
||||
setDate jRequestTime
|
||||
rtime <- formatTimeMail SelFormatDateTime jRequestTime
|
||||
addPart ($(ihamletFile "templates/mail/support.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||
|
||||
@ -22,6 +22,7 @@ dispatchNotificationCorrectionsAssigned nUser nSheet jRecipient = do
|
||||
]
|
||||
return (course, sheet, nbrSubs)
|
||||
when (nbrSubs > 0) . userMailT jRecipient $ do
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectCorrectionsAssigned courseShorthand sheetName
|
||||
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
|
||||
@ -19,6 +19,7 @@ dispatchNotificationCorrectionsNotDistributed nSheet jRecipient = do
|
||||
]
|
||||
return (course, sheet, nbrSubs)
|
||||
when (nbrSubs > 0) . userMailT jRecipient $ do
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectSubmissionsUnassigned courseShorthand sheetName
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
let termDesc = mr . ShortTermIdentifier $ unTermKey courseTerm
|
||||
|
||||
@ -17,6 +17,7 @@ dispatchNotificationSheetActive nSheet jRecipient = userMailT jRecipient $ do
|
||||
sheet <- getJust nSheet
|
||||
course <- belongsToJust sheetCourse sheet
|
||||
return (course, sheet)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectSheetActive courseShorthand sheetName
|
||||
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
|
||||
@ -20,6 +20,7 @@ dispatchNotificationSheetSoonInactive nSheet jRecipient = userMailT jRecipient $
|
||||
sheet <- getJust nSheet
|
||||
course <- belongsToJust sheetCourse sheet
|
||||
return (course, sheet)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectSheetSoonInactive courseShorthand sheetName
|
||||
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
@ -45,6 +46,7 @@ dispatchNotificationSheetInactive nSheet jRecipient = userMailT jRecipient $ do
|
||||
-- E.distinctOn [E.don (subUser E.^. SubmissionUserUser)] -- Not necessary due to UniqueSubmisionUser
|
||||
return (E.countRows :: E.SqlExpr (E.Value Int64))
|
||||
return (course, sheet, nrSubs, nrSubmitters)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectSheetInactive courseShorthand sheetName
|
||||
|
||||
MsgRenderer mr <- getMailMsgRenderer
|
||||
|
||||
@ -22,6 +22,7 @@ dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipien
|
||||
course <- belongsToJust sheetCourse sheet
|
||||
corrector <- traverse getJust submissionRatingBy
|
||||
return (course, sheet, submission, corrector)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
||||
|
||||
csid <- encrypt nSubmission
|
||||
|
||||
@ -19,6 +19,7 @@ dispatchNotificationUserRightsUpdate nUser _originalRights jRecipient = userMai
|
||||
adminSchools <- getSchoolsOf nUser UserAdminSchool UserAdminUser
|
||||
lecturerSchools <- getSchoolsOf nUser UserLecturerSchool UserLecturerUser
|
||||
return (user,adminSchools,lecturerSchools)
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI $ MsgMailSubjectUserRightsUpdate userDisplayName
|
||||
-- MsgRenderer mr <- getMailMsgRenderer
|
||||
addAlternatives $ do
|
||||
|
||||
@ -13,6 +13,7 @@ import Utils.Lens
|
||||
dispatchJobSendTestEmail :: Email -> MailContext -> Handler ()
|
||||
dispatchJobSendTestEmail jEmail jMailContext = mailT jMailContext $ do
|
||||
_mailTo .= [Address Nothing jEmail]
|
||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||
setSubjectI MsgMailTestSubject
|
||||
now <- liftIO getCurrentTime
|
||||
nDT <- formatTimeMail SelFormatDateTime now
|
||||
|
||||
@ -18,7 +18,10 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
||||
| JobQueueNotification { jNotification :: Notification }
|
||||
| JobHelpRequest { jSender :: Either (Maybe Address) UserId
|
||||
, jRequestTime :: UTCTime
|
||||
, jHelpRequest :: Text, jReferer :: Maybe Text }
|
||||
, jHelpSubject :: Maybe Text
|
||||
, jHelpRequest :: Text
|
||||
, jReferer :: Maybe Text
|
||||
}
|
||||
| JobSetLogSettings { jInstance :: InstanceId, jLogSettings :: LogSettings }
|
||||
| JobDistributeCorrections { jSheet :: SheetId }
|
||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||
|
||||
16
src/Mail.hs
16
src/Mail.hs
@ -27,7 +27,7 @@ module Mail
|
||||
, setSubjectI, setMailObjectId, setMailObjectId'
|
||||
, setDate, setDateCurrent
|
||||
, setMailSmtpData
|
||||
, _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailParts
|
||||
, _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailHeader, _mailParts
|
||||
, _partType, _partEncoding, _partFilename, _partHeaders, _partContent
|
||||
) where
|
||||
|
||||
@ -99,9 +99,18 @@ import Data.Universe.Instances.Reverse.Hashable ()
|
||||
|
||||
import GHC.Exts (IsList)
|
||||
|
||||
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||
|
||||
import Data.CaseInsensitive (CI)
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
|
||||
|
||||
makeLenses_ ''Mail
|
||||
makeLenses_ ''Part
|
||||
|
||||
_mailHeader :: CI ByteString -> Traversal' Mail Text
|
||||
_mailHeader hdrName = _mailHeaders . traverse . filtered (views _1 $ (== hdrName) . CI.mk) . _2
|
||||
|
||||
|
||||
newtype MailT m a = MailT { _unMailT :: RWST MailContext MailSmtpData Mail m a }
|
||||
deriving newtype ( MonadTrans, Monad, Functor, MonadFail, Applicative, Alternative, MonadPlus
|
||||
@ -443,7 +452,10 @@ setDate time = do
|
||||
|
||||
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m ()
|
||||
setMailSmtpData = do
|
||||
Address _ from <- use _mailFrom
|
||||
Just (Address _ from) <- runMaybeT $ asum
|
||||
[ MaybeT . preuses (_mailHeader "Sender") $ fromString . unpack
|
||||
, use _mailFrom
|
||||
]
|
||||
recps <- Set.fromList . map addressEmail . concat <$> forM [_mailTo, _mailCc, _mailBcc] use
|
||||
|
||||
tell $ mempty { smtpRecipients = recps }
|
||||
|
||||
41
src/Utils/Frontend/I18n.hs
Normal file
41
src/Utils/Frontend/I18n.hs
Normal file
@ -0,0 +1,41 @@
|
||||
module Utils.Frontend.I18n
|
||||
( FrontendMessage(..)
|
||||
) where
|
||||
|
||||
import ClassyPrelude
|
||||
import Data.Universe
|
||||
|
||||
import Control.Lens
|
||||
import Utils.PathPiece
|
||||
|
||||
import Web.PathPieces
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types (toJSONKeyText)
|
||||
import Data.Aeson.TH
|
||||
import qualified Data.Char as Char
|
||||
|
||||
|
||||
-- | I18n-Messages used in JavaScript-Frontend
|
||||
--
|
||||
-- Only nullary constructors are supported
|
||||
--
|
||||
-- @MsgCamelCaseIdentifier@ gets translated to @camelCaseIdentifier@ in Frontend (see `nullaryPathPiece` and `deriveJSON` below)
|
||||
data FrontendMessage = MsgFilesSelected
|
||||
| MsgSelectFile
|
||||
| MsgSelectFiles
|
||||
| MsgAsyncFormFailure
|
||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||
instance Universe FrontendMessage
|
||||
instance Finite FrontendMessage
|
||||
instance Hashable FrontendMessage
|
||||
|
||||
nullaryPathPiece ''FrontendMessage $ over _head Char.toLower . mconcat . drop 1 . splitCamel
|
||||
|
||||
deriveJSON defaultOptions
|
||||
{ constructorTagModifier = over _head Char.toLower . mconcat . drop 1 . splitCamel
|
||||
} ''FrontendMessage
|
||||
|
||||
instance ToJSONKey FrontendMessage where
|
||||
toJSONKey = toJSONKeyText toPathPiece
|
||||
instance FromJSONKey FrontendMessage where
|
||||
fromJSONKey = FromJSONKeyTextParser $ parseJSON . String
|
||||
@ -1,4 +1,4 @@
|
||||
module Utils.Modal
|
||||
module Utils.Frontend.Modal
|
||||
( Modal(..)
|
||||
, customModal
|
||||
, modal
|
||||
@ -7,7 +7,6 @@ module Utils.Modal
|
||||
import ClassyPrelude.Yesod
|
||||
|
||||
import Control.Lens
|
||||
import Control.Lens.Extras (is)
|
||||
import Utils.Route
|
||||
|
||||
import Settings (widgetFile)
|
||||
@ -22,13 +21,11 @@ data Modal site = Modal
|
||||
|
||||
customModal :: Modal site -> WidgetT site IO ()
|
||||
customModal Modal{..} = do
|
||||
let isDynamic = is _Left modalContent
|
||||
modalId' <- maybe newIdent return modalId
|
||||
triggerId' <- maybe newIdent return modalTriggerId
|
||||
|
||||
$(widgetFile "widgets/modal/modal")
|
||||
|
||||
route <- for (modalContent ^? _Left) toTextUrl
|
||||
route <- traverse toTextUrl $ modalContent ^? _Left
|
||||
modalTrigger route triggerId'
|
||||
|
||||
-- | Create a link to a modal
|
||||
@ -1,23 +1,3 @@
|
||||
/* ALERTS */
|
||||
/**
|
||||
.alert
|
||||
Regular Info Alert
|
||||
Disappears automatically after 30 seconds
|
||||
Disappears after x seconds if explicitly specified via data-decay='x'
|
||||
Can be told not to disappear with data-decay='0'
|
||||
|
||||
.alert-success
|
||||
Disappears automatically after 30 seconds
|
||||
|
||||
.alert-warning
|
||||
Does not disappear
|
||||
Orange regardless of user's selected theme
|
||||
|
||||
.alert-error
|
||||
Does not disappear
|
||||
Red regardless of user's selected theme
|
||||
|
||||
*/
|
||||
.alerts {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
@ -55,10 +55,6 @@
|
||||
.asidenav__box-title {
|
||||
font-size: 18px;
|
||||
padding-left: 10px;
|
||||
|
||||
&.js-show-hide__toggle::before {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,18 +90,9 @@
|
||||
margin-top: 30px;
|
||||
background-color: transparent;
|
||||
transition: all .2s ease;
|
||||
padding: 30px 13px 10px;
|
||||
padding: 10px 13px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--color-grey);
|
||||
|
||||
&.js-show-hide__toggle {
|
||||
|
||||
&::before {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* LOGO */
|
||||
@ -361,9 +348,5 @@
|
||||
background-color: var(--color-lightwhite);
|
||||
}
|
||||
}
|
||||
|
||||
.js-show-hide__toggle::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.async-form-response {
|
||||
.async-form__response {
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@ -7,15 +7,15 @@
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.async-form-response::before,
|
||||
.async-form-response::after {
|
||||
.async-form__response::before,
|
||||
.async-form__response::after {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.async-form-response--success::before {
|
||||
.async-form__response--success::before {
|
||||
content: '';
|
||||
width: 17px;
|
||||
height: 28px;
|
||||
@ -24,7 +24,7 @@
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.async-form-response--info::before {
|
||||
.async-form__response--info::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 30px;
|
||||
@ -32,7 +32,7 @@
|
||||
background-color: #777;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.async-form-response--info::after {
|
||||
.async-form__response--info::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
@ -40,14 +40,14 @@
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.async-form-response--warning::before {
|
||||
.async-form__response--warning::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 30px;
|
||||
background-color: rgb(255, 187, 0);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.async-form-response--warning::after {
|
||||
.async-form__response--warning::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
@ -56,14 +56,14 @@
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.async-form-response--error::before {
|
||||
.async-form__response--error::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 40px;
|
||||
background-color: #940d0d;
|
||||
transform: translateX(-50%) rotate(-45deg);
|
||||
}
|
||||
.async-form-response--error::after {
|
||||
.async-form__response--error::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 40px;
|
||||
@ -71,7 +71,7 @@
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.async-form-loading {
|
||||
.async-form--loading {
|
||||
opacity: 0.1;
|
||||
transition: opacity 800ms ease-out;
|
||||
pointer-events: none;
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
|
||||
label {
|
||||
display: block;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #f3f3f3;
|
||||
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05);
|
||||
border: 2px solid var(--color-primary);
|
||||
@ -55,14 +55,16 @@
|
||||
:checked + label::before {
|
||||
background-color: white;
|
||||
transform: rotate(45deg);
|
||||
left: 4px;
|
||||
left: 2px;
|
||||
top: 11px;
|
||||
}
|
||||
|
||||
:checked + label::after {
|
||||
background-color: white;
|
||||
transform: rotate(-45deg);
|
||||
top: 11px;
|
||||
width: 13px;
|
||||
top: 9px;
|
||||
width: 12px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
[disabled] + label {
|
||||
|
||||
@ -17,7 +17,7 @@ fieldset {
|
||||
}
|
||||
}
|
||||
|
||||
[data-autosubmit][type="submit"] {
|
||||
[uw-auto-submit-button][type="submit"] {
|
||||
animation: fade-in 500ms ease-in-out backwards;
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
|
||||
@ -25,27 +25,26 @@
|
||||
color: var(--color-fontsec);
|
||||
}
|
||||
|
||||
.form-group__label {
|
||||
.form-group-label {
|
||||
font-weight: 600;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.form-group__hint {
|
||||
.form-group-label__hint {
|
||||
margin-top: 7px;
|
||||
color: var(--color-fontsec);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group--required {
|
||||
|
||||
.form-group__label::after {
|
||||
.form-group-label__caption::after {
|
||||
content: ' *';
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--optional {
|
||||
.form-group__label::after {
|
||||
.form-group-label__caption::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
@ -196,7 +195,11 @@ option {
|
||||
}
|
||||
}
|
||||
|
||||
/* CUSTOM FILE INPUT */
|
||||
/* FILE INPUT */
|
||||
.file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-input__label {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@ -206,6 +209,31 @@ option {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-input__input--hidden {
|
||||
display: none;
|
||||
.file-input__info {
|
||||
font-size: .9rem;
|
||||
font-style: italic;
|
||||
margin: 10px 0;
|
||||
color: var(--color-fontsec);
|
||||
}
|
||||
|
||||
.file-input__list {
|
||||
margin-left: 40px;
|
||||
margin-top: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* PREVIOUSLY UPLOADED FILES */
|
||||
|
||||
.file-uploads-label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,9 +14,6 @@
|
||||
padding: 0 65px 0 20px;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
transition:
|
||||
opacity .2s .1s ease-in-out,
|
||||
transform .3s ease-in-out;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
@ -25,6 +22,9 @@
|
||||
pointer-events: auto;
|
||||
z-index: 200;
|
||||
transform: translate(-50%, -50%) scale(1, 1);
|
||||
transition:
|
||||
opacity .2s .1s ease-in-out,
|
||||
transform .3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.radio-group__option {
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.radio {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
$show-hide-toggle-size: 6px;
|
||||
|
||||
.js-show-hide__toggle {
|
||||
.show-hide__toggle {
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 3px 7px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-grey-lighter);
|
||||
@ -12,32 +11,33 @@ $show-hide-toggle-size: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.js-show-hide__toggle::before {
|
||||
.show-hide__toggle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: $show-hide-toggle-size;
|
||||
height: $show-hide-toggle-size;
|
||||
left: -15px;
|
||||
top: 12px - $show-hide-toggle-size / 2;
|
||||
top: 50%;
|
||||
color: var(--color-primary);
|
||||
border-right: 2px solid currentColor;
|
||||
border-top: 2px solid currentColor;
|
||||
transition: transform .2s ease;
|
||||
transform-origin: ($show-hide-toggle-size / 2);
|
||||
transform: translateY($show-hide-toggle-size) rotate(-45deg);
|
||||
transform: translateY(-50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.js-show-hide__target {
|
||||
transition: all .2s ease;
|
||||
.show-hide__toggle--right::before {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
|
||||
.js-show-hide--collapsed {
|
||||
.show-hide--collapsed {
|
||||
|
||||
.js-show-hide__toggle::before {
|
||||
transform: translateY($show-hide-toggle-size / 3) rotate(135deg);
|
||||
.show-hide__toggle::before {
|
||||
transform: translateY(-50%) rotate(135deg);
|
||||
}
|
||||
|
||||
.js-show-hide__target {
|
||||
:not(.show-hide__toggle) {
|
||||
display: block;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
(function collonadeClosure() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
window.utils.httpClient = (function() {
|
||||
window.HttpClient = (function() {
|
||||
|
||||
function _fetch(url, method, additionalHeaders, body) {
|
||||
var requestOptions = {
|
||||
44
static/js/services/i18n.js
Normal file
44
static/js/services/i18n.js
Normal file
@ -0,0 +1,44 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* I18n
|
||||
*
|
||||
* This module stores and serves translated strings, according to the users language settings.
|
||||
*
|
||||
* Translations are stored in /messages/frontend/*.msg.
|
||||
*
|
||||
* To make additions to any of these files accessible to JavaScrip Utilities
|
||||
* you need to add them to the respective *.msg file and to the list of FrontendMessages
|
||||
* in /src/Utils/Frontend/I18n.hs.
|
||||
*
|
||||
*/
|
||||
window.I18n = (function() {
|
||||
|
||||
var translations = {};
|
||||
|
||||
function addTranslation(id, translation) {
|
||||
translations[id] = translation;
|
||||
}
|
||||
|
||||
function addManyTranslations(manyTranslations) {
|
||||
Object.keys(manyTranslations).forEach(function(key) {
|
||||
addTranslation(key, manyTranslations[key]);
|
||||
});
|
||||
}
|
||||
|
||||
function getTranslation(id) {
|
||||
if (!translations[id]) {
|
||||
throw new Error('I18N Error: Translation missing for »' + id + '«!');
|
||||
}
|
||||
return translations[id];
|
||||
}
|
||||
|
||||
// public API
|
||||
return {
|
||||
add: addTranslation,
|
||||
addMany: addManyTranslations,
|
||||
get: getTranslation,
|
||||
};
|
||||
})();
|
||||
})();
|
||||
155
static/js/services/utilRegistry.js
Normal file
155
static/js/services/utilRegistry.js
Normal file
@ -0,0 +1,155 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var registeredUtils = [];
|
||||
var activeUtilInstances = [];
|
||||
|
||||
var DEBUG_MODE = /localhost/.test(window.location.href) && 0;
|
||||
|
||||
// Registry
|
||||
// (revealing module pattern)
|
||||
window.UtilRegistry = (function() {
|
||||
|
||||
/**
|
||||
* function registerUtil
|
||||
*
|
||||
* utils need to have at least these properties:
|
||||
* name: string | utils name, e.g. 'example'
|
||||
* selector: string | utils selector, e.g. '[uw-example]'
|
||||
* setup: Function | utils setup function, see below
|
||||
*
|
||||
* setup function must return instance object with at least these properties:
|
||||
* name: string | utils name
|
||||
* element: HTMLElement | element the util is applied to
|
||||
* destroy: Function | function to destroy the util and remove any listeners
|
||||
*
|
||||
* @param util Object Utility that should be added to the registry
|
||||
*/
|
||||
function registerUtil(util) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.log('registering util "' + util.name + '"');
|
||||
console.log({ util });
|
||||
}
|
||||
|
||||
registeredUtils.push(util);
|
||||
}
|
||||
|
||||
function deregisterUtil(name, destroy) {
|
||||
var utilIndex = _findUtilIndex(name);
|
||||
|
||||
if (utilIndex >= 0) {
|
||||
if (destroy === true) {
|
||||
_destroyUtilInstances(name);
|
||||
}
|
||||
|
||||
registeredUtils.splice(utilIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function setupAllUtils(scope) {
|
||||
if (DEBUG_MODE > 1) {
|
||||
console.info('registered js utilities:');
|
||||
console.table(registeredUtils);
|
||||
}
|
||||
|
||||
registeredUtils.forEach(function(util) {
|
||||
setupUtil(util, scope);
|
||||
});
|
||||
}
|
||||
|
||||
function setupUtil(util, scope) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.log('setting up util', { util });
|
||||
}
|
||||
|
||||
scope = scope || document.body;
|
||||
|
||||
if (util && typeof util.setup === 'function') {
|
||||
const elements = _findUtilElements(util, scope);
|
||||
|
||||
elements.forEach(function(element) {
|
||||
var utilInstance = null;
|
||||
|
||||
try {
|
||||
utilInstance = util.setup(element);
|
||||
} catch(err) {
|
||||
if (DEBUG_MODE > 0) {
|
||||
console.warn('Error while trying to initialize a utility!', { util , element, err });
|
||||
}
|
||||
}
|
||||
|
||||
if (utilInstance) {
|
||||
if (DEBUG_MODE > 2) {
|
||||
console.info('Got utility instance for utility "' + util.name + '"', { utilInstance });
|
||||
}
|
||||
|
||||
activeUtilInstances.push(utilInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findUtil(name) {
|
||||
return registeredUtils.find(function(util) {
|
||||
return util.name === name;
|
||||
});
|
||||
}
|
||||
|
||||
function _findUtilElements(util, scope) {
|
||||
if (scope && scope.matches(util.selector)) {
|
||||
return [scope];
|
||||
}
|
||||
return Array.from(scope.querySelectorAll(util.selector));
|
||||
}
|
||||
|
||||
function _findUtilIndex(name) {
|
||||
return registeredUtils.findIndex(function(util) {
|
||||
return util.name === name;
|
||||
});
|
||||
}
|
||||
|
||||
function _destroyUtilInstances(name) {
|
||||
activeUtilInstances
|
||||
.map(function(util, index) {
|
||||
return {
|
||||
util: util,
|
||||
index: index,
|
||||
};
|
||||
}).filter(function(activeUtil) {
|
||||
// find utils instances to destroy
|
||||
return activeUtil.util.name === name;
|
||||
}).forEach(function(activeUtil) {
|
||||
// destroy util instance
|
||||
activeUtil.util.destroy();
|
||||
delete activeUtilInstances[activeUtil.index];
|
||||
});
|
||||
|
||||
// get rid of now empty array slots
|
||||
activeUtilInstances = activeUtilInstances.filter(function(util) {
|
||||
return !!util;
|
||||
})
|
||||
}
|
||||
|
||||
// public API
|
||||
return {
|
||||
register: registerUtil,
|
||||
deregister: deregisterUtil,
|
||||
setupAll: setupAllUtils,
|
||||
setup: setupUtil,
|
||||
find: findUtil,
|
||||
}
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.UtilRegistry.setupAll();
|
||||
});
|
||||
|
||||
|
||||
// REMOVE ME. JUST HERE TO AVOID JS ERRORS
|
||||
window.utils = {
|
||||
setup: function(name) {
|
||||
console.log('not really setting up', name);
|
||||
},
|
||||
};
|
||||
|
||||
})();
|
||||
@ -1,68 +1,126 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Alerts Utility
|
||||
* makes alerts interactive
|
||||
*
|
||||
* Attribute: uw-alerts
|
||||
*
|
||||
* Types of alerts:
|
||||
* [default]
|
||||
* Regular Info Alert
|
||||
* Disappears automatically after 30 seconds
|
||||
* Disappears after x seconds if explicitly specified via data-decay='x'
|
||||
* Can be told not to disappear with data-decay='0'
|
||||
*
|
||||
* [success]
|
||||
* Currently no special visual appearance
|
||||
* Disappears automatically after 30 seconds
|
||||
*
|
||||
* [warning]
|
||||
* Will be coloured warning-orange regardless of user's selected theme
|
||||
* Does not disappear
|
||||
*
|
||||
* [error]
|
||||
* Will be coloured error-red regardless of user's selected theme
|
||||
* Does not disappear
|
||||
*
|
||||
* Example usage:
|
||||
* <div .alerts uw-alerts>
|
||||
* <div .alerts__toggler>
|
||||
* <div .alert.alert-info>
|
||||
* <div .alert__closer>
|
||||
* <div .alert__icon>
|
||||
* <div .alert__content>
|
||||
* This is some information
|
||||
*
|
||||
*/
|
||||
|
||||
var ALERTS_CLASS = 'alerts';
|
||||
var ALERTS_UTIL_NAME = 'alerts';
|
||||
var ALERTS_UTIL_SELECTOR = '[uw-alerts]';
|
||||
|
||||
var ALERTS_INITIALIZED_CLASS = 'alerts--initialized';
|
||||
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
|
||||
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
|
||||
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
|
||||
|
||||
var ALERT_CLASS = 'alert';
|
||||
var ALERT_INITIALIZED_CLASS = 'alert--initialized';
|
||||
var ALERT_CLOSER_CLASS = 'alert__closer';
|
||||
var ALERT_INVISIBLE_CLASS = 'alert--invisible';
|
||||
var ALERT_AUTO_HIDE_DELAY = 10;
|
||||
var ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success';
|
||||
|
||||
var JS_INITIALIZED_CLASS = 'js-initialized';
|
||||
|
||||
window.utils.alerts = function(alertsEl) {
|
||||
|
||||
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!alertsEl || !alertsEl.classList.contains(ALERTS_CLASS)) {
|
||||
throw new Error('utils.alerts has to be called with alerts element');
|
||||
}
|
||||
|
||||
var alertsUtil = function(element) {
|
||||
var togglerCheckRequested = false;
|
||||
var togglerElement;
|
||||
var alertElements;
|
||||
|
||||
var togglerEl = alertsEl.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Alerts util has to be called with an element!');
|
||||
}
|
||||
|
||||
var alertElements = Array.from(alertsEl.querySelectorAll('.' + ALERT_CLASS))
|
||||
.filter(function(alert) {
|
||||
return !alert.classList.contains(JS_INITIALIZED_CLASS);
|
||||
if (element.classList.contains(ALERTS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
togglerElement = element.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
||||
alertElements = gatherAlertElements();
|
||||
|
||||
initToggler();
|
||||
initAlerts();
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ALERTS_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function gatherAlertElements() {
|
||||
return Array.from(element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) {
|
||||
return !alert.classList.contains(ALERT_INITIALIZED_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
function initToggler() {
|
||||
togglerEl.addEventListener('click', function() {
|
||||
togglerElement.addEventListener('click', function() {
|
||||
alertElements.forEach(function(alertEl) {
|
||||
toggleAlert(alertEl, true);
|
||||
});
|
||||
togglerEl.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
||||
togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
||||
});
|
||||
alertsEl.classList.add(JS_INITIALIZED_CLASS);
|
||||
element.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
function initAlert(alertEl) {
|
||||
function initAlerts() {
|
||||
alertElements.forEach(initAlert);
|
||||
}
|
||||
|
||||
function initAlert(alertElement) {
|
||||
var autoHideDelay = ALERT_AUTO_HIDE_DELAY;
|
||||
if (alertEl.dataset.decay) {
|
||||
autoHideDelay = parseInt(alertEl.dataset.decay, 10);
|
||||
if (alertElement.dataset.decay) {
|
||||
autoHideDelay = parseInt(alertElement.dataset.decay, 10);
|
||||
}
|
||||
|
||||
var closeEl = alertEl.querySelector('.' + ALERT_CLOSER_CLASS);
|
||||
var closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS);
|
||||
closeEl.addEventListener('click', function() {
|
||||
toggleAlert(alertEl);
|
||||
toggleAlert(alertElement);
|
||||
});
|
||||
|
||||
if (autoHideDelay > 0 && alertEl.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
||||
if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
||||
window.setTimeout(function() {
|
||||
toggleAlert(alertEl);
|
||||
toggleAlert(alertElement);
|
||||
}, autoHideDelay * 1000);
|
||||
}
|
||||
|
||||
alertEl.classList.add(JS_INITIALIZED_CLASS);
|
||||
alertElement.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
function toggleAlert(alertEl, visible) {
|
||||
@ -80,17 +138,19 @@
|
||||
}, true);
|
||||
|
||||
window.setTimeout(function() {
|
||||
togglerEl.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
|
||||
togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
|
||||
togglerCheckRequested = false;
|
||||
}, ALERTS_TOGGLER_APPEAR_DELAY);
|
||||
}
|
||||
|
||||
initToggler();
|
||||
alertElements.forEach(initAlert);
|
||||
|
||||
return {
|
||||
scope: alertsEl,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: ALERTS_UTIL_NAME,
|
||||
selector: ALERTS_UTIL_SELECTOR,
|
||||
setup: alertsUtil,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,31 +1,74 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Asidenav Utility
|
||||
* Correctly positions hovered asidenav submenus and handles the favorites button on mobile
|
||||
*
|
||||
* Attribute: uw-asidenav
|
||||
*
|
||||
* Example usage:
|
||||
* <div uw-asidenav>
|
||||
* <div .asidenav>
|
||||
* <div .asidenav__box>
|
||||
* <ul .asidenav__list.list--iconless>
|
||||
* <li .asidenav__list-item>
|
||||
* <a .asidenav__link-wrapper href="#">
|
||||
* <div .asidenav__link-shorthand>EIP
|
||||
* <div .asidenav__link-label>Einführung in die Programmierung
|
||||
* <div .asidenav__nested-list-wrapper>
|
||||
* <ul .asidenav__nested-list.list--iconless>
|
||||
* Übungsblätter
|
||||
* ...
|
||||
*
|
||||
*/
|
||||
|
||||
var ASIDENAV_UTIL_NAME = 'asidenav';
|
||||
var ASIDENAV_UTIL_SELECTOR = '[uw-asidenav]';
|
||||
|
||||
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
|
||||
var FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
|
||||
var ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized';
|
||||
var ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
|
||||
var ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
|
||||
var ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
|
||||
|
||||
window.utils.aside = function(asideEl) {
|
||||
var asidenavUtil = function(element) {
|
||||
|
||||
if (!asideEl) {
|
||||
throw new Error('asideEl not defined');
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Asidenav utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
initFavoritesButton();
|
||||
initAsidenavSubmenus();
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ASIDENAV_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASIDENAV_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function initFavoritesButton() {
|
||||
var favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
|
||||
favoritesBtn.addEventListener('click', function(event) {
|
||||
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
|
||||
asideEl.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
||||
element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initAsidenavSubmenus() {
|
||||
var asidenavLinksWithSubmenus = Array.from(asideEl.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
|
||||
var asidenavLinksWithSubmenus = Array.from(element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
|
||||
.map(function(listItem) {
|
||||
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
|
||||
return { listItem, submenu };
|
||||
@ -53,12 +96,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
initFavoritesButton();
|
||||
initAsidenavSubmenus();
|
||||
|
||||
return {
|
||||
scope: asideEl,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: ASIDENAV_UTIL_NAME,
|
||||
selector: ASIDENAV_UTIL_SELECTOR,
|
||||
setup: asidenavUtil,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,33 +1,70 @@
|
||||
(function collonadeClosure() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Async Form Utility
|
||||
* prevents form submissions from reloading the page but instead firing an AJAX request
|
||||
*
|
||||
* Attribute: uw-async-form
|
||||
* (works only on <form> elements)
|
||||
*
|
||||
* Example usage:
|
||||
* <form uw-async-form method='POST' action='...'>
|
||||
* ...
|
||||
*
|
||||
* Internationalization:
|
||||
* This utility expects the following translations to be available:
|
||||
* asyncFormFailure: text that gets shown if an async form request fails
|
||||
* example: "Oops. Something went wrong."
|
||||
*/
|
||||
|
||||
var ASYNC_FORM_RESPONSE_CLASS = 'async-form-response';
|
||||
var ASYNC_FORM_LOADING_CLASS = 'async-form-loading';
|
||||
var ASYNC_FORM_UTIL_NAME = 'asyncForm';
|
||||
var ASYNC_FORM_UTIL_SELECTOR = 'form[uw-async-form]';
|
||||
|
||||
var ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
var ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
|
||||
var ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
|
||||
var ASYNC_FORM_MIN_DELAY = 600;
|
||||
var DEFAULT_FAILURE_MESSAGE = 'The response we received from the server did not match what we expected. Please let us know this happened via the help widget in the top navigation.';
|
||||
|
||||
window.utils.asyncForm = function(formElement, options) {
|
||||
var MODAL_SELECTOR = '.modal';
|
||||
var MODAL_HEADER_KEY = 'Is-Modal';
|
||||
var MODAL_HEADER_VALUE = 'True';
|
||||
|
||||
options = options || {};
|
||||
var asyncFormUtil = function(element) {
|
||||
|
||||
var lastRequestTimestamp = 0;
|
||||
|
||||
function setup() {
|
||||
formElement.addEventListener('submit', submitHandler);
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Async Form Utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
element.addEventListener('submit', submitHandler);
|
||||
|
||||
element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASYNC_FORM_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function processResponse(response) {
|
||||
var responseElement = makeResponseElement(response.content, response.status);
|
||||
var parentElement = formElement.parentElement;
|
||||
var parentElement = element.parentElement;
|
||||
|
||||
// make sure there is a delay between click and response
|
||||
var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now());
|
||||
|
||||
setTimeout(function() {
|
||||
parentElement.insertBefore(responseElement, formElement);
|
||||
formElement.remove();
|
||||
parentElement.insertBefore(responseElement, element);
|
||||
element.remove();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
@ -43,20 +80,23 @@
|
||||
function submitHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
formElement.classList.add(ASYNC_FORM_LOADING_CLASS)
|
||||
lastRequestTimestamp = Date.now();
|
||||
|
||||
var url = formElement.getAttribute('action');
|
||||
var headers = { };
|
||||
var body = new FormData(formElement);
|
||||
|
||||
if (options && options.headers) {
|
||||
Object.keys(options.headers).forEach(function(headerKey) {
|
||||
headers[headerKey] = options.headers[headerKey];
|
||||
});
|
||||
if (!HttpClient) {
|
||||
throw new Error('HttpClient not found! Can\'t fetch submit form asynchronously!');
|
||||
}
|
||||
|
||||
window.utils.httpClient.post(url, headers, body)
|
||||
element.classList.add(ASYNC_FORM_LOADING_CLASS)
|
||||
lastRequestTimestamp = Date.now();
|
||||
|
||||
var url = element.getAttribute('action');
|
||||
var headers = { };
|
||||
var body = new FormData(element);
|
||||
|
||||
var isModal = element.closest(MODAL_SELECTOR);
|
||||
if (!!isModal) {
|
||||
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
|
||||
}
|
||||
|
||||
HttpClient.post(url, headers, body)
|
||||
.then(function(response) {
|
||||
if (response.headers.get("content-type").indexOf("application/json") !== -1) {// checking response header
|
||||
return response.json();
|
||||
@ -66,21 +106,21 @@
|
||||
}).then(function(response) {
|
||||
processResponse(response[0]);
|
||||
}).catch(function(error) {
|
||||
var failureMessage = DEFAULT_FAILURE_MESSAGE;
|
||||
if (options.i18n && options.i18n.asyncFormFailure) {
|
||||
failureMessage = options.i18n.asyncFormFailure;
|
||||
}
|
||||
var failureMessage = I18n.get('asyncFormFailure');
|
||||
processResponse({ content: failureMessage });
|
||||
|
||||
formElement.classList.remove(ASYNC_FORM_LOADING_CLASS);
|
||||
element.classList.remove(ASYNC_FORM_LOADING_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
return {
|
||||
scope: formElement,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: ASYNC_FORM_UTIL_NAME,
|
||||
selector: ASYNC_FORM_UTIL_SELECTOR,
|
||||
setup: asyncFormUtil
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,112 +1,272 @@
|
||||
(function collonadeClosure() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Async Table Utility
|
||||
* makes table filters, sorting and pagination behave asynchronously via AJAX calls
|
||||
*
|
||||
* Attribute: uw-async-table
|
||||
*
|
||||
* Example usage:
|
||||
* (regular table)
|
||||
*/
|
||||
|
||||
var INPUT_DEBOUNCE = 600;
|
||||
var HEADER_HEIGHT = 80;
|
||||
var RESET_OPTIONS = [ 'scrollTo' ];
|
||||
var TABLE_FILTER_FORM_CLASS = 'table-filter-form';
|
||||
var ASYNC_TABLE_CONTENT_CHANGED_CLASS = 'async-table--changed';
|
||||
|
||||
var ASYNC_TABLE_UTIL_NAME = 'asyncTable';
|
||||
var ASYNC_TABLE_UTIL_SELECTOR = '[uw-async-table]';
|
||||
|
||||
var ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
|
||||
var ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
|
||||
var ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
|
||||
var ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
|
||||
var JS_INITIALIZED_CLASS = 'js-async-table-initialized';
|
||||
|
||||
window.utils.asyncTable = function(wrapper, options) {
|
||||
var ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
|
||||
var ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
|
||||
|
||||
options = options || {};
|
||||
var tableIdent = options.dbtIdent;
|
||||
var shortCircuitHeader = options ? options.headerDBTableShortcircuit : null;
|
||||
|
||||
var asyncTableUtil = function(element) {
|
||||
var asyncTableHeader;
|
||||
var asyncTableId;
|
||||
|
||||
var ths = [];
|
||||
var pageLinks = [];
|
||||
var pagesizeForm;
|
||||
var scrollTable;
|
||||
|
||||
var utilInstances = [];
|
||||
var tableFilterInputs = {
|
||||
search: [],
|
||||
input: [],
|
||||
change: [],
|
||||
select: [],
|
||||
}
|
||||
|
||||
function init() {
|
||||
var table = wrapper.querySelector('#' + tableIdent);
|
||||
|
||||
if (!table) {
|
||||
return;
|
||||
if (!element) {
|
||||
throw new Error('Async Table utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
scrollTable = wrapper.querySelector('.scrolltable');
|
||||
// param asyncTableDbHeader
|
||||
if (element.dataset.asyncTableDbHeader !== undefined) {
|
||||
asyncTableHeader = element.dataset.asyncTableDbHeader;
|
||||
}
|
||||
|
||||
// sortable table headers
|
||||
ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) {
|
||||
asyncTableId = element.querySelector('table').id;
|
||||
|
||||
// find scrolltable wrapper
|
||||
scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
|
||||
if (!scrollTable) {
|
||||
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
||||
}
|
||||
|
||||
setupSortableHeaders();
|
||||
setupPagination();
|
||||
setupPageSizeSelect();
|
||||
setupTableFilter();
|
||||
|
||||
processLocalStorage();
|
||||
|
||||
// clear currentTableUrl from previous requests
|
||||
setLocalStorageParameter('currentTableUrl', null);
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: ASYNC_TABLE_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function setupSortableHeaders() {
|
||||
ths = Array.from(scrollTable.querySelectorAll('th.sortable')).map(function(th) {
|
||||
return { element: th };
|
||||
});
|
||||
|
||||
// pagination links
|
||||
var pagination = wrapper.querySelector('#' + tableIdent + '-pagination');
|
||||
ths.forEach(function(th) {
|
||||
th.clickHandler = function(event) {
|
||||
setLocalStorageParameter('horizPos', (scrollTable || {}).scrollLeft);
|
||||
linkClickHandler(event);
|
||||
};
|
||||
th.element.addEventListener('click', th.clickHandler);
|
||||
});
|
||||
}
|
||||
|
||||
function setupPagination() {
|
||||
var pagination = element.querySelector('#' + asyncTableId + '-pagination');
|
||||
if (pagination) {
|
||||
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
|
||||
return { element: link };
|
||||
});
|
||||
|
||||
pageLinks.forEach(function(link) {
|
||||
link.clickHandler = function(event) {
|
||||
var tableBoundingRect = scrollTable.getBoundingClientRect();
|
||||
if (tableBoundingRect.top < HEADER_HEIGHT) {
|
||||
var scrollTo = {
|
||||
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
|
||||
left: scrollTable.offsetLeft || 0,
|
||||
behavior: 'smooth',
|
||||
};
|
||||
setLocalStorageParameter('scrollTo', scrollTo);
|
||||
}
|
||||
linkClickHandler(event);
|
||||
}
|
||||
link.element.addEventListener('click', link.clickHandler);
|
||||
});
|
||||
}
|
||||
|
||||
// pagesize form
|
||||
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
|
||||
|
||||
// check all
|
||||
utilInstances.push(window.utils.setup('checkAll', wrapper));
|
||||
|
||||
// showhide
|
||||
utilInstances.push(window.utils.setup('showHide', wrapper));
|
||||
|
||||
// filter
|
||||
var filterForm = wrapper.querySelector('.' + TABLE_FILTER_FORM_CLASS);
|
||||
if (filterForm) {
|
||||
options.updateTableFrom = updateTableFrom;
|
||||
utilInstances.push(window.utils.setup('asyncTableFilter', filterForm, options));
|
||||
}
|
||||
|
||||
// take options into account
|
||||
if (options.scrollTo) {
|
||||
window.scrollTo(options.scrollTo);
|
||||
}
|
||||
|
||||
if (options.horizPos && scrollTable) {
|
||||
scrollTable.scrollLeft = options.horizPos;
|
||||
}
|
||||
|
||||
setupListeners();
|
||||
wrapper.classList.add(JS_INITIALIZED_CLASS);
|
||||
}
|
||||
|
||||
function setupListeners() {
|
||||
ths.forEach(function(th) {
|
||||
th.clickHandler = function(event) {
|
||||
var boundClickHandler = clickHandler.bind(this);
|
||||
var horizPos = (scrollTable || {}).scrollLeft;
|
||||
boundClickHandler(event, { horizPos });
|
||||
};
|
||||
th.element.addEventListener('click', th.clickHandler);
|
||||
});
|
||||
|
||||
pageLinks.forEach(function(link) {
|
||||
link.clickHandler = function(event) {
|
||||
var boundClickHandler = clickHandler.bind(this);
|
||||
var tableBoundingRect = scrollTable.getBoundingClientRect();
|
||||
var tableOptions = {};
|
||||
if (tableBoundingRect.top < HEADER_HEIGHT) {
|
||||
tableOptions.scrollTo = {
|
||||
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
|
||||
left: scrollTable.offsetLeft || 0,
|
||||
behavior: 'smooth',
|
||||
};
|
||||
}
|
||||
boundClickHandler(event, tableOptions);
|
||||
}
|
||||
link.element.addEventListener('click', link.clickHandler);
|
||||
});
|
||||
function setupPageSizeSelect() {
|
||||
// pagesize form
|
||||
pagesizeForm = element.querySelector('#' + asyncTableId + '-pagesize-form');
|
||||
|
||||
if (pagesizeForm) {
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]');
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
|
||||
pagesizeSelect.addEventListener('change', changePagesizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function setupTableFilter() {
|
||||
var tableFilterForm = element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
||||
|
||||
if (tableFilterForm) {
|
||||
gatherTableFilterInputs(tableFilterForm);
|
||||
addTableFilterEventListeners();
|
||||
}
|
||||
}
|
||||
|
||||
function gatherTableFilterInputs(tableFilterForm) {
|
||||
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach(function(input) {
|
||||
tableFilterInputs.search.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('input[type="text"]')).forEach(function(input) {
|
||||
tableFilterInputs.input.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) {
|
||||
tableFilterInputs.change.push(input);
|
||||
});
|
||||
|
||||
Array.from(tableFilterForm.querySelectorAll('select')).forEach(function(input) {
|
||||
tableFilterInputs.select.push(input);
|
||||
});
|
||||
}
|
||||
|
||||
function addTableFilterEventListeners() {
|
||||
tableFilterInputs.search.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateFromTableFilter();
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
tableFilterInputs.input.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateFromTableFilter();
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
tableFilterInputs.change.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateFromTableFilter();
|
||||
});
|
||||
});
|
||||
|
||||
tableFilterInputs.select.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateFromTableFilter();
|
||||
});
|
||||
});
|
||||
|
||||
element.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
updateFromTableFilter();
|
||||
});
|
||||
}
|
||||
|
||||
function updateFromTableFilter() {
|
||||
var url = serializeTableFilterToURL();
|
||||
var callback = null;
|
||||
|
||||
var focusedSearch = tableFilterInputs.search.reduce(function(acc, input) {
|
||||
return acc || (input.matches(':focus') && input);
|
||||
}, null);
|
||||
// focus search input
|
||||
if (focusedSearch) {
|
||||
var selectionStart = focusedSearch.selectionStart;
|
||||
callback = function(wrapper) {
|
||||
var search = wrapper.querySelector('input[type="search"]');
|
||||
if (search) {
|
||||
search.focus();
|
||||
search.selectionStart = selectionStart;
|
||||
}
|
||||
};
|
||||
}
|
||||
updateTableFrom(url, callback);
|
||||
}
|
||||
|
||||
function serializeTableFilterToURL() {
|
||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
||||
|
||||
var formIdElement = element.querySelector(ASYNC_TABLE_FILTER_FORM_ID_SELECTOR);
|
||||
if (!formIdElement) {
|
||||
// cannot serialize the filter form without an identifier
|
||||
return;
|
||||
}
|
||||
|
||||
url.searchParams.set('form-identifier', formIdElement.value);
|
||||
url.searchParams.set('_hasdata', 'true');
|
||||
url.searchParams.set(asyncTableId + '-page', '0');
|
||||
|
||||
tableFilterInputs.search.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
tableFilterInputs.input.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
tableFilterInputs.change.forEach(function(input) {
|
||||
if (input.checked) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
}
|
||||
});
|
||||
|
||||
tableFilterInputs.select.forEach(function(select) {
|
||||
var options = Array.from(select.querySelectorAll('option'));
|
||||
var selected = options.find(function(option) { return option.selected });
|
||||
if (selected) {
|
||||
url.searchParams.set(select.name, selected.value);
|
||||
}
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function processLocalStorage() {
|
||||
var scrollTo = getLocalStorageParameter('scrollTo');
|
||||
if (scrollTo && scrollTable) {
|
||||
window.scrollTo(scrollTo);
|
||||
}
|
||||
setLocalStorageParameter('scrollTo', null);
|
||||
|
||||
var horizPos = getLocalStorageParameter('horizPos');
|
||||
if (horizPos && scrollTable) {
|
||||
scrollTable.scrollLeft = horizPos;
|
||||
}
|
||||
setLocalStorageParameter('horizPos', null);
|
||||
}
|
||||
|
||||
function removeListeners() {
|
||||
ths.forEach(function(th) {
|
||||
th.element.removeEventListener('click', th.clickHandler);
|
||||
@ -117,121 +277,124 @@
|
||||
});
|
||||
|
||||
if (pagesizeForm) {
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
|
||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]')
|
||||
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function clickHandler(event, tableOptions) {
|
||||
function linkClickHandler(event) {
|
||||
event.preventDefault();
|
||||
var url = getClickDestination(this);
|
||||
var url = getClickDestination(event.target);
|
||||
if (!url.match(/^http/)) {
|
||||
url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
|
||||
url = window.location.origin + window.location.pathname + url;
|
||||
}
|
||||
updateTableFrom(url, tableOptions);
|
||||
}
|
||||
|
||||
function getClickDestination(el) {
|
||||
if (!el.querySelector('a')) {
|
||||
return '';
|
||||
}
|
||||
return el.querySelector('a').getAttribute('href');
|
||||
}
|
||||
|
||||
function changePagesizeHandler(event) {
|
||||
var pagesizeParamKey = tableIdent + '-pagesize';
|
||||
var pageParamKey = tableIdent + '-page';
|
||||
var url = new URL(options.currentUrl || window.location.href);
|
||||
url.searchParams.set(pagesizeParamKey, event.target.value);
|
||||
url.searchParams.set(pageParamKey, 0);
|
||||
updateTableFrom(url);
|
||||
}
|
||||
|
||||
// fetches new sorted table from url with params and replaces contents of current table
|
||||
function updateTableFrom(url, tableOptions, callback) {
|
||||
if (!window.utils.httpClient) {
|
||||
throw new Error('httpClient not found!');
|
||||
function getClickDestination(el) {
|
||||
if (!el.matches('a') && !el.querySelector('a')) {
|
||||
return '';
|
||||
}
|
||||
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
||||
}
|
||||
|
||||
function changePagesizeHandler(event) {
|
||||
var pagesizeParamKey = asyncTableId + '-pagesize';
|
||||
var pageParamKey = asyncTableId + '-page';
|
||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
||||
url.searchParams.set(pagesizeParamKey, event.target.value);
|
||||
url.searchParams.set(pageParamKey, 0);
|
||||
updateTableFrom(url.href);
|
||||
}
|
||||
|
||||
// fetches new sorted element from url with params and replaces contents of current element
|
||||
function updateTableFrom(url, callback) {
|
||||
if (!HttpClient) {
|
||||
throw new Error('HttpClient not found!');
|
||||
}
|
||||
|
||||
wrapper.classList.add(ASYNC_TABLE_LOADING_CLASS);
|
||||
element.classList.add(ASYNC_TABLE_LOADING_CLASS);
|
||||
|
||||
tableOptions = tableOptions || {};
|
||||
var headers = {
|
||||
'Accept': 'text/html',
|
||||
[shortCircuitHeader]: tableIdent
|
||||
[asyncTableHeader]: asyncTableId
|
||||
};
|
||||
window.utils.httpClient.get(url, headers).then(function(response) {
|
||||
|
||||
HttpClient.get(url, headers).then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Looks like there was a problem fetching ' + url.href + '. Status Code: ' + response.status);
|
||||
}
|
||||
return response.text();
|
||||
}).then(function(data) {
|
||||
tableOptions.currentUrl = url.href;
|
||||
setLocalStorageParameter('currentTableUrl', url.href);
|
||||
// reset table
|
||||
removeListeners();
|
||||
updateWrapperContents(data, tableOptions);
|
||||
element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||
// update table with new
|
||||
updateWrapperContents(data);
|
||||
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback(wrapper);
|
||||
callback(element);
|
||||
}
|
||||
|
||||
wrapper.classList.remove(ASYNC_TABLE_LOADING_CLASS);
|
||||
element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
|
||||
}).catch(function(err) {
|
||||
console.error(err);
|
||||
element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWrapperContents(newHtml, tableOptions) {
|
||||
tableOptions = tableOptions || {};
|
||||
wrapper.innerHTML = newHtml;
|
||||
wrapper.classList.remove(JS_INITIALIZED_CLASS);
|
||||
wrapper.classList.add(ASYNC_TABLE_CONTENT_CHANGED_CLASS);
|
||||
function updateWrapperContents(newHtml) {
|
||||
var newPage = document.createElement('div');
|
||||
newPage.innerHTML = newHtml;
|
||||
var newWrapperContents = newPage.querySelector('#' + element.id);
|
||||
element.innerHTML = newWrapperContents.innerHTML;
|
||||
|
||||
destroyUtils();
|
||||
|
||||
// merge global options and table specific options
|
||||
var resetOptions = {};
|
||||
Object.keys(options)
|
||||
.filter(function(key) {
|
||||
return !RESET_OPTIONS.includes(key);
|
||||
})
|
||||
.forEach(function(key) {
|
||||
resetOptions[key] = options[key];
|
||||
});
|
||||
var combinedOptions = {};
|
||||
combinedOptions = Object.keys(tableOptions)
|
||||
.filter(function(key) {
|
||||
return tableOptions.hasOwnProperty(key);
|
||||
})
|
||||
.map(function(key) {
|
||||
return { key, value: tableOptions[key] }
|
||||
})
|
||||
.reduce(function(cumulatedOpts, opt) {
|
||||
cumulatedOpts[opt.key] = opt.value;
|
||||
return cumulatedOpts;
|
||||
}, resetOptions);
|
||||
|
||||
window.utils.setup('asyncTable', wrapper, combinedOptions);
|
||||
|
||||
Array.from(wrapper.querySelectorAll('form')).forEach(function(form) {
|
||||
utilInstances.push(window.utils.setup('form', form));
|
||||
});
|
||||
Array.from(wrapper.querySelectorAll('.modal')).forEach(function(modal) {
|
||||
utilInstances.push(window.utils.setup('modal', modal));
|
||||
});
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.setupAll(element);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyUtils() {
|
||||
utilInstances.filter(function(utilInstance) {
|
||||
return !!utilInstance;
|
||||
}).forEach(function(utilInstance) {
|
||||
utilInstance.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
scope: wrapper,
|
||||
destroy: destroyUtils,
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
function setLocalStorageParameter(key, value) {
|
||||
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
|
||||
if (value !== null) {
|
||||
currentLSState[key] = value;
|
||||
} else {
|
||||
delete currentLSState[key];
|
||||
}
|
||||
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
|
||||
}
|
||||
|
||||
function getLocalStorageParameter(key) {
|
||||
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
|
||||
return currentLSState[key];
|
||||
}
|
||||
|
||||
// debounce function, taken from Underscore.js
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
|
||||
// register async table utility
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: ASYNC_TABLE_UTIL_NAME,
|
||||
selector: ASYNC_TABLE_UTIL_SELECTOR,
|
||||
setup: asyncTableUtil,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,171 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
var ASYNC_TABLE_FILTER_LOADING_CLASS = 'async-table-filter--loading';
|
||||
var JS_INITIALIZED_CLASS = 'js-async-table-filter-initialized';
|
||||
var INPUT_DEBOUNCE = 600;
|
||||
|
||||
// debounce function, taken from Underscore.js
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
|
||||
window.utils.asyncTableFilter = function(formElement, options) {
|
||||
if (!options || !options.updateTableFrom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formElement.matches('.' + JS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var formIdElement = formElement.querySelector('[name="form-identifier"]');
|
||||
if (!formIdElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var tableIdent = options.dbtIdent;
|
||||
var formId = formIdElement.value;
|
||||
var inputs = {
|
||||
search: [],
|
||||
input: [],
|
||||
change: [],
|
||||
select: [],
|
||||
}
|
||||
|
||||
function setup() {
|
||||
gatherInputs();
|
||||
addEventListeners();
|
||||
}
|
||||
|
||||
function gatherInputs() {
|
||||
Array.from(formElement.querySelectorAll('input[type="search"]')).forEach(function(input) {
|
||||
inputs.search.push(input);
|
||||
});
|
||||
|
||||
Array.from(formElement.querySelectorAll('input[type="text"]')).forEach(function(input) {
|
||||
inputs.input.push(input);
|
||||
});
|
||||
|
||||
Array.from(formElement.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) {
|
||||
inputs.change.push(input);
|
||||
});
|
||||
|
||||
Array.from(formElement.querySelectorAll('select')).forEach(function(input) {
|
||||
inputs.select.push(input);
|
||||
});
|
||||
}
|
||||
|
||||
function addEventListeners() {
|
||||
inputs.search.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateTable();
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
inputs.input.forEach(function(input) {
|
||||
var debouncedInput = debounce(function() {
|
||||
if (input.value.length === 0 || input.value.length > 2) {
|
||||
updateTable();
|
||||
}
|
||||
}, INPUT_DEBOUNCE);
|
||||
input.addEventListener('input', debouncedInput);
|
||||
});
|
||||
|
||||
inputs.change.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateTable();
|
||||
});
|
||||
});
|
||||
|
||||
inputs.select.forEach(function(input) {
|
||||
input.addEventListener('change', function() {
|
||||
updateTable();
|
||||
});
|
||||
});
|
||||
|
||||
formElement.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
updateTable();
|
||||
});
|
||||
}
|
||||
|
||||
function updateTable() {
|
||||
var url = serializeFormToURL();
|
||||
var callback = null;
|
||||
|
||||
formElement.classList.add(ASYNC_TABLE_FILTER_LOADING_CLASS);
|
||||
|
||||
var focusedSearch = inputs.search.reduce(function(acc, input) {
|
||||
return acc || (input.matches(':focus') && input);
|
||||
}, null);
|
||||
// focus search input
|
||||
if (focusedSearch) {
|
||||
var selectionStart = focusedSearch.selectionStart;
|
||||
callback = function(wrapper) {
|
||||
var search = wrapper.querySelector('input[type="search"]');
|
||||
if (search) {
|
||||
search.focus();
|
||||
search.selectionStart = selectionStart;
|
||||
}
|
||||
};
|
||||
}
|
||||
options.updateTableFrom(url, options, callback);
|
||||
}
|
||||
|
||||
function serializeFormToURL() {
|
||||
var url = new URL(options.currentUrl || window.location.href);
|
||||
url.searchParams.set('form-identifier', formId);
|
||||
url.searchParams.set('_hasdata', 'true');
|
||||
url.searchParams.set(tableIdent + '-page', '0');
|
||||
|
||||
inputs.search.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
inputs.input.forEach(function(input) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
});
|
||||
|
||||
inputs.change.forEach(function(input) {
|
||||
if (input.checked) {
|
||||
url.searchParams.set(input.name, input.value);
|
||||
}
|
||||
});
|
||||
|
||||
inputs.select.forEach(function(select) {
|
||||
var options = Array.from(select.querySelectorAll('option'));
|
||||
var selected = options.find(function(option) { return option.selected });
|
||||
if (selected) {
|
||||
url.searchParams.set(select.name, selected.value);
|
||||
}
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
return {
|
||||
scope: formElement,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
})();
|
||||
@ -1,40 +1,54 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Check All Checkbox Utility
|
||||
* adds a Check All Checkbox above columns with only checkboxes
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (will be set up automatically on tables)
|
||||
*
|
||||
* Example usage:
|
||||
* (table with one column thats only checkboxes)
|
||||
*/
|
||||
|
||||
var CHECK_ALL_UTIL_NAME = 'checkAll';
|
||||
var CHECK_ALL_UTIL_SELECTOR = 'table';
|
||||
|
||||
var ASYNC_TABLE_CONTENT_CHANGED_CLASS = 'async-table--changed';
|
||||
var JS_INITIALIZED_CLASS = 'js-check-all-initialized';
|
||||
var CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||
|
||||
function getCheckboxId() {
|
||||
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
||||
}
|
||||
|
||||
window.utils.checkAll = function(wrapper, options) {
|
||||
|
||||
if ((!wrapper || wrapper.classList.contains(JS_INITIALIZED_CLASS)) && !wrapper.classList.contains(ASYNC_TABLE_CONTENT_CHANGED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
options = options || {};
|
||||
var CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
|
||||
|
||||
var checkAllUtil = function(element) {
|
||||
var columns = [];
|
||||
var checkboxColumn = [];
|
||||
var checkAllCheckbox = null;
|
||||
|
||||
var utilInstances = [];
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Check All utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
columns = gatherColumns(wrapper);
|
||||
gatherColumns();
|
||||
setupCheckAllCheckbox();
|
||||
|
||||
setupCheckAllCheckbox(findCheckboxColumn(columns));
|
||||
// mark initialized
|
||||
element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
|
||||
|
||||
wrapper.classList.add(JS_INITIALIZED_CLASS);
|
||||
return {
|
||||
name: CHECK_ALL_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function gatherColumns(table) {
|
||||
var rows = Array.from(table.querySelectorAll('tr'));
|
||||
function getCheckboxId() {
|
||||
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
||||
}
|
||||
|
||||
function gatherColumns(tble) {
|
||||
var rows = Array.from(element.querySelectorAll('tr'));
|
||||
var cols = [];
|
||||
rows.forEach(function(tr) {
|
||||
var cells = Array.from(tr.querySelectorAll('td'));
|
||||
@ -45,7 +59,7 @@
|
||||
cols[cellIndex].push(cell);
|
||||
});
|
||||
});
|
||||
return cols;
|
||||
columns = cols;
|
||||
}
|
||||
|
||||
function findCheckboxColumn(columns) {
|
||||
@ -68,21 +82,26 @@
|
||||
return onlyCheckboxes;
|
||||
}
|
||||
|
||||
function setupCheckAllCheckbox(columnId) {
|
||||
if (columnId === null) {
|
||||
function setupCheckAllCheckbox() {
|
||||
var checkboxColumnId = findCheckboxColumn(columns);
|
||||
if (checkboxColumnId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkboxColumn = columns[columnId];
|
||||
var firstRow = wrapper.querySelector('tr');
|
||||
var th = Array.from(firstRow.querySelectorAll('th, td'))[columnId];
|
||||
checkboxColumn = columns[checkboxColumnId];
|
||||
var firstRow = element.querySelector('tr');
|
||||
var th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
|
||||
th.innerHTML = 'test';
|
||||
checkAllCheckbox = document.createElement('input');
|
||||
checkAllCheckbox.setAttribute('type', 'checkbox');
|
||||
checkAllCheckbox.setAttribute('id', getCheckboxId());
|
||||
th.innerHTML = '';
|
||||
th.insertBefore(checkAllCheckbox, null);
|
||||
utilInstances.push(window.utils.setup('checkbox', checkAllCheckbox));
|
||||
|
||||
// manually set up newly created checkbox
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.setup(UtilRegistry.find('checkbox'));
|
||||
}
|
||||
|
||||
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);
|
||||
setupCheckboxListeners();
|
||||
@ -103,9 +122,9 @@
|
||||
}
|
||||
|
||||
function updateCheckAllCheckboxState() {
|
||||
var allChecked = checkboxColumn.reduce(function(acc, cell) {
|
||||
return acc && cell.querySelector(CHECKBOX_SELECTOR).checked;
|
||||
}, true);
|
||||
var allChecked = checkboxColumn.every(function(cell) {
|
||||
return cell.querySelector(CHECKBOX_SELECTOR).checked;
|
||||
})
|
||||
checkAllCheckbox.checked = allChecked;
|
||||
}
|
||||
|
||||
@ -115,17 +134,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
utilInstances.forEach(function(util) {
|
||||
util.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
return {
|
||||
scope: wrapper,
|
||||
destroy: destroy,
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
// register check all checkbox util
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: CHECK_ALL_UTIL_NAME,
|
||||
selector: CHECK_ALL_UTIL_SELECTOR,
|
||||
setup: checkAllUtil
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,202 +1,315 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
var formUtilities = [];
|
||||
|
||||
var JS_INITIALIZED = 'js-form-initialized';
|
||||
var SUBMIT_BUTTON_SELECTOR = '[type="submit"]:not([formnovalidate])';
|
||||
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
|
||||
var AJAX_SUBMIT_FLAG = 'ajaxSubmit';
|
||||
/**
|
||||
*
|
||||
* Reactive Submit Button Utility
|
||||
* disables a forms LAST sumit button as long as the required inputs are invalid
|
||||
* (only checks if the value of the inputs are not empty)
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all form tags)
|
||||
*
|
||||
* Params:
|
||||
* data-formnorequired: string
|
||||
* If present the submit button will never get disabled
|
||||
*
|
||||
* Example usage:
|
||||
* <form uw-reactive-submit-button>
|
||||
* <input type="text" required>
|
||||
* <button type="submit">
|
||||
* </form>
|
||||
*/
|
||||
|
||||
var FORM_GROUP_CLASS = 'form-group';
|
||||
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
|
||||
var REACTIVE_SUBMIT_BUTTON_UTIL_NAME = 'reactiveSubmitButton';
|
||||
var REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR = 'form';
|
||||
|
||||
function formValidator(inputs) {
|
||||
var done = true;
|
||||
inputs.forEach(function(inp) {
|
||||
var len = inp.value.trim().length;
|
||||
if (done && len === 0) {
|
||||
done = false;
|
||||
var REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
|
||||
|
||||
var reactiveSubmitButtonUtil = function(element) {
|
||||
var requiredInputs;
|
||||
var submitButton;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
|
||||
}
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
window.utils.form = function(form, options) {
|
||||
options = options || {};
|
||||
if (element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// dont initialize form if it is in a modal and is not forced
|
||||
if (form.closest('.modal') && !options.force) {
|
||||
return false;
|
||||
// abort if form has param data-formnorequired
|
||||
if (element.dataset.formnorequired !== undefined) {
|
||||
throw new Error('Form has formnorequired data attribute. Will skip setup of reactive submit button.');
|
||||
}
|
||||
|
||||
requiredInputs = Array.from(element.querySelectorAll('[required]'));
|
||||
if (!requiredInputs) {
|
||||
// abort if form has no required inputs
|
||||
throw new Error('Submit button has formnorequired data attribute. Will skip setup of reactive submit button.');
|
||||
}
|
||||
|
||||
var submitButtons = Array.from(element.querySelectorAll('[type="submit"]'));
|
||||
if (!submitButtons || !submitButtons.length) {
|
||||
throw new Error('Reactive Submit Button utility couldn\'t find any submit buttons!');
|
||||
}
|
||||
submitButton = submitButtons.reverse()[0];
|
||||
// abort if form has param data-formnorequired
|
||||
if (submitButton.dataset.formnorequired !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setupInputs();
|
||||
updateButtonState();
|
||||
|
||||
element.classList.add(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
// dont initialize form if already initialized and should not be force-initialized
|
||||
if (form.classList.contains(JS_INITIALIZED) && !options.force) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var utilInstances = [];
|
||||
|
||||
// reactive buttons
|
||||
utilInstances.push(window.utils.setup('reactiveButton', form));
|
||||
|
||||
// conditonal fieldsets
|
||||
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
|
||||
utilInstances.push(window.utils.setup('interactiveFieldset', form, { fieldSets }));
|
||||
|
||||
// hide autoSubmit submit button
|
||||
utilInstances.push(window.utils.setup('autoSubmit', form, options));
|
||||
|
||||
// async form
|
||||
if (AJAX_SUBMIT_FLAG in form.dataset) {
|
||||
utilInstances.push(window.utils.setup('asyncForm', form, options));
|
||||
}
|
||||
|
||||
// inputs
|
||||
utilInstances.push(window.utils.setup('inputs', form, options));
|
||||
|
||||
// form group errors
|
||||
var formGroups = Array.from(form.querySelectorAll('.' + FORM_GROUP_CLASS));
|
||||
formGroups.forEach(function(formGroup) {
|
||||
utilInstances.push(window.utils.setup('errorRemover', formGroup, options));
|
||||
});
|
||||
|
||||
form.classList.add(JS_INITIALIZED);
|
||||
|
||||
function destroyUtils() {
|
||||
utilInstances.filter(function(utilInstance) {
|
||||
return !!utilInstance;
|
||||
}).forEach(function(utilInstance) {
|
||||
utilInstance.destroy();
|
||||
function setupInputs() {
|
||||
requiredInputs.forEach(function(el) {
|
||||
var checkbox = el.getAttribute('type') === 'checkbox';
|
||||
var eventType = checkbox ? 'change' : 'input';
|
||||
el.addEventListener(eventType, function() {
|
||||
updateButtonState();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
scope: form,
|
||||
destroy: destroyUtils,
|
||||
};
|
||||
};
|
||||
|
||||
// registers input-listener for each element in <inputs> (array) and
|
||||
// enables <button> if <formValidator> for these inputs returns true
|
||||
window.utils.reactiveButton = function(form, options) {
|
||||
var button = form.querySelector(SUBMIT_BUTTON_SELECTOR);
|
||||
var requireds = Array.from(form.querySelectorAll('[required]'));
|
||||
|
||||
if (!button || button.matches(AUTOSUBMIT_BUTTON_SELECTOR)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requireds.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
|
||||
button.addEventListener('click', function() {
|
||||
form.submit();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
updateButtonState();
|
||||
|
||||
requireds.forEach(function(el) {
|
||||
var checkbox = el.getAttribute('type') === 'checkbox';
|
||||
var eventType = checkbox ? 'change' : 'input';
|
||||
el.addEventListener(eventType, function() {
|
||||
updateButtonState();
|
||||
});
|
||||
});
|
||||
|
||||
function updateButtonState() {
|
||||
if (formValidator(requireds) === true) {
|
||||
button.removeAttribute('disabled');
|
||||
if (inputsValid()) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
} else {
|
||||
button.setAttribute('disabled', 'true');
|
||||
submitButton.setAttribute('disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scope: form,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
|
||||
window.utils.interactiveFieldset = function(form, options) {
|
||||
options = options || {};
|
||||
var fieldSets = options.fieldSets;
|
||||
|
||||
if (!fieldSets) {
|
||||
throw new Error('interactiveFieldset must be passed fieldSets via options');
|
||||
function inputsValid() {
|
||||
var done = true;
|
||||
requiredInputs.forEach(function(inp) {
|
||||
var len = inp.value.trim().length;
|
||||
if (done && len === 0) {
|
||||
done = false;
|
||||
}
|
||||
});
|
||||
return done;
|
||||
}
|
||||
|
||||
var fields = fieldSets.map(function(fs) {
|
||||
return init();
|
||||
};
|
||||
|
||||
formUtilities.push({
|
||||
name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
|
||||
selector: REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR,
|
||||
setup: reactiveSubmitButtonUtil,
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* Interactive Fieldset Utility
|
||||
* shows/hides inputs based on value of particular input
|
||||
*
|
||||
* Attribute: uw-interactive-fieldset
|
||||
*
|
||||
* Params:
|
||||
* data-conditional-input: string
|
||||
* Selector for the input that this fieldset watches for changes
|
||||
* data-conditional-value: string
|
||||
* The value the conditional input needs to be set to for this fieldset to be shown
|
||||
*
|
||||
* Example usage:
|
||||
* <input id="input-0" type="text">
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="yes">...</fieldset>
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="no">...</fieldset>
|
||||
* ## example with <select>
|
||||
* <select id="select-0">
|
||||
* <option value="0">Zero
|
||||
* <option value="1">One
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="0">...</fieldset>
|
||||
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="1">...</fieldset>
|
||||
*/
|
||||
|
||||
var INTERACTIVE_FIELDSET_UTIL_NAME = 'interactiveFieldset';
|
||||
var INTERACTIVE_FIELDSET_UTIL_SELECTOR = '[uw-interactive-fieldset]';
|
||||
|
||||
var INTERACTIVE_FIELDSET_INITIALIZED_CLASS = 'interactive-fieldset--initialized';
|
||||
|
||||
var interactiveFieldsetUtil = function(element) {
|
||||
var conditionalInput;
|
||||
var conditionalValue;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Interactive Fieldset utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// param conditionalInput
|
||||
if (!element.dataset.conditionalInput) {
|
||||
throw new Error('Interactive Fieldset needs a selector for a conditional input!');
|
||||
}
|
||||
conditionalInput = document.querySelector('#' + element.dataset.conditionalInput);
|
||||
if (!conditionalInput) {
|
||||
// abort if form has no required inputs
|
||||
throw new Error('Couldn\'t find the conditional input. Aborting setup for interactive fieldset.');
|
||||
}
|
||||
|
||||
// param conditionalValue
|
||||
if (!element.dataset.conditionalValue) {
|
||||
throw new Error('Interactive Fieldset needs a conditional value!');
|
||||
}
|
||||
conditionalValue = element.dataset.conditionalValue;
|
||||
|
||||
// add event listener
|
||||
conditionalInput.addEventListener('input', updateVisibility);
|
||||
|
||||
// initial visibility update
|
||||
updateVisibility();
|
||||
|
||||
// mark as initialized
|
||||
element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
fieldSet: fs,
|
||||
condId: fs.dataset.conditionalId,
|
||||
condValue: fs.dataset.conditionalValue,
|
||||
condEl: form.querySelector('#' + fs.dataset.conditionalId),
|
||||
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}).filter(function(field) {
|
||||
return !!field.condEl;
|
||||
});
|
||||
|
||||
function updateFields() {
|
||||
fields.forEach(function(field) {
|
||||
field.fieldSet.classList.toggle('hidden', field.condEl.value !== field.condValue);
|
||||
});
|
||||
}
|
||||
|
||||
function addEventListeners() {
|
||||
fields.forEach(function(field) {
|
||||
field.condEl.addEventListener('input', updateFields)
|
||||
});
|
||||
function updateVisibility() {
|
||||
element.classList.toggle('hidden', conditionalInput.value !== conditionalValue);
|
||||
}
|
||||
|
||||
if (fieldSets.length) {
|
||||
addEventListeners();
|
||||
updateFields();
|
||||
return init();
|
||||
};
|
||||
|
||||
formUtilities.push({
|
||||
name: INTERACTIVE_FIELDSET_UTIL_NAME,
|
||||
selector: INTERACTIVE_FIELDSET_UTIL_SELECTOR,
|
||||
setup: interactiveFieldsetUtil,
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* Auto Submit Button Utility
|
||||
* Hides submit buttons in forms that are submitted programmatically
|
||||
* We hide the button using JavaScript so no-js users will still be able to submit the form
|
||||
*
|
||||
* Attribute: uw-auto-submit-button
|
||||
*
|
||||
* Example usage:
|
||||
* <button type="submit" uw-auto-submit-button>Submit
|
||||
*/
|
||||
|
||||
var AUTO_SUBMIT_BUTTON_UTIL_NAME = 'autoSubmitButton';
|
||||
var AUTO_SUBMIT_BUTTON_UTIL_SELECTOR = '[uw-auto-submit-button]';
|
||||
|
||||
var AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS = 'auto-submit-button--initialized';
|
||||
var AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
|
||||
|
||||
var autoSubmitButtonUtil = function(element) {
|
||||
if (!element) {
|
||||
throw new Error('Auto Submit Button utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
// hide and mark initialized
|
||||
element.classList.add(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
scope: form,
|
||||
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
|
||||
window.utils.autoSubmit = function(form, options) {
|
||||
var button = form.querySelector(AUTOSUBMIT_BUTTON_SELECTOR);
|
||||
if (button) {
|
||||
button.classList.add('hidden');
|
||||
}
|
||||
formUtilities.push({
|
||||
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
|
||||
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
|
||||
setup: autoSubmitButtonUtil,
|
||||
});
|
||||
|
||||
return {
|
||||
scope: form,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
/**
|
||||
*
|
||||
* Form Error Remover Utility
|
||||
* Removes errors from inputs when they are focused
|
||||
*
|
||||
* Attribute: [none]
|
||||
* (automatically setup on all form tags)
|
||||
*
|
||||
* Example usage:
|
||||
* (any regular form that can show input errors)
|
||||
*/
|
||||
|
||||
// listens for focus events and removes any errors on an input
|
||||
window.utils.errorRemover = function(formGroup, options) {
|
||||
var FORM_ERROR_REMOVER_UTIL_NAME = 'formErrorRemover';
|
||||
var FORM_ERROR_REMOVER_UTIL_SELECTOR = 'form';
|
||||
|
||||
var inputElement = formGroup.querySelector('input:not([type="hidden"]), textarea, select');
|
||||
if (!inputElement) {
|
||||
return false;
|
||||
}
|
||||
var FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
|
||||
var FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
|
||||
|
||||
inputElement.addEventListener('focus', focusListener);
|
||||
var FORM_GROUP_SELECTOR = '.form-group';
|
||||
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
|
||||
|
||||
function focusListener() {
|
||||
var hasError = formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||
if (hasError) {
|
||||
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||
var formErrorRemoverUtil = function(element) {
|
||||
var formGroups;
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Form Error Remover utility needs to be passed an element!');
|
||||
}
|
||||
|
||||
// find form groups
|
||||
formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR));
|
||||
|
||||
formGroups.forEach(function(formGroup) {
|
||||
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
|
||||
if (!inputElements) {
|
||||
return false;
|
||||
}
|
||||
|
||||
inputElements.forEach(function(inputElement) {
|
||||
inputElement.addEventListener('input', function() {
|
||||
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// mark initialized
|
||||
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scope: formGroup,
|
||||
destroy: function() {
|
||||
inputElement.removeEventListener('focus', focusListener);
|
||||
},
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
formUtilities.push({
|
||||
name: FORM_ERROR_REMOVER_UTIL_NAME,
|
||||
selector: FORM_ERROR_REMOVER_UTIL_SELECTOR,
|
||||
setup: formErrorRemoverUtil,
|
||||
});
|
||||
|
||||
// register the collected form utilities
|
||||
if (UtilRegistry) {
|
||||
formUtilities.forEach(UtilRegistry.register);
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@ -1,253 +1,200 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
var inputUtilities = [];
|
||||
|
||||
var JS_INITIALIZED_CLASS = 'js-inputs-initialized';
|
||||
/**
|
||||
*
|
||||
* FileInput Utility
|
||||
* wraps native file input
|
||||
*
|
||||
* Attribute: uw-file-input
|
||||
* (element must be an input of type='file')
|
||||
*
|
||||
* Example usage:
|
||||
* <input type='file' uw-file-input>
|
||||
*
|
||||
* Internationalization:
|
||||
* This utility expects the following translations to be available:
|
||||
* »filesSelected«: label of multi-input button after selection
|
||||
* example: "Dateien ausgewählt" (will be prepended by number of selected files)
|
||||
* »selectFile«: label of single-input button before selection
|
||||
* example: "Datei auswählen"
|
||||
* »selectFiles«: label of multi-input button before selection
|
||||
* example: "Datei(en) auswählen"
|
||||
*
|
||||
*/
|
||||
|
||||
window.utils.inputs = function(wrapper, options) {
|
||||
options = options || {};
|
||||
var utilInstances = [];
|
||||
var FILE_INPUT_UTIL_NAME = 'fileInput';
|
||||
var FILE_INPUT_UTIL_SELECTOR = 'input[type="file"][uw-file-input]';
|
||||
|
||||
if (wrapper.classList.contains(JS_INITIALIZED_CLASS) && !options.force) {
|
||||
return false;
|
||||
}
|
||||
var FILE_INPUT_CLASS = 'file-input';
|
||||
var FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';
|
||||
var FILE_INPUT_LIST_CLASS = 'file-input__list';
|
||||
var FILE_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
|
||||
var FILE_INPUT_LABEL_CLASS = 'file-input__label';
|
||||
|
||||
// checkboxes
|
||||
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"]'));
|
||||
checkboxes.forEach(function(checkbox) {
|
||||
utilInstances.push(window.utils.setup('checkbox', checkbox));
|
||||
});
|
||||
var fileInputUtil = function(element) {
|
||||
var isMultiFileInput = false;
|
||||
var fileList;
|
||||
var label;
|
||||
|
||||
// radios
|
||||
var radios = Array.from(wrapper.querySelectorAll('input[type="radio"]'));
|
||||
radios.forEach(function(radio) {
|
||||
utilInstances.push(window.utils.setup('radio', radio));
|
||||
});
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('FileInput utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
// file-uploads
|
||||
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
|
||||
fileUploads.forEach(function(input) {
|
||||
utilInstances.push(window.utils.setup('fileUpload', input, options));
|
||||
});
|
||||
if (element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) {
|
||||
throw new Error('FileInput utility already initialized!');
|
||||
}
|
||||
|
||||
// file-checkboxes
|
||||
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
|
||||
fileCheckboxes.forEach(function(input) {
|
||||
utilInstances.push(window.utils.setup('fileCheckbox', input, options));
|
||||
});
|
||||
// check if is multi-file input
|
||||
isMultiFileInput = element.hasAttribute('multiple');
|
||||
if (isMultiFileInput) {
|
||||
fileList = createFileList();
|
||||
}
|
||||
|
||||
function destroyUtils() {
|
||||
utilInstances.filter(function(utilInstance) {
|
||||
return !!utilInstance;
|
||||
}).forEach(function(utilInstance) {
|
||||
utilInstance.destroy();
|
||||
label = createFileLabel();
|
||||
updateLabel();
|
||||
|
||||
// add change listener
|
||||
element.addEventListener('change', function() {
|
||||
updateLabel();
|
||||
renderFileList();
|
||||
});
|
||||
|
||||
// add util class for styling and mark as initialized
|
||||
element.classList.add(FILE_INPUT_CLASS, FILE_INPUT_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: FILE_INPUT_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
wrapper.classList.add(JS_INITIALIZED_CLASS);
|
||||
function renderFileList() {
|
||||
if (!fileList) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: wrapper,
|
||||
destroy: destroyUtils,
|
||||
};
|
||||
};
|
||||
|
||||
// (multiple) dynamic file uploads
|
||||
// expects i18n object with following strings:
|
||||
// »filesSelected«: label of multi-upload button after selection
|
||||
// example: "Dateien ausgewählt" (will be prepended by number of selected files)
|
||||
// »selectFile«: label of single-upload button before selection
|
||||
// example: "Datei auswählen"
|
||||
// »selectFiles«: label of multi-upload button before selection
|
||||
// example: "Datei(en) auswählen"
|
||||
|
||||
var FILE_UPLOAD_INPUT_LIST_CLASS = 'file-input__list';
|
||||
var FILE_UPLOAD_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
|
||||
var FILE_UPLOAD_INPUT_LABEL_CLASS = 'file-input__label';
|
||||
var FILE_UPLOAD_INPUT_HIDDEN_CLASS = 'file-input__input--hidden';
|
||||
|
||||
window.utils.fileUpload = function(input, options) {
|
||||
var isMulti = input.hasAttribute('multiple');
|
||||
var fileList = isMulti ? addFileList() : null;
|
||||
var label = addFileLabel();
|
||||
var i18n = options.i18n;
|
||||
|
||||
if (!i18n) {
|
||||
throw new Error('window.utils.fileUpload(input, options) needs to be passed i18n object via options');
|
||||
}
|
||||
|
||||
function renderFileList(files) {
|
||||
var files = element.files;
|
||||
fileList.innerHTML = '';
|
||||
Array.from(files).forEach(function(file, index) {
|
||||
Array.from(files).forEach(function(file) {
|
||||
var fileDisplayEl = document.createElement('li');
|
||||
fileDisplayEl.innerHTML = file.name;
|
||||
fileList.appendChild(fileDisplayEl);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLabel(files) {
|
||||
if (files.length) {
|
||||
if (isMulti) {
|
||||
label.innerText = files.length + ' ' + i18n.filesSelected;
|
||||
} else {
|
||||
label.innerHTML = files[0].name;
|
||||
}
|
||||
} else {
|
||||
resetFileLabel();
|
||||
}
|
||||
}
|
||||
|
||||
function addFileList() {
|
||||
function createFileList() {
|
||||
var list = document.createElement('ol');
|
||||
list.classList.add(FILE_UPLOAD_INPUT_LIST_CLASS);
|
||||
var unpackEl = input.parentElement.querySelector('.' + FILE_UPLOAD_INPUT_UNPACK_CHECKBOX_CLASS);
|
||||
list.classList.add(FILE_INPUT_LIST_CLASS);
|
||||
var unpackEl = element.parentElement.querySelector('.' + FILE_INPUT_UNPACK_CHECKBOX_CLASS);
|
||||
if (unpackEl) {
|
||||
input.parentElement.insertBefore(list, unpackEl);
|
||||
element.parentElement.insertBefore(list, unpackEl);
|
||||
} else {
|
||||
input.parentElement.appendChild(list);
|
||||
element.parentElement.appendChild(list);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function addFileLabel() {
|
||||
function createFileLabel() {
|
||||
var label = document.createElement('label');
|
||||
label.classList.add(FILE_UPLOAD_INPUT_LABEL_CLASS);
|
||||
label.setAttribute('for', input.id);
|
||||
input.parentElement.insertBefore(label, input);
|
||||
label.classList.add(FILE_INPUT_LABEL_CLASS);
|
||||
label.setAttribute('for', element.id);
|
||||
element.parentElement.insertBefore(label, element);
|
||||
return label;
|
||||
}
|
||||
|
||||
function resetFileLabel() {
|
||||
if (isMulti) {
|
||||
label.innerText = i18n.selectFiles;
|
||||
function updateLabel() {
|
||||
var files = element.files;
|
||||
if (files && files.length) {
|
||||
label.innerText = isMultiFileInput ? files.length + ' ' + I18n.get('filesSelected') : files[0].name;
|
||||
} else {
|
||||
label.innerText = i18n.selectFile;
|
||||
label.innerText = isMultiFileInput ? I18n.get('selectFiles') : I18n.get('selectFile');
|
||||
}
|
||||
}
|
||||
|
||||
// initial setup
|
||||
resetFileLabel();
|
||||
input.classList.add(FILE_UPLOAD_INPUT_HIDDEN_CLASS);
|
||||
input.addEventListener('change', function() {
|
||||
input.dispatchEvent(new Event('input'));
|
||||
if (isMulti) {
|
||||
renderFileList(input.files);
|
||||
}
|
||||
|
||||
updateLabel(input.files);
|
||||
});
|
||||
|
||||
return {
|
||||
scope: input,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
}
|
||||
|
||||
// to remove previously uploaded files
|
||||
inputUtilities.push({
|
||||
name: FILE_INPUT_UTIL_NAME,
|
||||
selector: FILE_INPUT_UTIL_SELECTOR,
|
||||
setup: fileInputUtil,
|
||||
})
|
||||
|
||||
var FILE_UPLOAD_CONTAINER_CLASS = 'file-container';
|
||||
var FILE_UPLOAD_CONTAINER_CHECKED_CLASS = 'file-container--checked';
|
||||
/**
|
||||
*
|
||||
* Checkbox Utility
|
||||
* wraps native checkbox
|
||||
*
|
||||
* Attribute: (none)
|
||||
* (element must be an input of type="checkbox")
|
||||
*
|
||||
* Example usage:
|
||||
* <input type="checkbox">
|
||||
*
|
||||
*/
|
||||
|
||||
window.utils.fileCheckbox = function(input) {
|
||||
// adds eventlistener(s)
|
||||
function addListener(container) {
|
||||
input.addEventListener('change', function(event) {
|
||||
container.classList.toggle(FILE_UPLOAD_CONTAINER_CHECKED_CLASS, this.checked);
|
||||
});
|
||||
}
|
||||
var CHECKBOX_UTIL_NAME = 'checkbox';
|
||||
var CHECKBOX_UTIL_SELECTOR = 'input[type="checkbox"]';
|
||||
|
||||
// initial setup
|
||||
function setup() {
|
||||
var cont = input.parentNode;
|
||||
while (cont !== document.body) {
|
||||
if (cont.matches('.' + FILE_UPLOAD_CONTAINER_CLASS)) {
|
||||
break;
|
||||
}
|
||||
cont = cont.parentNode;
|
||||
var CHECKBOX_CLASS = 'checkbox';
|
||||
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||
|
||||
var checkboxUtil = function(element) {
|
||||
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('Checkbox utility cannot be setup without an element!');
|
||||
}
|
||||
addListener(cont);
|
||||
}
|
||||
|
||||
setup();
|
||||
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
|
||||
// throw new Error('Checkbox utility already initialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: input,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
|
||||
// throw new Error('Checkbox element\'s wrapper already has class "' + CHECKBOX_CLASS + '"!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// turns native checkboxes into custom ones
|
||||
window.utils.checkbox = function(input) {
|
||||
var siblingEl = element.nextElementSibling;
|
||||
var parentEl = element.parentElement;
|
||||
|
||||
if (!input.parentElement.classList.contains('checkbox')) {
|
||||
var parentEl = input.parentElement;
|
||||
var siblingEl = input.nextElementSibling;
|
||||
var wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add(CHECKBOX_CLASS);
|
||||
|
||||
var labelEl = document.createElement('label');
|
||||
wrapperEl.classList.add('checkbox');
|
||||
labelEl.setAttribute('for', input.id);
|
||||
wrapperEl.appendChild(input);
|
||||
labelEl.setAttribute('for', element.id);
|
||||
|
||||
wrapperEl.appendChild(element);
|
||||
wrapperEl.appendChild(labelEl);
|
||||
|
||||
if (siblingEl) {
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
} else {
|
||||
parentEl.appendChild(wrapperEl);
|
||||
}
|
||||
parentEl.insertBefore(wrapperEl, siblingEl);
|
||||
|
||||
element.classList.add(CHECKBOX_INITIALIZED_CLASS);
|
||||
|
||||
return {
|
||||
name: CHECKBOX_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scope: input,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
}
|
||||
|
||||
// turns native radio buttons into custom ones
|
||||
window.utils.radio = function(input) {
|
||||
inputUtilities.push({
|
||||
name: CHECKBOX_UTIL_NAME,
|
||||
selector: CHECKBOX_UTIL_SELECTOR,
|
||||
setup: checkboxUtil,
|
||||
});
|
||||
|
||||
if (!input.parentElement.classList.contains('radio')) {
|
||||
var parentEl = input.parentElement;
|
||||
var siblingEl = input.nextElementSibling;
|
||||
var wrapperEl = document.createElement('div');
|
||||
wrapperEl.classList.add('radio');
|
||||
wrapperEl.appendChild(input);
|
||||
|
||||
if (siblingEl && siblingEl.matches('label')) {
|
||||
wrapperEl.appendChild(siblingEl);
|
||||
}
|
||||
|
||||
parentEl.appendChild(wrapperEl);
|
||||
}
|
||||
|
||||
return {
|
||||
scope: input,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
// Override implicit submit (pressing enter) behaviour to trigger a specified submit button instead of the default
|
||||
window.utils.implicitSubmit = function(input, options) {
|
||||
var submit = options.submit;
|
||||
|
||||
if (!submit) {
|
||||
throw new Error('window.utils.implicitSubmit(input, options) needs to be passed a submit element via options');
|
||||
}
|
||||
|
||||
var doSubmit = function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
submit.click();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keypress', doSubmit);
|
||||
|
||||
return {
|
||||
scope: input,
|
||||
destroy: function() {
|
||||
input.removeEventListener('keypress', doSubmit);
|
||||
},
|
||||
};
|
||||
// register the collected input utilities
|
||||
if (UtilRegistry) {
|
||||
inputUtilities.forEach(UtilRegistry.register);
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,155 +1,196 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
/**
|
||||
*
|
||||
* Modal Utility
|
||||
*
|
||||
* Attribute: uw-modal
|
||||
*
|
||||
* Params:
|
||||
* data-modal-trigger: string
|
||||
* Selector for the element that toggles the modal.
|
||||
* If trigger element has "href" attribute the modal will be dynamically loaded from the referenced page
|
||||
* data-modal-closeable: boolean property
|
||||
* If the param is present the modal will have a close-icon and can also be closed by clicking anywhere on the overlay
|
||||
*
|
||||
* Example usage:
|
||||
* <div uw-modal data-modal-trigger='#trigger' data-modal-closeable>This is the modal content
|
||||
* <div id='trigger'>Click me to open the modal
|
||||
*/
|
||||
|
||||
var MODAL_UTIL_NAME = 'modal';
|
||||
var MODAL_UTIL_SELECTOR = '[uw-modal]';
|
||||
|
||||
var JS_INITIALIZED_CLASS = 'js-modal-initialized';
|
||||
var MODAL_OPEN_CLASS = 'modal--open';
|
||||
var MODAL_TRIGGER_CLASS = 'modal__trigger';
|
||||
var MODAL_CONTENT_CLASS = 'modal__content';
|
||||
var MAIN_CONTENT_CLASS = 'main__content-body'
|
||||
var MODAL_CLOSABLE_FLAG = 'closeable';
|
||||
var MODAL_DYNAMIC_FLAG = 'dynamic';
|
||||
var MODAL_HEADERS = {
|
||||
'Is-Modal': 'True',
|
||||
};
|
||||
var OVERLAY_CLASS = 'modal__overlay';
|
||||
var OVERLAY_OPEN_CLASS = 'modal__overlay--open';
|
||||
var CLOSER_CLASS = 'modal__closer';
|
||||
|
||||
window.utils.modal = function(modalElement, options) {
|
||||
var MODAL_INITIALIZED_CLASS = 'modal--initialized';
|
||||
var MODAL_CLASS = 'modal';
|
||||
var MODAL_OPEN_CLASS = 'modal--open';
|
||||
var MODAL_TRIGGER_CLASS = 'modal__trigger';
|
||||
var MODAL_CONTENT_CLASS = 'modal__content';
|
||||
var MODAL_OVERLAY_CLASS = 'modal__overlay';
|
||||
var MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
|
||||
var MODAL_CLOSER_CLASS = 'modal__closer';
|
||||
|
||||
if (!modalElement || modalElement.classList.contains(JS_INITIALIZED_CLASS)) {
|
||||
return;
|
||||
}
|
||||
var MAIN_CONTENT_CLASS = 'main__content-body'
|
||||
|
||||
var utilInstances = [];
|
||||
var modalUtil = function(element) {
|
||||
|
||||
var overlayElement = document.createElement('div');
|
||||
var closerElement = document.createElement('div');
|
||||
var triggerElement = document.querySelector('#' + modalElement.dataset.trigger);
|
||||
var modalUrl;
|
||||
|
||||
function setup() {
|
||||
document.body.insertBefore(modalElement, null);
|
||||
function _init() {
|
||||
if (!element) {
|
||||
throw new Error('Modal utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
setupForm();
|
||||
setupCloser();
|
||||
if (element.classList.contains(MODAL_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||
triggerElement.addEventListener('click', openHandler, false);
|
||||
// param modalTrigger
|
||||
if (!element.dataset.modalTrigger) {
|
||||
throw new Error('Modal utility cannot be setup without a trigger element!');
|
||||
} else {
|
||||
setupTrigger();
|
||||
}
|
||||
|
||||
modalElement.classList.add(JS_INITIALIZED_CLASS);
|
||||
// param modalCloseable
|
||||
if (element.dataset.modalCloseable !== undefined) {
|
||||
setupCloser();
|
||||
}
|
||||
|
||||
// setupForm();
|
||||
|
||||
// mark as initialized and add modal class for styling
|
||||
element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
|
||||
|
||||
return {
|
||||
name: MODAL_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {}
|
||||
};
|
||||
}
|
||||
|
||||
function openHandler(event) {
|
||||
function setupTrigger() {
|
||||
var triggerSelector = element.dataset.modalTrigger;
|
||||
if (!triggerSelector.startsWith('#')) {
|
||||
triggerSelector = '#' + triggerSelector;
|
||||
}
|
||||
var triggerElement = document.querySelector(triggerSelector);
|
||||
|
||||
if (!triggerElement) {
|
||||
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
|
||||
}
|
||||
|
||||
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||
triggerElement.addEventListener('click', onTriggerClicked, false);
|
||||
modalUrl = triggerElement.getAttribute('href');
|
||||
}
|
||||
|
||||
function setupCloser() {
|
||||
var closerElement = document.createElement('div');
|
||||
element.insertBefore(closerElement, null);
|
||||
closerElement.classList.add(MODAL_CLOSER_CLASS);
|
||||
closerElement.addEventListener('click', onCloseClicked, false);
|
||||
overlayElement.addEventListener('click', onCloseClicked, false);
|
||||
}
|
||||
|
||||
function onTriggerClicked(event) {
|
||||
event.preventDefault();
|
||||
open();
|
||||
}
|
||||
|
||||
function open() {
|
||||
modalElement.classList.add(MODAL_OPEN_CLASS);
|
||||
overlayElement.classList.add(OVERLAY_CLASS);
|
||||
document.body.insertBefore(overlayElement, modalElement);
|
||||
overlayElement.classList.add(OVERLAY_OPEN_CLASS);
|
||||
|
||||
var modalUrl = triggerElement.getAttribute('href');
|
||||
if (modalUrl && MODAL_DYNAMIC_FLAG in modalElement.dataset) {
|
||||
fillModal(modalUrl);
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', keyupHandler);
|
||||
}
|
||||
|
||||
function closeHandler(event) {
|
||||
function onCloseClicked(event) {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}
|
||||
|
||||
function close() {
|
||||
overlayElement.classList.remove(OVERLAY_OPEN_CLASS);
|
||||
modalElement.classList.remove(MODAL_OPEN_CLASS);
|
||||
function onKeyUp(event) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
document.removeEventListener('keyup', keyupHandler);
|
||||
function open() {
|
||||
document.body.insertBefore(element, null);
|
||||
element.classList.add(MODAL_OPEN_CLASS);
|
||||
overlayElement.classList.add(MODAL_OVERLAY_CLASS);
|
||||
document.body.insertBefore(overlayElement, element);
|
||||
overlayElement.classList.add(MODAL_OVERLAY_OPEN_CLASS);
|
||||
|
||||
if (modalUrl) {
|
||||
fillModal(modalUrl);
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', onKeyUp);
|
||||
}
|
||||
|
||||
function close() {
|
||||
overlayElement.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
|
||||
element.classList.remove(MODAL_OPEN_CLASS);
|
||||
|
||||
document.removeEventListener('keyup', onKeyUp);
|
||||
};
|
||||
|
||||
function setupForm() {
|
||||
var form = modalElement.querySelector('form');
|
||||
if (form) {
|
||||
utilInstances.push(window.utils.setup('form', form, { headers: MODAL_HEADERS, force: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function setupCloser() {
|
||||
if (MODAL_CLOSABLE_FLAG in modalElement.dataset) {
|
||||
modalElement.insertBefore(closerElement, null);
|
||||
closerElement.classList.add(CLOSER_CLASS);
|
||||
closerElement.addEventListener('click', closeHandler, false);
|
||||
overlayElement.addEventListener('click', closeHandler, false);
|
||||
}
|
||||
}
|
||||
|
||||
function fillModal(url) {
|
||||
if (!window.utils.httpClient) {
|
||||
throw new Error('httpClient not found! Can\' fetch modal content from ' + url);
|
||||
if (!HttpClient) {
|
||||
throw new Error('HttpClient not found! Can\'t fetch modal content from ' + url);
|
||||
}
|
||||
|
||||
window.utils.httpClient.get(url, MODAL_HEADERS)
|
||||
HttpClient.get(url, MODAL_HEADERS)
|
||||
.then(function(response) {
|
||||
response.text().then(processResponse);
|
||||
});
|
||||
}
|
||||
|
||||
function processResponse(reponseBody) {
|
||||
function processResponse(responseBody) {
|
||||
var responseElement = document.createElement('div');
|
||||
responseElement.innerHTML = responseBody;
|
||||
|
||||
var modalContent = document.createElement('div');
|
||||
modalContent.classList.add(MODAL_CONTENT_CLASS);
|
||||
modalContent.innerHTML = reponseBody;
|
||||
|
||||
var contentBody = modalContent.querySelector('.' + MAIN_CONTENT_CLASS);
|
||||
var contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
|
||||
|
||||
if (contentBody) {
|
||||
modalContent.innerHTML = contentBody.innerHTML;
|
||||
}
|
||||
|
||||
var previousModalContent = modalElement.querySelector('.' + MODAL_CONTENT_CLASS);
|
||||
var previousModalContent = element.querySelector('.' + MODAL_CONTENT_CLASS);
|
||||
if (previousModalContent) {
|
||||
previousModalContent.remove();
|
||||
}
|
||||
|
||||
modalContent = withPrefixedInputIDs(modalContent);
|
||||
modalElement.insertBefore(modalContent, null);
|
||||
setupForm();
|
||||
element.insertBefore(modalContent, null);
|
||||
|
||||
// setup any newly arrived utils
|
||||
UtilRegistry.setupAll(element);
|
||||
}
|
||||
|
||||
function withPrefixedInputIDs(modalContent) {
|
||||
var idAttrs = ['id', 'for', 'data-conditional-id'];
|
||||
var idAttrs = ['id', 'for', 'data-conditional-input'];
|
||||
idAttrs.forEach(function(attr) {
|
||||
modalContent.querySelectorAll('[' + attr + ']').forEach(function(input) {
|
||||
var value = modalElement.id + '__' + input.getAttribute(attr);
|
||||
Array.from(modalContent.querySelectorAll('[' + attr + ']')).forEach(function(input) {
|
||||
var value = element.id + '__' + input.getAttribute(attr);
|
||||
input.setAttribute(attr, value);
|
||||
});
|
||||
});
|
||||
return modalContent;
|
||||
}
|
||||
|
||||
function keyupHandler(event) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
function destroyUtils() {
|
||||
utilInstances.filter(function(utilInstance) {
|
||||
return !!utilInstance;
|
||||
}).forEach(function(utilInstance) {
|
||||
utilInstance.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
scope: modalElement,
|
||||
destroy: destroyUtils,
|
||||
};
|
||||
return _init();
|
||||
};
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: MODAL_UTIL_NAME,
|
||||
selector: MODAL_UTIL_SELECTOR,
|
||||
setup: modalUtil
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
var registeredSetupListeners = {};
|
||||
var activeInstances = {};
|
||||
|
||||
/**
|
||||
* setup function to initiate a util (utilName) on a scope (sope) with options (options).
|
||||
*
|
||||
* Utils need to be defined as property of `window.utils` and need to accept a scope and (optionally) options.
|
||||
* Example: `window.utils.autoSubmit = function(scope, options) { ... };`
|
||||
*/
|
||||
|
||||
window.utils.setup = function(utilName, scope, options) {
|
||||
if (!utilName || !scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
var utilInstance;
|
||||
|
||||
// i18n
|
||||
if (window.I18N) {
|
||||
options.i18n = window.I18N;
|
||||
}
|
||||
|
||||
if (activeInstances[utilName]) {
|
||||
var instanceWithSameScope = activeInstances[utilName]
|
||||
.filter(function(instance) { return !!instance; })
|
||||
.find(function(instance) {
|
||||
return instance.scope === scope;
|
||||
});
|
||||
var isAlreadySetup = !!instanceWithSameScope;
|
||||
|
||||
if (isAlreadySetup) {
|
||||
console.warn('Trying to setup a JS utility that\'s already been set up', { utility: utilName, scope, options });
|
||||
if (!options.force) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
var listener = function(event) {
|
||||
if (event.detail.targetUtil !== utilName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.setupFunction) {
|
||||
utilInstance = options.setupFunction(scope, options);
|
||||
} else {
|
||||
var util = window.utils[utilName];
|
||||
if (!util) {
|
||||
throw new Error('"' + utilName + '" is not a known js util');
|
||||
}
|
||||
|
||||
utilInstance = util(scope, options);
|
||||
}
|
||||
|
||||
if (utilInstance) {
|
||||
if (activeInstances[utilName] && Array.isArray(activeInstances[utilName])) {
|
||||
activeInstances[utilName].push(utilInstance);
|
||||
} else {
|
||||
activeInstances[utilName] = [ utilInstance ];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (registeredSetupListeners[utilName] && Array.isArray(registeredSetupListeners[utilName])) {
|
||||
window.utils.teardown(utilName);
|
||||
}
|
||||
|
||||
if (!registeredSetupListeners[utilName] || Array.isArray(registeredSetupListeners[utilName])) {
|
||||
registeredSetupListeners[utilName] = [];
|
||||
}
|
||||
registeredSetupListeners[utilName].push(listener);
|
||||
|
||||
document.addEventListener('setup', listener);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('setup', {
|
||||
detail: { targetUtil: utilName, module: 'none' },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
return utilInstance;
|
||||
};
|
||||
|
||||
window.utils.teardown = function(utilName, destroy) {
|
||||
if (registeredSetupListeners[utilName]) {
|
||||
registeredSetupListeners[utilName]
|
||||
.filter(function(listener) { return !!listener })
|
||||
.forEach(function(listener) {
|
||||
document.removeEventListener('setup', listener);
|
||||
});
|
||||
delete registeredSetupListeners[utilName];
|
||||
}
|
||||
|
||||
if (destroy === true && activeInstances[utilName]) {
|
||||
activeInstances[utilName]
|
||||
.filter(function(instance) { return !!instance })
|
||||
.forEach(function(instance) {
|
||||
instance.destroy();
|
||||
});
|
||||
delete activeInstances[utilName];
|
||||
}
|
||||
}
|
||||
})();
|
||||
@ -1,77 +1,114 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
var SHOW_HIDE_UTIL_NAME = 'showHide';
|
||||
var SHOW_HIDE_UTIL_SELECTOR = '[uw-show-hide]';
|
||||
|
||||
var JS_INITIALIZED_CLASS = 'js-show-hide-initialized';
|
||||
var LOCAL_STORAGE_SHOW_HIDE = 'SHOW_HIDE';
|
||||
var SHOW_HIDE_TOGGLE_CLASS = 'js-show-hide__toggle';
|
||||
var SHOW_HIDE_COLLAPSED_CLASS = 'js-show-hide--collapsed';
|
||||
var SHOW_HIDE_TARGET_CLASS = 'js-show-hide__target';
|
||||
var SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
|
||||
var SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
|
||||
var SHOW_HIDE_COLLAPSED_CLASS = 'show-hide--collapsed';
|
||||
var SHOW_HIDE_TOGGLE_CLASS = 'show-hide__toggle';
|
||||
var SHOW_HIDE_TOGGLE_RIGHT_CLASS = 'show-hide__toggle--right';
|
||||
|
||||
/**
|
||||
* div
|
||||
* div.js-show-hide__toggle
|
||||
* toggle here
|
||||
* div
|
||||
* content here
|
||||
*
|
||||
* ShowHide Utility
|
||||
*
|
||||
* Attribute: uw-show-hide
|
||||
*
|
||||
* Params: (all optional)
|
||||
* data-show-hide-id: string
|
||||
* If this param is given the state of the utility will be persisted in the clients local storage.
|
||||
* data-show-hide-collapsed: boolean property
|
||||
* If this param is present the ShowHide utility will be collapsed. This value will be overruled by any value stored in the LocalStorage.
|
||||
* data-show-hide-align: 'right'
|
||||
* Where to put the arrow that marks the element as a ShowHide toggle. Left of toggle by default.
|
||||
*
|
||||
* Example usage:
|
||||
* <div>
|
||||
* <div uw-show-hide>Click me
|
||||
* <div>This will be toggled
|
||||
* <div>This will be toggled as well
|
||||
*/
|
||||
window.utils.showHide = function(wrapper, options) {
|
||||
var showHideUtil = function(element) {
|
||||
|
||||
options = options || {};
|
||||
var showHideId;
|
||||
|
||||
function addEventHandler(el) {
|
||||
el.addEventListener('click', function elClickListener() {
|
||||
var newState = el.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
|
||||
updateLSState(el.dataset.shIndex || null, newState);
|
||||
function init() {
|
||||
if (!element) {
|
||||
throw new Error('ShowHide utility cannot be setup without an element!');
|
||||
}
|
||||
|
||||
if (element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// register click listener
|
||||
addClickListener();
|
||||
|
||||
// param showHideId
|
||||
if (element.dataset.showHideId) {
|
||||
showHideId = element.dataset.showHideId;
|
||||
}
|
||||
|
||||
// param showHideCollapsed
|
||||
var collapsed = false;
|
||||
if (element.dataset.showHideCollapsed !== undefined) {
|
||||
collapsed = true;
|
||||
}
|
||||
if (showHideId) {
|
||||
var localStorageCollapsed = getLocalStorage()[showHideId];
|
||||
if (typeof localStorageCollapsed !== 'undefined') {
|
||||
collapsed = localStorageCollapsed;
|
||||
}
|
||||
}
|
||||
element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
|
||||
|
||||
// param showHideAlign
|
||||
var alignment = element.dataset.showHideAlign;
|
||||
if (alignment === 'right') {
|
||||
element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
|
||||
}
|
||||
|
||||
// mark as initialized
|
||||
element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
||||
|
||||
return {
|
||||
name: SHOW_HIDE_UTIL_NAME,
|
||||
element: element,
|
||||
destroy: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
function addClickListener() {
|
||||
element.addEventListener('click', function clickListener() {
|
||||
var newState = element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
|
||||
|
||||
if (showHideId) {
|
||||
setLocalStorage(showHideId, newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateLSState(index, state) {
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
var lsData = getLocalStorageData();
|
||||
lsData[index] = state;
|
||||
window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData));
|
||||
function setLocalStorage(id, state) {
|
||||
var lsData = getLocalStorage();
|
||||
lsData[id] = state;
|
||||
window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData));
|
||||
}
|
||||
|
||||
function collapsedStateInLocalStorage(index) {
|
||||
var lsState = getLocalStorageData();
|
||||
return lsState[index];
|
||||
function getLocalStorage() {
|
||||
return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {};
|
||||
}
|
||||
|
||||
function getLocalStorageData() {
|
||||
return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SHOW_HIDE)) || {};
|
||||
}
|
||||
|
||||
Array
|
||||
.from(wrapper.querySelectorAll('.' + SHOW_HIDE_TOGGLE_CLASS))
|
||||
.forEach(function(el) {
|
||||
if (el.classList.contains(JS_INITIALIZED_CLASS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = el.dataset.shIndex || null;
|
||||
var isCollapsed = el.dataset.collapsed === 'true';
|
||||
var lsCollapsedState = collapsedStateInLocalStorage(index);
|
||||
if (typeof lsCollapsedState !== 'undefined') {
|
||||
isCollapsed = lsCollapsedState;
|
||||
}
|
||||
el.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, isCollapsed);
|
||||
|
||||
Array.from(el.parentElement.children).forEach(function(el) {
|
||||
if (!el.classList.contains('' + SHOW_HIDE_TOGGLE_CLASS)) {
|
||||
el.classList.add(SHOW_HIDE_TARGET_CLASS);
|
||||
}
|
||||
});
|
||||
el.classList.add(JS_INITIALIZED_CLASS);
|
||||
addEventHandler(el);
|
||||
});
|
||||
|
||||
return {
|
||||
scope: wrapper,
|
||||
destroy: function() {},
|
||||
};
|
||||
return init();
|
||||
};
|
||||
|
||||
if (UtilRegistry) {
|
||||
UtilRegistry.register({
|
||||
name: SHOW_HIDE_UTIL_NAME,
|
||||
selector: SHOW_HIDE_UTIL_SELECTOR,
|
||||
setup: showHideUtil
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
Der Handler sollte jeweils aktuelle Beispiele für alle möglichen Funktionalitäten enthalten, so dass man immer weiß, wo man nachschlagen kann.
|
||||
|
||||
<section>
|
||||
<h2 .js-show-hide__toggle>Teilweise funktionierende Abschnitte
|
||||
<h2 uw-show-hide>Teilweise funktionierende Abschnitte
|
||||
|
||||
<ul>
|
||||
<li .list-group-item>
|
||||
@ -26,11 +26,11 @@
|
||||
<li>
|
||||
Knopf-Test:
|
||||
^{btnForm}
|
||||
<li><br>
|
||||
Modals:
|
||||
^{modal "Klick mich für Ajax-Test" (Left $ SomeRoute UsersR)}
|
||||
^{modal "Klick mich für Content-Test" (Right "Test Inhalt für Modal")}
|
||||
<li>
|
||||
^{modal "Email-Test" (Right emailWidget')}
|
||||
Modals:
|
||||
<ul>
|
||||
<li>^{modal "Klick mich für Ajax-Test" (Left $ SomeRoute UsersR)}
|
||||
<li>^{modal "Klick mich für Content-Test" (Right "Test Inhalt für Modal")}
|
||||
<li>^{modal "Email-Test" (Right emailWidget')}
|
||||
<li>
|
||||
Some icons: ^{isVisible False} ^{hasComment True}
|
||||
|
||||
@ -12,7 +12,7 @@ $newline never
|
||||
|
||||
^{pageHead pc}
|
||||
|
||||
<body .no-js .theme--#{toPathPiece currentTheme} :isAuth:.logged-in :isModal:.modal>
|
||||
<body .no-js .theme--#{toPathPiece currentTheme} :isAuth:.logged-in>
|
||||
<!-- removes no-js class from body if client supports javascript -->
|
||||
<script>
|
||||
document.body.classList.remove('no-js');
|
||||
|
||||
@ -6,7 +6,7 @@ $if not isModal
|
||||
|
||||
<div .main>
|
||||
|
||||
<div .main__content>
|
||||
<div .main__content uw-poc>
|
||||
|
||||
$if not isModal
|
||||
<!-- breadcrumbs -->
|
||||
|
||||
@ -35,16 +35,12 @@ function setupDatepicker(wrapper) {
|
||||
});
|
||||
}
|
||||
|
||||
// this global I18N object will be picked up automatically by the setup util
|
||||
window.I18N = {
|
||||
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
|
||||
selectFile: 'Datei auswählen',
|
||||
selectFiles: 'Datei(en) auswählen',
|
||||
asyncFormFailure: 'Da ist etwas schief gelaufen, das tut uns Leid.<br>Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben.<br><br>Vielen Dank für deine Hilfe',
|
||||
};
|
||||
if (I18n) {
|
||||
I18n.addMany(#{frontendI18n});
|
||||
} else {
|
||||
throw new Error('I18n JavaScript service is missing!');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.utils.setup('flatpickr', document.body, { setupFunction: setupDatepicker });
|
||||
window.utils.setup('showHide', document.body);
|
||||
window.utils.setup('inputs', document.body);
|
||||
setupDatepicker(document.body);
|
||||
});
|
||||
|
||||
@ -189,27 +189,21 @@ h4 {
|
||||
}
|
||||
|
||||
@media (min-width: 426px) {
|
||||
:not(.modal) {
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-md, 50px);
|
||||
}
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-md, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
:not(.modal) {
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-lg, 20%);
|
||||
margin-top: var(--header-height);
|
||||
}
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-lg, 20%);
|
||||
margin-top: var(--header-height);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
:not(.modal) {
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-xl, 250px);
|
||||
}
|
||||
.main__content {
|
||||
margin-left: var(--asidenav-width-xl, 250px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<p>
|
||||
_{MsgHelpIntroduction}
|
||||
^{form}
|
||||
^{formWidget}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
$newline text
|
||||
<section>
|
||||
UniWorX erfahrene Veranstalter finden
|
||||
hier die wichtigsten Neuerungen.
|
||||
@ -6,6 +7,9 @@
|
||||
<h2>Bekannte Probleme in Bearbeitung
|
||||
|
||||
<dl .deflist>
|
||||
<dt .deflist__dt>Derzeit keine bekannt.
|
||||
|
||||
|
||||
$#
|
||||
$# MOVE ITEM TO SECTION "VERANSTALTUNGEN", once it is implemented:
|
||||
$#
|
||||
@ -81,6 +85,15 @@
|
||||
von der Studentenkanzelei keine aktuelle und vollständige Schlüsselzuordnung
|
||||
bekommen können.
|
||||
|
||||
<dt .deflist__dt> Aus Studentensicht
|
||||
<dd .deflist__dd>
|
||||
<p>
|
||||
UniWorX hatte spezielle Links "Aus Studentensicht", welche in Uni2work überflüssig geworden sind.
|
||||
Stattdessen kann man sich in Uni2work #
|
||||
<a href=@{AuthPredsR}>Berechtigungen hier temporär selbst entziehen
|
||||
. Um die eigene Veranstaltung aus Sicht eines Teilnehmers zu sehen, deaktiviert man #
|
||||
die Berechtigungsprüfungen "_{MsgAuthTagLecturer}" und/oder "_{MsgAuthTagCorrector}"
|
||||
|
||||
<section>
|
||||
<h2>Übungsbetrieb
|
||||
|
||||
@ -114,14 +127,18 @@
|
||||
<dt .deflist__dt> Lösungshinweise
|
||||
<dd .deflist__dd>
|
||||
Zusätzlich zu Aufgabe und Lösung können Hinweise ab einem
|
||||
Datum vor Abgabfrist freigeschaltet werden,
|
||||
Datum vor Ende des Abgabezeitraums freigeschaltet werden,
|
||||
z.B. Lösungen zu Präsenzaufgaben.
|
||||
|
||||
<dt .deflist__dt> Sichtbarkeit
|
||||
<dd .deflist__dd>
|
||||
Übungsblätter können bis zu einem Datum vor den Teilnehmern versteckt werden.
|
||||
<p>
|
||||
Die Aufgabenstellung ist erst mit Eröffnung der Abgabe erhältlich,
|
||||
Übungsblätter können bis zu einem Datum "Sichtabr ab" vor allen Teilnehmern versteckt werden.
|
||||
Das kann nützlich sein, um Tutoren und Korrektoren ein provisorisches Übungsblatt verfügbar zu machen,
|
||||
dessen Bewertungsmodalitäten und Fristen sich noch ändern können.
|
||||
<p>
|
||||
Erst wenn das Blatt sichtbar wird, sehen die Teilnehmer in Ihrer Übersichtsliste.
|
||||
Alle Dateien zur Aufgabenstellung sind aber erst mit Beginn des Abgabezeitraums erhältlich,
|
||||
so wie bisher in UniWorX auch.
|
||||
|
||||
<dt .deflist__dt> Zeitstempel
|
||||
|
||||
@ -1,15 +1,22 @@
|
||||
$newline never
|
||||
$forall FileUploadInfo{..} <- fileInfos
|
||||
<div .file-container :fuiChecked:.file-container--checked>
|
||||
<label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle}
|
||||
<div .checkbox>
|
||||
<input .file-container__checkbox.file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
|
||||
<label for=#{fuiHtmlId}>
|
||||
$if not (null fileInfos)
|
||||
<div .file-uploads-label>_{MsgPreviouslyUploadedInfo}
|
||||
<ol .file-input__list>
|
||||
$forall FileUploadInfo{..} <- fileInfos
|
||||
<li>
|
||||
<div .file-container>
|
||||
<label for=#{fuiHtmlId}>#{fuiTitle}
|
||||
<input type=checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId}>
|
||||
|
||||
<div .file-input__info>
|
||||
_{MsgPreviouslyUploadedDeletionInfo}
|
||||
|
||||
<div .file-uploads-label>_{MsgAddMoreFiles}
|
||||
|
||||
$# new files
|
||||
<input type="file" name=#{fieldName} id=#{fieldId} multiple :req:required="required">
|
||||
<input type="file" uw-file-input name=#{fieldName} id=#{fieldId} multiple :req:required="required">
|
||||
|
||||
<div .file-input__multi-info>
|
||||
<div .file-input__info>
|
||||
_{MsgMultiFileUploadInfo}
|
||||
|
||||
<div .file-input__unpack>
|
||||
|
||||
@ -9,47 +9,3 @@
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-input__multi-info {
|
||||
font-size: .9rem;
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
color: var(--color-fontsec);
|
||||
}
|
||||
|
||||
.file-input__list {
|
||||
margin-left: 15px;
|
||||
margin-top: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.file-container--checked {
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.file-container__label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
/* TODO: get this from .msg-file */
|
||||
content: '(wird entfernt)';
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-container__label {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
24
templates/profile.julius
Normal file
24
templates/profile.julius
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_THEME = 'theme--default';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
var themeSwitcher = document.querySelector('#theme-select');
|
||||
var currentTheme = DEFAULT_THEME;
|
||||
|
||||
if (themeSwitcher) {
|
||||
currentTheme = 'theme--' + themeSwitcher.value;
|
||||
themeSwitcher.addEventListener('input', function() {
|
||||
|
||||
var desiredTheme = 'theme--' + themeSwitcher.value;
|
||||
document.body.classList.remove(currentTheme);
|
||||
document.body.classList.add(desiredTheme);
|
||||
currentTheme = desiredTheme;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
@ -1,6 +1,6 @@
|
||||
$newline never
|
||||
<div .scrolltable>
|
||||
<table *{dbsAttrs'}>
|
||||
<table *{dbsAttrs'} data-async-table-db-header=#{toPathPiece HeaderDBTableShortcircuit}>
|
||||
$maybe wHeaders' <- wHeaders
|
||||
<thead>
|
||||
<tr .table__row.table__row--head>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
$newline never
|
||||
<div .table-filter>
|
||||
<h3 .js-show-hide__toggle data-sh-index=table-filter data-collapsed=true>Filter
|
||||
<h3 .table-filter__toggle uw-show-hide data-show-hide-id=table-filter data-show-hide-collapsed>Filter
|
||||
<div>
|
||||
^{filterForm}
|
||||
^{scrolltable}
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
.table-filter {
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.table-filter__toggle {
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
$newline never
|
||||
<div ##{wIdent "table-wrapper"}>
|
||||
<div ##{wIdent "table-wrapper"} uw-async-table>
|
||||
^{table}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var dbtIdent = #{String dbtIdent};
|
||||
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
|
||||
var selector = '#' + dbtIdent + '-table-wrapper';
|
||||
var wrapper = document.querySelector(selector);
|
||||
|
||||
if (wrapper) {
|
||||
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
|
||||
}
|
||||
});
|
||||
@ -3,7 +3,7 @@ $newline never
|
||||
$case formLayout
|
||||
$of FormDBTablePagesize
|
||||
$forall view <- fieldViews
|
||||
<label .form-group__label.label-pagesize for=#{fvId view}>#{fvLabel view}
|
||||
<label .form-group-label.label-pagesize for=#{fvId view}>#{fvLabel view}
|
||||
^{fvInput view}
|
||||
$of _
|
||||
$forall view <- fieldViews
|
||||
@ -13,10 +13,11 @@ $case formLayout
|
||||
$else
|
||||
<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 for=#{fvId view}>
|
||||
#{fvLabel view}
|
||||
<label .form-group-label for=#{fvId view}>
|
||||
<span .form-group-label__caption>
|
||||
#{fvLabel view}
|
||||
$maybe hint <- fvTooltip view
|
||||
<div .form-group__hint>^{hint}
|
||||
<div .form-group-label__hint>^{hint}
|
||||
<div .form-group__input>
|
||||
^{fvInput view}
|
||||
$maybe err <- fvErrors view
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div #alerts-1 .alerts> <!-- make wIdent work here instead of '#alerts-1' -->
|
||||
<div #alerts-1 .alerts uw-alerts>
|
||||
<div .alerts__toggler>
|
||||
$forall (status, msg) <- mmsgs
|
||||
$with status2 <- bool status "info" (status == "")
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var alertsElement = document.querySelector('#' + 'alerts-1');
|
||||
window.utils.setup('alerts', alertsElement);
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<aside .main__aside>
|
||||
<aside .main__aside uw-asidenav>
|
||||
<div .asidenav__logo>
|
||||
<a href="/" .asidenav__logo-link>
|
||||
<span .asidenav__logo-link-item.asidenav__logo-lmu>LMU
|
||||
@ -7,10 +7,10 @@ $newline never
|
||||
|
||||
<div .asidenav>
|
||||
$forall tid <- favouriteTerms
|
||||
<div .asidenav__box.js-show-hide>
|
||||
<h3 .asidenav__box-title.js-show-hide__toggle data-sh-index="#{termToText tid}">
|
||||
<div .asidenav__box>
|
||||
<h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right>
|
||||
_{ShortTermIdentifier tid}
|
||||
<ul .asidenav__list.js-show-hide__target.list--iconless>
|
||||
<ul .asidenav__list.list--iconless>
|
||||
$forall (Course{courseShorthand, courseName}, courseRoute, pageActions) <- favouriteTerm tid
|
||||
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
|
||||
<a .asidenav__link-wrapper href=@{courseRoute}>
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var asidenavEl = document.querySelector('.main__aside');
|
||||
window.utils.setup('aside', asidenavEl);
|
||||
});
|
||||
@ -15,5 +15,5 @@ $# Wrapper for all kinds of forms
|
||||
^{submitButtonView}
|
||||
$of FormAutoSubmit
|
||||
^{formWidget}
|
||||
<button type=submit data-autosubmit>
|
||||
<button type=submit uw-auto-submit-button>
|
||||
^{btnLabel BtnSubmit}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.utils.setup('form', document.querySelector('#' + #{String formId}));
|
||||
});
|
||||
@ -6,7 +6,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var cellInputs = Array.from(form.querySelectorAll('.massinput--cell input:not([type=hidden])'));
|
||||
|
||||
cellInputs.forEach(function(input) {
|
||||
window.utils.setup('implicitSubmit', input, { submit: formSubmit });
|
||||
makeImplicitSubmit(input, formSubmit);
|
||||
});
|
||||
|
||||
|
||||
@ -15,7 +15,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var addInputs = Array.from(wrapper.querySelectorAll('input:not([type=hidden]):not(.btn-mass-input-add)'));
|
||||
|
||||
addInputs.forEach(function(input) {
|
||||
window.utils.setup('implicitSubmit', input, { submit: addSubmit });
|
||||
makeImplicitSubmit(input, addSubmit);
|
||||
});
|
||||
});
|
||||
|
||||
// Override implicit submit (pressing enter) behaviour to trigger a specified submit button instead of the default
|
||||
function makeImplicitSubmit(input, submit) {
|
||||
if (!submit) {
|
||||
throw new Error('implicitSubmit(input, options) needs to be passed a submit element via options');
|
||||
}
|
||||
|
||||
var doSubmit = function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
submit.click();
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keypress', doSubmit);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
$newline never
|
||||
<div .modal.js-modal #modal-#{modalId'} data-trigger=#{triggerId'} data-closeable :isDynamic:data-dynamic>
|
||||
<div uw-modal data-modal-trigger=##{triggerId'} data-modal-closeable>
|
||||
$case modalContent
|
||||
$of Right content
|
||||
<div .modal__content>
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var modal = document.querySelector('#modal-' + #{String modalId'});
|
||||
if (modal) {
|
||||
window.utils.setup('modal', modal);
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
<fieldset data-conditional-id="#{fvId actionView}" data-conditional-value="#{toPathPiece act}">
|
||||
<fieldset uw-interactive-fieldset data-conditional-input=#{fvId actionView} data-conditional-value=#{toPathPiece act}>
|
||||
<legend>
|
||||
_{act}
|
||||
^{w}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.utils = window.utils || {};
|
||||
|
||||
window.utils.stickynav = function (nav) {
|
||||
var ticking = false;
|
||||
|
||||
init();
|
||||
function init() {
|
||||
window.addEventListener('scroll', function (e) {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(update);
|
||||
ticking = true;
|
||||
}
|
||||
}, false);
|
||||
update();
|
||||
}
|
||||
|
||||
function update() {
|
||||
var sticky = window.scrollY > 30;
|
||||
nav.classList.toggle('navbar--sticky', sticky);
|
||||
ticking = false;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
document.addEventListener('setup', function (e) {
|
||||
// utils.stickynav(e.detail.scope.querySelector('.js-sticky-navbar'));
|
||||
});
|
||||
@ -7,7 +7,7 @@ $newline never
|
||||
$of PageActionPrime
|
||||
<div .pagenav__list-item>
|
||||
$if menuItemModal
|
||||
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
|
||||
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
|
||||
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
|
||||
$of _
|
||||
$if hasSecondaryPageActions
|
||||
@ -18,6 +18,6 @@ $newline never
|
||||
$of PageActionSecondary
|
||||
<div .pagenav__list-item.pagenav__list-item--secondary>
|
||||
$if menuItemModal
|
||||
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
|
||||
<div uw-modal data-modal-trigger=#{menuIdent} data-modal-closeable>
|
||||
<a .pagenav__link-wrapper.pagenav__link-wrapper--secondary href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
|
||||
$of _
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
$# Display Rating, expects
|
||||
$# sub :: Submission
|
||||
$# submissionRatingDone :: Submission -> Bool
|
||||
$# submissionRatingPoints :: Maybe points
|
||||
|
||||
$maybe points <- submissionRatingPoints
|
||||
$maybe grading <- preview _grading sheetType
|
||||
$if submissionRatingDone sub
|
||||
$maybe (grading, points) <- mTuple (preview _grading sheetType) submissionRatingPoints
|
||||
$case grading
|
||||
$of Points{..}
|
||||
_{MsgAchievedOf points maxPoints}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user