Merge branch 'js-utils-cleanup'

This commit is contained in:
Felix Hamann 2019-02-20 22:07:02 +01:00
commit 55c1385e3e
80 changed files with 790 additions and 712 deletions

View File

@ -7,7 +7,7 @@ import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead)
import Utils.Form
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
@ -54,4 +54,4 @@ dummyLogin = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm
$(widgetFile "widgets/dummy-login-form")
$(widgetFile "widgets/dummy-login-form/dummy-login-form")

View File

@ -36,7 +36,7 @@ data CampusMessage = MsgCampusIdentNote
| MsgCampusSubmit
| MsgCampusInvalidCredentials
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSettings userFilter
@ -48,7 +48,7 @@ findUser LdapConf{..} ldap campusIdent = Ldap.search ldap ldapBase userSearchSet
, Ldap.time ldapSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways
]
userPrincipalName :: Ldap.Attr
userPrincipalName = Ldap.Attr "userPrincipalName"
@ -105,7 +105,7 @@ campusLogin conf@LdapConf{..} pool = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard campusForm
$(widgetFile "widgets/campus-login-form")
$(widgetFile "widgets/campus-login/campus-login-form")
data CampusUserException = CampusUserLdapError LdapPoolError
| CampusUserHostNotResolved String

View File

