Merge branch 'master' into feat/tokens
This commit is contained in:
commit
d037434dc2
2
hlint.sh
2
hlint.sh
@ -1,3 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec -- ./test.sh uniworx:test:hlint
|
||||
exec -- stack build --test --fast --flag uniworx:dev --flag uniworx:library-only 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!
|
||||
@ -263,7 +263,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
|
||||
@ -307,7 +307,10 @@ SettingsUpdate: Einstellungen erfolgreich gespeichert
|
||||
NotificationSettingsUpdate: Benachrichtigungs-Einstellungen erfolgreich 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
|
||||
@ -757,4 +760,4 @@ EmailInvitationWarning: Dem System ist kein Nutzer mit dieser Addresse bekannt.
|
||||
LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lType} für #{csh} eingetragen
|
||||
LecturerInvitationDeclined csh@CourseShorthand: Sie haben die Einladung, Kursverwalter für #{csh} zu werden, abgelehnt
|
||||
CourseLecInviteHeading courseName@Text: Einladung zum Kursverwalter für #{courseName}
|
||||
CourseLecInviteExplanation: Sie wurden eingeladen, Verwalter für einen Kurs zu sein.
|
||||
CourseLecInviteExplanation: Sie wurden eingeladen, Verwalter für einen Kurs zu sein.
|
||||
|
||||
@ -201,6 +201,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.
|
||||
@ -1129,6 +1130,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
|
||||
@ -1140,20 +1146,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
|
||||
|
||||
@ -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", "")]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -170,4 +170,4 @@ homeUpcomingSheets uid = do
|
||||
, dbtParams = def
|
||||
, dbtIdent = "upcoming-sheets" :: Text
|
||||
}
|
||||
$(widgetFile "home/upcomingSheets")
|
||||
$(widgetFile "home/upcomingSheets")
|
||||
@ -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>
|
||||
|
||||
@ -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 (CachedMemoT(..))
|
||||
|
||||
|
||||
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(tableFilterForm);
|
||||
}
|
||||
}
|
||||
|
||||
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(tableFilterForm) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
tableFilterForm.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,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/profile.julius
Normal file
24
templates/profile/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 _
|
||||
|
||||
Loading…
Reference in New Issue
Block a user