diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs index e7033f3d8..cdb8db1e8 100644 --- a/src/Auth/Dummy.hs +++ b/src/Auth/Dummy.hs @@ -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") diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index cd2a9a037..861c03620 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -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 diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs index 68df34703..74c4e67a3 100644 --- a/src/Auth/PWHash.hs +++ b/src/Auth/PWHash.hs @@ -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") diff --git a/src/Foundation.hs b/src/Foundation.hs index 9653564e0..a5a8e1f56 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -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") diff --git a/src/Handler/Corrections.hs b/src/Handler/Corrections.hs index 1fece3778..29f31b107 100644 --- a/src/Handler/Corrections.hs +++ b/src/Handler/Corrections.hs @@ -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 diff --git a/src/Handler/Course.hs b/src/Handler/Course.hs index 080cb2d22..18d507e0f 100644 --- a/src/Handler/Course.hs +++ b/src/Handler/Course.hs @@ -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 diff --git a/src/Handler/Profile.hs b/src/Handler/Profile.hs index 1e2547db9..33736976e 100644 --- a/src/Handler/Profile.hs +++ b/src/Handler/Profile.hs @@ -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") diff --git a/src/Handler/Sheet.hs b/src/Handler/Sheet.hs index 615fa91e1..ebd365521 100644 --- a/src/Handler/Sheet.hs +++ b/src/Handler/Sheet.hs @@ -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") diff --git a/src/Handler/Users.hs b/src/Handler/Users.hs index 7196055a9..bd0feefe1 100644 --- a/src/Handler/Users.hs +++ b/src/Handler/Users.hs @@ -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) -> diff --git a/src/Handler/Utils/Delete.hs b/src/Handler/Utils/Delete.hs index c35d2a14f..400ef2d72 100644 --- a/src/Handler/Utils/Delete.hs +++ b/src/Handler/Utils/Delete.hs @@ -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") diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index f7a3579cd..0e50c8820 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -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 diff --git a/src/Handler/Utils/SheetType.hs b/src/Handler/Utils/SheetType.hs index 43b98a92f..9e69815c9 100644 --- a/src/Handler/Utils/SheetType.hs +++ b/src/Handler/Utils/SheetType.hs @@ -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") diff --git a/src/Handler/Utils/Table/Pagination.hs b/src/Handler/Utils/Table/Pagination.hs index 368758bea..b5b36fdcf 100644 --- a/src/Handler/Utils/Table/Pagination.hs +++ b/src/Handler/Utils/Table/Pagination.hs @@ -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| -
+
_{msg} |] diff --git a/src/Handler/Utils/Templates.hs b/src/Handler/Utils/Templates.hs index ce0d70bc0..ae8673af1 100644 --- a/src/Handler/Utils/Templates.hs +++ b/src/Handler/Utils/Templates.hs @@ -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 diff --git a/src/Utils/Form.hs b/src/Utils/Form.hs index c4fe43f80..787a2e4b5 100644 --- a/src/Utils/Form.hs +++ b/src/Utils/Form.hs @@ -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 diff --git a/templates/widgets/asidenav.lucius b/static/css/utils/asidenav.scss similarity index 92% rename from templates/widgets/asidenav.lucius rename to static/css/utils/asidenav.scss index 9123c407d..51fe73163 100644 --- a/templates/widgets/asidenav.lucius +++ b/static/css/utils/asidenav.scss @@ -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); } } diff --git a/templates/widgets/form.lucius b/static/css/utils/form.scss similarity index 100% rename from templates/widgets/form.lucius rename to static/css/utils/form.scss diff --git a/templates/standalone/inputs.lucius b/static/css/utils/inputs.scss similarity index 100% rename from templates/standalone/inputs.lucius rename to static/css/utils/inputs.scss diff --git a/templates/standalone/showHide.lucius b/static/css/utils/showHide.scss similarity index 94% rename from templates/standalone/showHide.lucius rename to static/css/utils/showHide.scss index d1413c0fb..64dfe367c 100644 --- a/templates/standalone/showHide.lucius +++ b/static/css/utils/showHide.scss @@ -1,7 +1,3 @@ -.js-show-hide { - position: relative; -} - .js-show-hide__toggle { position: relative; cursor: pointer; diff --git a/static/css/utils/tabber.css b/static/css/utils/tabber.scss similarity index 89% rename from static/css/utils/tabber.css rename to static/css/utils/tabber.scss index 6f823b410..d135a5f27 100644 --- a/static/css/utils/tabber.css +++ b/static/css/utils/tabber.scss @@ -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); } diff --git a/templates/standalone/tooltip.lucius b/static/css/utils/tooltip.scss similarity index 97% rename from templates/standalone/tooltip.lucius rename to static/css/utils/tooltip.scss index 0a2154768..7e538e46a 100644 --- a/templates/standalone/tooltip.lucius +++ b/static/css/utils/tooltip.scss @@ -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; diff --git a/static/js/utils/alerts.js b/static/js/utils/alerts.js index eec2fb73a..d52a47376 100644 --- a/static/js/utils/alerts.js +++ b/static/js/utils/alerts.js @@ -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; diff --git a/static/js/utils/asidenav.js b/static/js/utils/asidenav.js new file mode 100644 index 000000000..154232109 --- /dev/null +++ b/static/js/utils/asidenav.js @@ -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(); + }; +})(); diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js new file mode 100644 index 000000000..fe164d3b2 --- /dev/null +++ b/static/js/utils/asyncTable.js @@ -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(); + }; +})(); diff --git a/static/js/utils/featureChecker.js b/static/js/utils/featureChecker.js deleted file mode 100644 index ad8e26303..000000000 --- a/static/js/utils/featureChecker.js +++ /dev/null @@ -1,4 +0,0 @@ -window.addEventListener('touchstart', function onFirstTouch() { - document.body.classList.add('touch-supported'); - window.removeEventListener('touchstart', onFirstTouch, false); -}, false); diff --git a/templates/widgets/form.julius b/static/js/utils/form.js similarity index 51% rename from templates/widgets/form.julius rename to static/js/utils/form.js index e318ca3f3..30ba76c96 100644 --- a/templates/widgets/form.julius +++ b/static/js/utils/form.js @@ -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 (array) and - // enables