@ -35,7 +35,7 @@ hashForm = HashLogin
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
<* submitButton
hashLogin :: ( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
@ -90,5 +90,5 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
$(widgetFile "widgets/hash-login-form")
$(widgetFile "widgets/hash-login-form/hash-login-form")

View File

@ -982,19 +982,19 @@ siteLayout' headingOverride widget = do
-- you to use normal widget features in default-layout.
navbar :: Widget
navbar = $(widgetFile "widgets/navbar")
navbar = $(widgetFile "widgets/navbar/navbar")
asidenav :: Widget
asidenav = $(widgetFile "widgets/asidenav")
asidenav = $(widgetFile "widgets/asidenav/asidenav")
footer :: Widget
footer = $(widgetFile "widgets/footer")
footer = $(widgetFile "widgets/footer/footer")
alerts :: Widget
alerts = $(widgetFile "widgets/alerts/alerts")
contentHeadline :: Maybe Widget
contentHeadline = headingOverride <|> (pageHeading =<< mcurrentRoute)
breadcrumbsWgt :: Widget
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs")
breadcrumbsWgt = $(widgetFile "widgets/breadcrumbs/breadcrumbs")
pageaction :: Widget
pageaction = $(widgetFile "widgets/pageaction")
pageaction = $(widgetFile "widgets/pageaction/pageaction")
-- functions to determine if there are page-actions (primary or secondary)
hasPageActions, hasSecondaryPageActions, hasPrimaryPageActions :: Bool
hasPageActions = hasPrimaryPageActions || hasSecondaryPageActions
@ -1002,25 +1002,35 @@ siteLayout' headingOverride widget = do
hasPrimaryPageActions = any (is _PageActionPrime) $ toListOf (traverse . _1 . _menuItemType) menuTypes
pc <- widgetToPageContent $ do
addScript $ StaticR js_vendor_zepto_js
-- 3rd party
addScript $ StaticR js_vendor_flatpickr_js
addScript $ StaticR js_polyfills_fetchPolyfill_js
addScript $ StaticR js_polyfills_urlPolyfill_js
addScript $ StaticR js_utils_featureChecker_js
addScript $ StaticR js_utils_tabber_js
addScript $ StaticR js_utils_alerts_js
addScript $ StaticR js_vendor_zepto_js
addStylesheet $ StaticR css_vendor_flatpickr_css
addStylesheet $ StaticR css_vendor_fontawesome_css
-- fonts
addStylesheet $ StaticR css_fonts_css
addStylesheet $ StaticR css_utils_tabber_css
-- polyfills
addScript $ StaticR js_polyfills_fetchPolyfill_js
addScript $ StaticR js_polyfills_urlPolyfill_js
-- JavaScript utils
addScript $ StaticR js_utils_alerts_js
addScript $ StaticR js_utils_asidenav_js
addScript $ StaticR js_utils_asyncTable_js
addScript $ StaticR js_utils_form_js
addScript $ StaticR js_utils_inputs_js
addScript $ StaticR js_utils_setup_js
addScript $ StaticR js_utils_showHide_js
addScript $ StaticR js_utils_tabber_js
addStylesheet $ StaticR css_utils_alerts_scss
addStylesheet $ StaticR css_utils_asidenav_scss
addStylesheet $ StaticR css_utils_form_scss
addStylesheet $ StaticR css_utils_inputs_scss
addStylesheet $ StaticR css_utils_showHide_scss
addStylesheet $ StaticR css_utils_tabber_scss
addStylesheet $ StaticR css_utils_tooltip_scss
-- widgets
$(widgetFile "default-layout")
$(widgetFile "standalone/modal")
$(widgetFile "standalone/showHide")
$(widgetFile "standalone/inputs")
$(widgetFile "standalone/tooltip")
$(widgetFile "standalone/tabber")
$(widgetFile "standalone/datepicker")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")

View File

@ -162,7 +162,7 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
cid <- encrypt subId
return $ CSubmissionR tid ssh csh sheetName cid CorrectionR
in mconcat
[ anchorCellM mkRoute $(widgetFile "widgets/rating")
[ anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
, writerCell $ do
let
summary :: SheetTypeSummary

View File

@ -297,7 +297,7 @@ registerForm registered msecret extra = do
(Just _) | not registered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
_ -> return (Nothing,Nothing)
(btnRes, btnView) <- mreq (buttonField $ bool BtnRegister BtnDeregister registered) "buttonField ignores settings anyway" Nothing
let widget = $(widgetFile "widgets/registerForm")
let widget = $(widgetFile "widgets/register-form/register-form")
let msecretRes | Just res <- msecretRes' = Just <$> res
| otherwise = FormSuccess Nothing
return (btnRes *> ((==msecret) <$> msecretRes), widget) -- checks that correct button was pressed, and ignores result of btnRes

View File

@ -221,7 +221,7 @@ getProfileDataR = do
-- Delete Button
(btnWdgt, btnEnctype) <- generateFormPost (buttonForm :: Form ButtonDelete)
defaultLayout $ do
let delWdgt = $(widgetFile "widgets/data-delete")
let delWdgt = $(widgetFile "widgets/data-delete/data-delete")
$(widgetFile "profileData")
$(widgetFile "dsgvDisclaimer")

View File

@ -205,7 +205,7 @@ getSheetListR tid ssh csh = do
mkRoute = do
cid' <- mkCid
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
acell = anchorCellM mkRoute $(widgetFile "widgets/rating")
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
in cellTell acell $ stats submissionRatingPoints
, sortable Nothing -- (Just "percent")

View File

@ -182,7 +182,7 @@ postAdminUserR uuid = do
return (school, cbAdmin, cbLecturer)
let result = forM boxRights $ \(Entity sid _, (resAdmin,_), (resLecturer, _)) ->
(,,) <$> pure sid <*> resAdmin <*> resLecturer
return (result,$(widgetFile "widgets/user-rights-form"))
return (result,$(widgetFile "widgets/user-rights-form/user-rights-form"))
let userRightsAction changes = do
void . runDB $
forM changes $ \(sid, userAdmin, userLecturer) ->

View File

@ -90,9 +90,9 @@ getDeleteR DeleteRoute{..} = do
(deleteFormWdgt, deleteFormEnctype) <- generateFormPost $ confirmForm' drRecords confirmString
Just targetRoute <- getCurrentRoute
sendResponse =<<
defaultLayout $(widgetFile "widgets/delete-confirmation")
defaultLayout $(widgetFile "widgets/delete-confirmation/delete-confirmation")

View File

@ -591,9 +591,9 @@ multiAction acts defAction = do
widgets <- mapM mToWidget results
let actionWidgets = Map.foldrWithKey accWidget [] widgets
accWidget _act Nothing = id
accWidget act (Just w) = cons $(widgetFile "widgets/multiAction")
accWidget act (Just w) = cons $(widgetFile "widgets/multi-action/multi-action")
actionResults = Map.map fst results
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multiActionCollect"))
return ((actionResults Map.!) =<< actionRes, $(widgetFile "widgets/multi-action/multi-action-collect"))
multiActionA :: (RenderMessage UniWorX action, PathPiece action, Ord action, Eq action)
=> FieldSettings UniWorX

View File

@ -25,7 +25,7 @@ gradeSummaryWidget title sts =
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
hasPoints = positiveSum $ numSheetsPoints sumSummaries
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
rowWdgts = [ $(widgetFile "widgets/gradingSummaryRow")
rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row")
| (sumHeader,summary) <-
[ (MsgSheetTypeNormal' ,normalSummary)
, (MsgSheetTypeBonus' ,bonusSummary)
@ -33,4 +33,4 @@ gradeSummaryWidget title sts =
] ]
in if 0 == numSheets sumSummaries
then mempty
else $(widgetFile "widgets/gradingSummary")
else $(widgetFile "widgets/grading-summary/grading-summary")

View File

@ -576,7 +576,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
, let t' = toPathPiece $ SortingSetting t d
]
wIdent :: Text -> Text
wIdent = toPathPiece . WithIdent dbtIdent
wIdent = toPathPiece . WithIdent dbtIdent
dbsAttrs'
| not $ null dbtIdent = ("id", dbtIdent) : dbsAttrs
| otherwise = dbsAttrs
@ -802,7 +802,7 @@ cellTooltip :: (RenderMessage UniWorX msg, IsDBTable m a) => msg -> DBCell m a -
cellTooltip msg = cellContents.mapped %~ (<> tipWdgt)
where
tipWdgt = [whamlet|
<div .js-tooltip>
<div .tooltip>
<div .tooltip__handle>
<div .tooltip__content>_{msg}
|]

View File

@ -9,7 +9,7 @@ modal modalTrigger modalContent = do
let modalDynamic = isLeft modalContent
modalId <- newIdent
triggerId <- newIdent
$(widgetFile "widgets/modal")
$(widgetFile "widgets/modal/modal")
case modalContent of
Left route -> do
route' <- toTextUrl route

View File

@ -41,7 +41,7 @@ data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize
renderAForm :: Monad m => FormLayout -> FormRender m a
renderAForm formLayout aform fragment = do
(res, ($ []) -> fieldViews) <- aFormToForm aform
let widget = $(widgetFile "widgets/form")
let widget = $(widgetFile "widgets/form/form")
return (res, widget)
--------------------
@ -367,7 +367,7 @@ reorderField optList = Field{..}
isSel n = (==) (either (const $ map optionInternalValue olOptions) id val !! pred n) . optionInternalValue
nums = map (id &&& withNum theId) [1..length olOptions]
withNum t n = tshow n <> "." <> t
$(widgetFile "widgets/permutation")
$(widgetFile "widgets/permutation/permutation")
optionsFinite :: ( MonadHandler m
, Finite a

View File

@ -4,12 +4,11 @@
z-index: 1;
top: 0;
left: 0;
flex: 0 0 0;
flex-basis: var(--asidenav-width-lg, 20%);
min-height: calc(100% - var(--header-height));
transition: all .2s ease-out;
width: var(--asidenav-width-lg, 20%);
height: 100%;
flex: 0 0 0;
flex-basis: var(--asidenav-width-lg, 20%);
transition: all .2s ease-out;
&::before {
position: absolute;
@ -72,6 +71,14 @@
.asidenav {
color: var(--color-font);
min-height: calc(100% - var(--header-height));
height: 400px;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0;
}
}
.asidenav__box {
@ -183,9 +190,7 @@
/* LIST-ITEM */
.asidenav__list-item {
position: relative;
color: var(--color-font);
min-height: 50px;
display: flex;
flex-direction: column;
justify-content: flex-start;
@ -207,10 +212,8 @@
text-shadow: none;
}
.asidenav__nested-list {
transform: translateX(100%);
opacity: 1;
width: 200px;
.asidenav__nested-list-wrapper {
display: block;
}
}
}
@ -242,7 +245,7 @@
display: flex;
flex: 1;
align-items: center;
padding: 7px 10px;
padding: 8px 3px;
justify-content: flex-start;
color: var(--color-font);
width: 100%;
@ -258,17 +261,17 @@
}
/* hover sub-menus */
.asidenav__nested-list {
.asidenav__nested-list-wrapper {
position: absolute;
top: 0;
right: 0;
z-index: 10;
display: none;
color: var(--color-font);
transform: translateX(0);
opacity: 0;
width: 0;
overflow: hidden;
z-index: -1;
box-shadow: 0 0 13px rgba(0, 0, 0, 0.4);
background-color: var(--color-grey-light);
box-shadow: 1px 1px 1px 0px var(--color-grey);
}
.asidenav__nested-list {
min-width: 200px;
}
@media (max-width: 425px) {
@ -280,19 +283,16 @@
.asidenav__nested-list-item {
position: relative;
color: var(--color-lightwhite);
background-color: var(--color-dark);
&:hover {
background-color: var(--color-darker);
background-color: var(--color-lightwhite);
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
border-left: 20px solid white;
transition: all .2s ease;
color: var(--color-lightwhite);
color: var(--color-font);
}
}

View File

@ -1,7 +1,3 @@
.js-show-hide {
position: relative;
}
.js-show-hide__toggle {
position: relative;
cursor: pointer;

View File

@ -17,7 +17,7 @@
text-align: center;
padding: 0 13px;
margin: 0 2px;
background-color: #b3b7c1;
background-color: var(--color-dark);
color: white;
font-size: 16px;
text-transform: uppercase;
@ -35,5 +35,5 @@
.tab-opener.tab-visible {
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: #5F98C2;
border-bottom-color: var(--color-primary);
}

View File

@ -1,4 +1,4 @@
.js-tooltip {
.tooltip {
position: relative;
display: inline-block;
height: 1.5rem;
@ -67,7 +67,7 @@
@media (max-width: 768px) {
.js-tooltip {
.tooltip {
display: block;
margin-top: 10px;

View File

@ -3,6 +3,7 @@
window.utils = window.utils || {};
var ALERTS_CLASS = 'alerts';
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
@ -18,7 +19,11 @@
window.utils.alerts = function(alertsEl) {
if (alertsEl.classList.contains(JS_INITIALIZED_CLASS)) {
return;
return false;
}
if (!alertsEl || !alertsEl.classList.contains(ALERTS_CLASS)) {
throw new Error('utils.alerts has to be called with alerts element');
}
var togglerCheckRequested = false;

View File

@ -0,0 +1,59 @@
(function() {
'use strict';
window.utils = window.utils || {};
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
var FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
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) {
if (!asideEl) {
throw new Error('asideEl not defined');
}
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);
event.preventDefault();
}, true);
}
function initAsidenavSubmenus() {
var asidenavLinksWithSubmenus = Array.from(asideEl.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
.map(function(listItem) {
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
return { listItem, submenu };
}).filter(function(union) {
return union.submenu !== null;
});
asidenavLinksWithSubmenus.forEach(function(union) {
union.listItem.addEventListener('mouseover', createMouseoverHandler(union));
});
}
function createMouseoverHandler(union) {
return function mouseoverHanlder(event) {
var rectListItem = union.listItem.getBoundingClientRect();
var rectSubMenu = union.submenu.getBoundingClientRect();
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
if (window.innerHeight - rectListItem.top < rectSubMenu.height) {
union.submenu.style.top = (rectListItem.top + rectListItem.height - rectSubMenu.height) + 'px';
} else {
union.submenu.style.top = rectListItem.top + 'px';
}
};
}
initFavoritesButton();
initAsidenavSubmenus();
};
})();

View File

@ -0,0 +1,202 @@
(function collonadeClosure() {
'use strict';
window.utils = window.utils || {};
var HEADER_HEIGHT = 80;
var RESET_OPTIONS = [ 'scrollTo' ];
window.utils.asyncTable = function(wrapper, options) {
options = options || {};
var tableIdent = options.dbtIdent;
var shortCircuitHeader = options ? options.headerDBTableShortcircuit : null;
var ths = [];
var pageLinks = [];
var pagesizeForm;
var scrollTable;
function init() {
var table = wrapper.querySelector('#' + tableIdent);
if (!table) {
return;
}
scrollTable = wrapper.querySelector('.scrolltable');
// sortable table headers
ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) {
return { element: th };
});
// pagination links
var pagination = wrapper.querySelector('#' + tableIdent + '-pagination');
if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link };
});
}
// pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// take options into account
if (options && options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos;
}
setupListeners();
wrapper.classList.add('js-initialized');
}
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);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
}
}
function removeListeners() {
ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
}
}
function clickHandler(event, tableOptions) {
event.preventDefault();
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
updateTableFrom(url, tableOptions);
}
function getClickDestination(el) {
if (!el.querySelector('a')) {
return '';
}
return el.querySelector('a').getAttribute('href');
}
function changePagesizeHandler(event) {
var currentTableUrl = options.currentUrl || window.location.href;
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value);
url = new URL(getUrlWithResetPagenumber(url));
updateTableFrom(url);
}
function getUrlWithUpdatedPagesize(url, pagesize) {
if (url.indexOf('pagesize') >= 0) {
return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize);
} else if (url.indexOf('?') >= 0) {
return url += '&' + tableIdent + '-pagesize=' + pagesize;
}
return url += '?' + tableIdent + '-pagesize=' + pagesize;
}
function getUrlWithResetPagenumber(url) {
return url.replace(/-page=\d+/, '-page=0');
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url, tableOptions) {
tableOptions = tableOptions || {};
fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html',
[shortCircuitHeader]: tableIdent
}
}).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;
removeListeners();
updateWrapperContents(data, tableOptions);
}).catch(function(err) {
console.error(err);
});
}
function updateWrapperContents(newHtml, tableOptions) {
tableOptions = tableOptions || {};
wrapper.innerHTML = newHtml;
wrapper.classList.remove("js-initialized");
// setup the wrapper and its components to behave async again
window.utils.teardown('asyncTable');
window.utils.teardown('form');
// 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) {
window.utils.setup('form', form);
});
}
init();
};
})();

