Merge branch 'master' into feat/tokens

This commit is contained in:
Gregor Kleen 2019-04-17 09:09:20 +02:00
commit d037434dc2
60 changed files with 1691 additions and 1333 deletions

View File

@ -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
View 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!

View File

@ -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.

View File

@ -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

View File

@ -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", "")]
}

View File

@ -170,4 +170,4 @@ homeUpcomingSheets uid = do
, dbtParams = def
, dbtIdent = "upcoming-sheets" :: Text
}
$(widgetFile "home/upcomingSheets")
$(widgetFile "home/upcomingSheets")

View File

@ -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>

View File

@ -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(..))

View 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

View File

@ -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

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -5,10 +5,6 @@
display: flex;
}
.radio-group__option {
min-width: 30px;
}
.radio {
position: relative;
display: inline-block;

View File

@ -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;

View File

@ -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 = {

View 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,
};
})();
})();

View 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);
},
};
})();

View File

@ -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,
});
}
})();

View File

@ -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,
});
}
})();

View File

@ -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
});
}
})();

View File

@ -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,
});
}
})();

View File

@ -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() {},
};
}
})();

View File

@ -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
});
}
})();

View File

@ -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);
}
})();

View File

@ -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);
}
})();

View File

@ -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
});
}
})();

View File

@ -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];
}
}
})();

View File

@ -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
});
}
})();

View File

@ -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}

View File

@ -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');

View File

@ -6,7 +6,7 @@ $if not isModal
<div .main>
<div .main__content>
<div .main__content uw-poc>
$if not isModal
<!-- breadcrumbs -->

View File

@ -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);
});

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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;
}

View 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;
});
}
});
})();

View File

@ -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>

View File

@ -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}

View File

@ -1,3 +1,7 @@
.table-filter {
margin-bottom: 13px;
}
.table-filter__toggle {
padding: 3px 7px;
}

View File

@ -1,3 +1,3 @@
$newline never
<div ##{wIdent "table-wrapper"}>
<div ##{wIdent "table-wrapper"} uw-async-table>
^{table}

View File

@ -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 });
}
});

View File

@ -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

View File

@ -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 == "")

View File

@ -1,4 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
var alertsElement = document.querySelector('#' + 'alerts-1');
window.utils.setup('alerts', alertsElement);
});

View File

@ -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}>

View File

@ -1,4 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
var asidenavEl = document.querySelector('.main__aside');
window.utils.setup('aside', asidenavEl);
});

View File

@ -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}

View File

@ -1,3 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
window.utils.setup('form', document.querySelector('#' + #{String formId}));
});

View File

@ -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);
}
});

View File

@ -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>

View File

@ -1,6 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
var modal = document.querySelector('#modal-' + #{String modalId'});
if (modal) {
window.utils.setup('modal', modal);
}
});

View File

@ -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}

View File

@ -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'));
});

View File

@ -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 _