View File

@ -1,4 +0,0 @@
window.addEventListener('touchstart', function onFirstTouch() {
document.body.classList.add('touch-supported');
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);

View File

@ -3,13 +3,57 @@
window.utils = window.utils || {};
var JS_INITIALIZED = 'js-initialized';
var SUBMIT_BUTTON_SELECTOR = '[type="submit"]:not([formnovalidate])';
var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]';
function formValidator(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
window.utils.form = function(form, options) {
if (form.classList.contains(JS_INITIALIZED)) {
return false;
}
form.classList.add(JS_INITIALIZED);
// reactive buttons
var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR);
if (submitBtn) {
window.utils.setup('reactiveButton', form, { button: submitBtn });
}
// conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.setup('interactiveFieldset', form, { fieldSets });
// hide autoSubmit submit button
window.utils.setup('autoSubmit', form, options);
};
// registers input-listener for each element in <inputs> (array) and
// enables <button> if <validation> for these inputs returns true
window.utils.reactiveButton = function(form, button, validation) {
// enables <button> if <formValidator> for these inputs returns true
window.utils.reactiveButton = function(form, options) {
options = options || {};
var button = options.button;
var requireds = Array.from(form.querySelectorAll('[required]'));
if (!button) {
throw new Error('Please provide both a button to reactiveButton');
}
if (requireds.length == 0) {
return false;
}
if (typeof button.dataset.formnorequired !== 'undefined' && button.dataset.formnorequired !== null) {
button.addEventListener('click', function() {
form.submit();
@ -27,7 +71,7 @@
});
function updateButtonState() {
if (validation.call(null, requireds) === true) {
if (formValidator(requireds) === true) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', 'true');
@ -35,7 +79,14 @@
}
};
window.utils.interactiveFieldset = function(form, fieldSets) {
window.utils.interactiveFieldset = function(form, options) {
options = options || {};
var fieldSets = options.fieldSets;
if (!fieldSets) {
throw new Error('interactiveFieldset must be passed fieldSets via options');
}
var fields = fieldSets.map(function(fs) {
return {
fieldSet: fs,
@ -64,58 +115,11 @@
updateFields();
}
};
window.utils.autoSubmit = function(form, options) {
var button = form.querySelector(AUTOSUBMIT_BUTTON_SELECTOR);
if (button) {
button.classList.add('hidden');
}
};
})();
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
var forms = e.detail.scope.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
// auto reactiveButton submit-buttons with required fields
var submitBtns = Array.from(form.querySelectorAll('[type=submit]:not([formnovalidate])'));
submitBtns.forEach(function(submitBtn) {
window.utils.reactiveButton(form, submitBtn, validateForm);
});
// auto conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.interactiveFieldset(form, fieldSets);
});
function validateForm(inputs) {
var done = true;
inputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
});
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'autoSubmit')
return;
Array.from(e.detail.scope.querySelectorAll('[data-autosubmit]:not(.js-initialized)')).forEach(function(elem) {
if ((elem instanceof HTMLInputElement && elem.type == 'submit') || (elem instanceof HTMLButtonElement && elem.type == 'submit')) {
var ancestor = elem.closest('.form-group');
var target = ancestor || elem;
target.classList.add('hidden');
}
elem.classList.add('js-initialized');
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'autoSubmit' }, bubbles: true, cancelable: true }))
});

171
static/js/utils/inputs.js Normal file
View File

@ -0,0 +1,171 @@
(function() {
'use strict';
window.utils = window.utils || {};
var JS_INITIALIZED_CLASS = 'js-initialized';
function isNotInitialized(element) {
return !element.classList.contains(JS_INITIALIZED_CLASS);
}
window.utils.inputs = function(wrapper, options) {
// checkboxes / radios
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"], input[type="radio"]'));
checkboxes.filter(isNotInitialized).forEach(window.utils.checkboxRadio);
// file-uploads
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
fileUploads.filter(isNotInitialized).forEach(function(input) {
window.utils.fileUpload(input, options);
});
// file-checkboxes
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
fileCheckboxes.filter(isNotInitialized).forEach(function(inp) {
window.utils.fileCheckbox(inp);
inp.classList.add(JS_INITIALIZED_CLASS);
});
};
// (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');
}
input.classList.add(JS_INITIALIZED_CLASS);
function renderFileList(files) {
fileList.innerHTML = '';
Array.from(files).forEach(function(file, index) {
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() {
var list = document.createElement('ol');
list.classList.add(FILE_UPLOAD_INPUT_LIST_CLASS);
var unpackEl = input.parentElement.querySelector('.' + FILE_UPLOAD_INPUT_UNPACK_CHECKBOX_CLASS);
if (unpackEl) {
input.parentElement.insertBefore(list, unpackEl);
} else {
input.parentElement.appendChild(list);
}
return list;
}
function addFileLabel() {
var label = document.createElement('label');
label.classList.add(FILE_UPLOAD_INPUT_LABEL_CLASS);
label.setAttribute('for', input.id);
input.parentElement.insertBefore(label, input);
return label;
}
function resetFileLabel() {
if (isMulti) {
label.innerText = i18n.selectFiles;
} else {
label.innerText = i18n.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);
});
}
// to remove previously uploaded files
var FILE_UPLOAD_CONTAINER_CLASS = 'file-container';
var FILE_UPLOAD_CONTAINER_CHECKED_CLASS = 'file-container--checked';
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);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.matches('.' + FILE_UPLOAD_CONTAINER_CLASS)) {
break;
}
cont = cont.parentNode;
}
addListener(cont);
input.classList.add(JS_INITIALIZED_CLASS);
cont.classList.add(JS_INITIALIZED_CLASS);
}
setup();
}
// turns native checkboxes and radio buttons into custom ones
window.utils.checkboxRadio = function(input) {
var type = input.getAttribute('type');
if (!input.parentElement.classList.contains(type)) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label');
wrapperEl.classList.add(type);
labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
input.classList.add(JS_INITIALIZED_CLASS);
if (siblingEl) {
parentEl.insertBefore(wrapperEl, siblingEl);
} else {
parentEl.appendChild(wrapperEl);
}
}
}
})();

58
static/js/utils/setup.js Normal file
View File

@ -0,0 +1,58 @@
(function() {
'use strict';
window.utils = window.utils || {};
var registeredSetupListeners = {};
window.utils.setup = function(utilType, scope, options) {
if (!utilType || !scope) {
return;
}
options = options || {};
var listener = function(event) {
if (event.detail.targetUtil !== utilType) {
return false;
}
if (options.setupFunction) {
options.setupFunction(scope, options);
} else {
var util = window.utils[utilType];
if (!util) {
throw new Error('"' + utilType + '" is not a known js util');
}
util(scope, options);
}
};
if (registeredSetupListeners[utilType] && !options.singleton) {
registeredSetupListeners[utilType].push(listener);
} else {
window.utils.teardown(utilType);
registeredSetupListeners[utilType] = [ listener ];
}
document.addEventListener('setup', listener);
document.dispatchEvent(new CustomEvent('setup', {
detail: { targetUtil: utilType, module: 'none' },
bubbles: true,
cancelable: true,
}));
};
window.utils.teardown = function(utilType) {
if (registeredSetupListeners[utilType]) {
registeredSetupListeners[utilType].forEach(function(listener) {
document.removeEventListener('setup', listener);
});
delete registeredSetupListeners[utilType];
}
}
})();

View File

@ -0,0 +1,55 @@
(function() {
'use strict';
window.utils = window.utils || {};
var LOCAL_STORAGE_SHOW_HIDE = 'SHOW_HIDE';
/**
* div
* div.js-show-hide__toggle
* toggle here
* div
* content here
*/
window.utils.showHide = function(wrapper, options) {
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var newState = el.parentElement.classList.toggle('js-show-hide--collapsed');
updateLSState(el.dataset.shIndex || null, newState);
});
}
function updateLSState(index, state) {
if (!index) {
return false;
}
var lsData = fromLocalStorage();
lsData[index] = state;
window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(index) {
return fromLocalStorage()[index] || null;
}
function fromLocalStorage() {
return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SHOW_HIDE)) || {};
}
Array
.from(wrapper.querySelectorAll('.js-show-hide__toggle'))
.forEach(function(el) {
var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle(
'js-show-hide--collapsed',
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true'
);
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
addEventHandler(el);
});
};
})();

View File

@ -0,0 +1,48 @@
function setupDatepicker(wrapper) {
"use strict";
var config = {
dtLocal: {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
dateFormat: "Y-m-dTH:i",
time_24hr: true
},
d: {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true
},
t: {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true
}
};
Array.from(wrapper.querySelectorAll('input[type="date"]')).forEach(function(el) {
flatpickr(el, config.d);
});
Array.from(wrapper.querySelectorAll('input[type="time"]')).forEach(function(el) {
flatpickr(el, config.t);
});
Array.from(wrapper.querySelectorAll('input[type="datetime-local"]')).forEach(function(el) {
flatpickr(el, config.dtLocal);
});
}
document.addEventListener('DOMContentLoaded', function() {
var I18N = {
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
selectFile: 'Datei auswählen',
selectFiles: 'Datei(en) auswählen',
};
window.utils.setup('flatpickr', document.body, { setupFunction: setupDatepicker });
window.utils.setup('showHide', document.body);
window.utils.setup('inputs', document.body, { i18n: I18N });
});

View File

@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', function () {
var themeSelector = document.querySelector('#theme-select');
if (themeSelector) {
themeSelector.addEventListener('change', function() {
// get rid of old themes on body
var options = Array.from(themeSelector.options)
.forEach(function (option) {
document.body.classList.remove(optionToTheme(option));
});
// add newly selected theme
document.body.classList.add(optionToTheme(themeSelector.selectedOptions[0]));
});
}
function optionToTheme(option) {
return optionValue = 'theme--' + option.value;
}
});

View File

@ -3,7 +3,7 @@ $forall FileUploadInfo{..} <- fileInfos
<div .file-container :fuiChecked:.file-container--checked>
<label .file-container__label.btn for=#{fuiHtmlId}>#{fuiTitle}
<div .checkbox>
<input .file-container__checkbox.js-file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
<input .file-container__checkbox.file-checkbox id=#{fuiHtmlId} name=#{fieldName} :fuiChecked:checked value=#{toPathPiece fuiId} type="checkbox">
<label for=#{fuiHtmlId}>
$# new files
@ -15,6 +15,6 @@ $# new files
<div .file-input__unpack>
<label for=#{fieldId}_zip>ZIPs automatisch entpacken
<input type=checkbox id=#{fieldId}_zip name=#{fieldName} value=#{unpackZips}>
<div class="js-tooltip">
<div class="tooltip">
<div class="tooltip__handle">
<div class="tooltip__content">Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu.

View File

@ -1,17 +0,0 @@
document.addEventListener('setup', function (e) {
var themeSelector = e.detail.scope.querySelector('#theme-select');
themeSelector.addEventListener('change', function() {
// get rid of old themes on body
var options = Array.from(themeSelector.options)
.forEach(function (option) {
document.body.classList.remove(optionToTheme(option));
});
// add newly selected theme
document.body.classList.add(optionToTheme(themeSelector.selectedOptions[0]));
});
function optionToTheme(option) {
return optionValue = 'theme--' + option.value;
}
});

View File

@ -21,7 +21,7 @@ $maybe descr <- sheetDescription sheet
<dd .deflist__dd>_{sheetSubmissionMode sheet}
$case sheetSubmissionMode sheet
$of CorrectorSubmissions
<div .js-tooltip>
<div .tooltip>
<div .tooltip__handle>
<div .tooltip__content>_{MsgSheetCorrectorSubmissionsTip}
$of _

View File

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

View File

@ -1,43 +0,0 @@
document.addEventListener('setup', function(e) {
"use strict";
if (e.detail.module && e.detail.module !== 'datepicker')
return;
var config = {
dtLocal: {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i",
dateFormat: "Y-m-dTH:i",
time_24hr: true
},
d: {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true
},
t: {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true
}
};
Array.from(e.detail.scope.querySelectorAll('input[type="date"]')).forEach(function(el) {
flatpickr(el, config.d);
});
Array.from(e.detail.scope.querySelectorAll('input[type="time"]')).forEach(function(el) {
flatpickr(el, config.t);
});
Array.from(e.detail.scope.querySelectorAll('input[type="datetime-local"]')).forEach(function(el) {
flatpickr(el, config.dtLocal);
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'datepicker' }, bubbles: true, cancelable: true }));
});

View File

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

View File

@ -1,147 +0,0 @@
(function() {
'use strict';
window.utils = window.utils || {};
// allows for multiple file uploads with separate inputs
window.utils.initializeFileUpload = function(input) {
var isMulti = input.hasAttribute('multiple');
var fileList = isMulti ? addFileList() : null;
var label = addFileLabel();
function renderFileList(files) {
fileList.innerHTML = '';
Array.from(files).forEach(function(file, index) {
var fileDisplayEl = document.createElement('li');
fileDisplayEl.innerHTML = file.name;
fileList.appendChild(fileDisplayEl);
});
}
function updateLabel(files) {
if (files.length) {
if (isMulti) {
label.innerText = files.length + ' Dateien ausgwählt';
} else {
label.innerHTML = files[0].name;
}
} else {
resetFileLabel();
}
}
function addFileList() {
var list = document.createElement('ol');
list.classList.add('file-input__list');
var unpackEl = input.parentElement.querySelector('.file-input__unpack');
if (unpackEl) {
input.parentElement.insertBefore(list, unpackEl);
} else {
input.parentElement.appendChild(list);
}
return list;
}
function addFileLabel() {
var label = document.createElement('label');
label.classList.add('file-input__label');
label.setAttribute('for', input.id);
input.parentElement.insertBefore(label, input);
return label;
}
function resetFileLabel() {
// interpolate translated String here
label.innerText = 'Datei' + (isMulti ? 'en' : '') + ' auswählen';
}
// initial setup
resetFileLabel();
input.classList.add('file-input__input--hidden');
input.addEventListener('change', function() {
input.dispatchEvent(new Event('input'));
if (isMulti) {
renderFileList(input.files);
}
updateLabel(input.files);
});
}
// to remove previously uploaded files
window.utils.reactiveFileCheckbox = function(input) {
// adds eventlistener(s)
function addListener(container) {
input.addEventListener('change', function(event) {
container.classList.toggle('file-container--checked', this.checked);
});
}
// initial setup
function setup() {
var cont = input.parentNode;
while (cont !== document.body) {
if (cont.matches('.file-container')) {
break;
}
cont = cont.parentNode;
}
addListener(cont);
}
setup();
}
window.utils.initializeCheckboxRadio = function(input, type) {
if (!input.parentElement.classList.contains(type)) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label');
wrapperEl.classList.add(type);
labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
if (siblingEl) {
parentEl.insertBefore(wrapperEl, siblingEl);
} else {
parentEl.appendChild(wrapperEl);
}
}
}
})();
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'inputs')
return;
// initialize checkboxes
Array.from(e.detail.scope.querySelectorAll('input[type="checkbox"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeCheckboxRadio(inp, 'checkbox');
inp.classList.add("js-initialized");
});
// initialize radios
Array.from(e.detail.scope.querySelectorAll('input[type="radio"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeCheckboxRadio(inp, 'radio');
inp.classList.add("js-initialized");
});
// initialize file-upload-fields
Array.from(e.detail.scope.querySelectorAll('input[type="file"]:not(.js-initialized)')).forEach(function(inp) {
window.utils.initializeFileUpload(inp);
inp.classList.add("js-initialized");
});
// initialize file-checkbox-fields
Array.from(e.detail.scope.querySelectorAll('.js-file-checkbox:not(.js-initialized)')).forEach(function(inp) {
window.utils.reactiveFileCheckbox(inp);
inp.classList.add("js-initialized");
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'inputs' }, bubbles: true, cancelable: true }));
});

View File

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

View File

@ -1,58 +0,0 @@
/**
* div
* div.js-show-hide__toggle
* toggle here
* div
* content here
*/
document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
var LSNAME = 'SHOW_HIDE';
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var newState = el.parentElement.classList.toggle('js-show-hide--collapsed');
updateLSState(el.dataset.shIndex || null, newState);
});
}
function updateLSState(index, state) {
if (!index) {
return false;
}
var lsData = fromLocalStorage();
lsData[index] = state;
window.localStorage.setItem(LSNAME, JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(index) {
return fromLocalStorage()[index] || null;
}
function fromLocalStorage() {
return JSON.parse(window.localStorage.getItem(LSNAME)) || {};
}
Array
.from(e.detail.scope.querySelectorAll('.js-show-hide__toggle'))
.forEach(function(el) {
var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle(
'js-show-hide--collapsed',
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true'
);
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
}
});
addEventHandler(el);
});
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
});

View File

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

View File

@ -1,7 +0,0 @@
.tab-opener {
background-color: var(--color-dark);
&.tab-visible {
border-bottom-color: var(--color-primary);
}
}

View File

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

View File

@ -1,67 +0,0 @@
(function() {;
'use strict';
window.utils = window.utils || {};
window.utils.tooltip = function(tt) {
var handle = tt.querySelector('.tooltip__handle');
var content = tt.querySelector('.tooltip__content');
var left = false;
// initially set content to hidden
content.classList.add('hidden');
handle.addEventListener('mouseenter', function() {
left = false;
content.classList.toggle('to-left', handle.getBoundingClientRect().left + 300 > window.innerWidth);
content.classList.remove('hidden');
});
handle.addEventListener('mouseleave', function() {
left = true;
window.setTimeout(function() {
if (left) {
content.classList.add('hidden');
}
}, 250);
});
};
window.utils.tooltipFromAttribute = function(el) {
var tt = document.createElement('div');
var handle = document.createElement('div');
var content = document.createElement('div');
tt.classList.add('js-tooltip');
handle.classList.add('tooltip__handle');
content.classList.add('tooltip__content', 'hidden');
handle.innerText = '?';
content.innerHTML = el.getAttribute('data-tooltip');
tt.appendChild(handle);
tt.appendChild(content);
if (el.nextSiblingElement) {
el.parentElement.insertBefore(tt, el.nextSiblingElement);
} else {
el.parentElement.appendChild(tt);
}
};
})();
document.addEventListener('setup', function(e) {
// JS-TOOLTIPS NOT USED CURRENTLY.
// initialize tooltips set via `data-tooltip`
// Array.from(e.detail.scope.querySelectorAll('[data-tooltip]')).forEach(function(el) {
// window.utils.tooltipFromAttribute(el)
// });
// initialize tooltips
// Array.from(e.detail.scope.querySelectorAll('.js-tooltip')).forEach(function(tt) {
// window.utils.tooltip(tt);
// });
});

View File

@ -1,186 +1,10 @@
(function collonadeClosure() {
'use strict';
window.utils = window.utils || {};
window.utils.asyncTable = function(wrapper, options) {
var tableIdent = wrapper.dataset.dbtIdent;
var shortCircuitHeader = #{String (toPathPiece HeaderDBTableShortcircuit)};
var ths = [];
var pageLinks = [];
var pagesizeForm;
var scrollTable;
function init() {
var table = wrapper.querySelector('#' + tableIdent);
if (!table) {
return;
}
scrollTable = wrapper.querySelector('.scrolltable');
// sortable table headers
ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) {
return { element: th };
});
// pagination links
var pagination = wrapper.querySelector('#' + tableIdent + '-pagination');
if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link };
});
}
// pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// take options into account
if (options && options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos;
}
setupListeners();
wrapper.classList.add('js-initialized');
}
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 wrapperBoundingRect = wrapper.getBoundingClientRect();
var options = {};
if (wrapperBoundingRect.top < 160) {
options.scrollTo = {
top: (wrapper.offsetTop || 0) - 60,
left: wrapper.offsetLeft || 0,
behavior: 'smooth',
};
}
boundClickHandler(event, options);
}
link.element.addEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
}
}
function removeListeners() {
ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]')
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
}
}
function clickHandler(event, options) {
event.preventDefault();
var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this));
updateTableFrom(url, options);
}
function getClickDestination(el) {
if (!el.querySelector('a')) {
return '';
}
return el.querySelector('a').getAttribute('href');
}
function changePagesizeHandler(event) {
var currentTableUrl = wrapper.dataset.currentUrl || window.location.href;
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value);
url = getUrlWithResetPagenumber(url);
updateTableFrom(url);
}
function getUrlWithUpdatedPagesize(url, pagesize) {
if (url.indexOf('pagesize') >= 0) {
return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize);
} else if (url.indexOf('?') >= 0) {
return url += '&' + tableIdent + '-pagesize=' + pagesize;
}
return url += '?' + tableIdent + '-pagesize=' + pagesize;
}
function getUrlWithResetPagenumber(url) {
return url.replace(/-page=\d+/, '-page=0');
}
function updateWrapperContents(newHtml, options) {
wrapper.innerHTML = newHtml;
wrapper.classList.remove("js-initialized");
// setup the wrapper and its components to behave async again
window.utils.asyncTable(wrapper, options);
// make sure to hide any new submit buttons
document.dispatchEvent(new CustomEvent('setup', {
detail: {
scope: wrapper,
module: 'autoSubmit'
}
}));
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url, options) {
fetch(url, {
credentials: 'same-origin',
headers: {
'Accept': 'text/html',
[shortCircuitHeader]: tableIdent
}
}).then(function(response) {
if (!response.ok) {
throw new Error('Looks like there was a problem fetching ' + url + '. Status Code: ' + response.status);
}
return response.text();
}).then(function(data) {
wrapper.dataset.currentUrl = url;
removeListeners();
updateWrapperContents(data, options);
}).catch(function(err) {
console.error(err);
});
}
init();
};
})();
document.addEventListener('DOMContentLoaded', function() {
var dbtIdent = #{String $ dbtIdent};
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)';
var wrapper = document.querySelector(selector);
if (wrapper) {
wrapper.dataset.dbtIdent = dbtIdent;
window.utils.asyncTable(wrapper);
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
}
});

View File

@ -1,8 +1,8 @@
<div #alerts .alerts>
<div #alerts-1 .alerts> <!-- make wIdent work here instead of '#alerts-1' -->
<div .alerts__toggler>
$forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}">
<div .alert.alert-#{status2}>
<div .alert__closer>
<div .alert__icon>
<div .alert__content>

View File

@ -1,18 +1,4 @@
document.addEventListener('setup', function(e) {
if (!e.detail.module || e.detail.module !== 'alerts') {
return;
}
// setup alerts
if (e.detail.scope.classList.contains('alerts')) {
window.utils.alerts(e.detail.scope);
} else {
var alertsEl = e.detail.scope.querySelector('.alerts');
if (alertsEl)
window.utils.alerts(alertsEl);
}
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'alerts' }, bubbles: true, cancelable: true }))
var alertsElement = document.querySelector('#' + 'alerts-1');
window.utils.setup('alerts', alertsElement);
});

View File

@ -1,23 +0,0 @@
(function() {
'use strict';
window.utils = window.utils || {};
window.utils.aside = function(asideEl) {
function init() {
var favoritesBtn = document.querySelector('.navbar__list-item--favorite');
favoritesBtn.addEventListener('click', function(event) {
favoritesBtn.classList.toggle('navbar__list-item--active');
asideEl.classList.toggle('main__aside--expanded');
event.preventDefault();
}, true);
}
init();
};
})();
document.addEventListener('DOMContentLoaded', function() {
var asidenavEl = document.querySelector('.main__aside');
window.utils.aside(asidenavEl);
});

View File

@ -16,10 +16,11 @@ $newline never
<a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{courseShorthand}
<div .asidenav__link-label>#{courseName}
<ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$case menuItemType
$of PageActionPrime
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _
<div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless>
$forall (MenuItem{menuItemType, menuItemLabel}, route) <- pageActions
$case menuItemType
$of PageActionPrime
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>_{menuItemLabel}
$of _

View File

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

View File

@ -13,7 +13,7 @@ $case formLayout
<label .form-group__label for=#{fvId view}>
#{fvLabel view}
$maybe tooltip <- fvTooltip view
<div .js-tooltip>
<div .tooltip>
<div .tooltip__handle>
<div .tooltip__content>^{tooltip}
<div .form-group__input>

View File

@ -0,0 +1,5 @@
document.addEventListener('DOMContentLoaded', function() {
Array.from(document.querySelectorAll('form')).forEach(function(form) {
window.utils.setup('form', form);
});
});

View File

@ -51,4 +51,4 @@ $#
\ (_{title $ getSum $ summary ^. _numSheetsPoints})
$# Kurze Alternative mit Hashtag-Symbol für "Anzahl"
$# \ (##{display $ summary ^. _numSheetsPoints})
<td .table__td>#{display $ summary ^. _numSheets}
<td .table__td>#{display $ summary ^. _numSheets}