Merge branch 'master' into 'live'

Master

Closes #230

See merge request !105
This commit is contained in:
Gregor Kleen 2018-11-22 21:46:45 +01:00
commit c466a6fa05
24 changed files with 417 additions and 132 deletions

View File

@ -108,6 +108,7 @@ dependencies:
- mmorph - mmorph
- clientsession - clientsession
- monad-memo - monad-memo
- xss-sanitize
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -16,8 +16,6 @@ import Auth.PWHash
import Auth.Dummy import Auth.Dummy
import Jobs.Types import Jobs.Types
import Handler.Utils.Templates (siteModalId, modalParameter)
import qualified Network.Wai as W (pathInfo) import qualified Network.Wai as W (pathInfo)
import Yesod.Default.Util (addStaticContentExternal) import Yesod.Default.Util (addStaticContentExternal)
@ -768,7 +766,7 @@ siteLayout headingOverride widget = do
master <- getYesod master <- getYesod
let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master
isModal <- isJust <$> siteModalId isModal <- hasCustomHeader HeaderIsModal
mcurrentRoute <- getCurrentRoute mcurrentRoute <- getCurrentRoute
@ -806,7 +804,7 @@ siteLayout headingOverride widget = do
return (c, courseRoute, items') return (c, courseRoute, items')
mmsgs <- if mmsgs <- if
| isModal -> return [] | isModal -> getMessages
| otherwise -> do | otherwise -> do
applySystemMessages applySystemMessages
authTagPivots <- fromMaybe Set.empty <$> getSessionJson SessionInactiveAuthTags authTagPivots <- fromMaybe Set.empty <$> getSessionJson SessionInactiveAuthTags
@ -987,7 +985,7 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
{ menuItemType = NavbarRight { menuItemType = NavbarRight
, menuItemLabel = MsgMenuHelp , menuItemLabel = MsgMenuHelp
, menuItemIcon = Just "question" , menuItemIcon = Just "question"
, menuItemRoute = SomeRoute (HelpR, catMaybes [("site", ) . toPathPiece <$> mCurrentRoute]) , menuItemRoute = SomeRoute (HelpR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mCurrentRoute])
, menuItemModal = True , menuItemModal = True
, menuItemAccessCallback' = return True , menuItemAccessCallback' = return True
} }

View File

@ -257,31 +257,24 @@ getHelpR, postHelpR :: Handler Html
getHelpR = postHelpR getHelpR = postHelpR
postHelpR = do postHelpR = do
mUid <- maybeAuthId mUid <- maybeAuthId
mReferer <- flip formResultMaybe return <=< runInputGetResult $ iopt routeField "site" mReferer <- flip formResultMaybe return <=< runInputGetResult $ iopt routeField (toPathPiece GetReferer)
((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid ((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid
case res of formResultModal res HelpR $ \HelpForm{..} -> do
FormSuccess HelpForm{..} -> do now <- liftIO getCurrentTime
now <- liftIO getCurrentTime hfReferer' <- traverse toTextUrl hfReferer
hfReferer' <- traverse toTextUrl hfReferer queueJob' JobHelpRequest
queueJob' JobHelpRequest { jSender = hfUserId
{ jSender = hfUserId , jHelpRequest = hfRequest
, jHelpRequest = hfRequest , jRequestTime = now
, jRequestTime = now , jReferer = hfReferer'
, jReferer = hfReferer' }
} tell . pure =<< messageI Success MsgHelpSent
-- redirect $ HelpR
addMessageI Success MsgHelpSent
return ()
{-selectRep $ do
provideJson ()
provideRep (redirect $ HelpR :: Handler Html) -}
FormMissing -> return ()
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
defaultLayout $ do defaultLayout $ do
setTitle "Hilfe" -- TODO: International setTitle "Hilfe" -- TODO: International
isModal <- hasCustomHeader HeaderIsModal
$(widgetFile "help") $(widgetFile "help")

View File

@ -1,6 +1,7 @@
module Handler.Utils.Form module Handler.Utils.Form
( module Handler.Utils.Form ( module Handler.Utils.Form
, module Utils.Form , module Utils.Form
, MonadWriter(..)
) where ) where
import Utils.Form import Utils.Form
@ -35,6 +36,7 @@ import qualified Data.Set as Set
import Data.Map (Map, (!)) import Data.Map (Map, (!))
import qualified Data.Map as Map import qualified Data.Map as Map
import Control.Monad.Trans.Writer (execWriterT, WriterT)
import Control.Monad.Writer.Class import Control.Monad.Writer.Class
import Data.Scientific (Scientific) import Data.Scientific (Scientific)
@ -587,5 +589,16 @@ multiActionA FieldSettings{..} acts defAction = formToAForm $ do
} }
]) ])
formResultModal :: (MonadHandler m, RedirectUrl (HandlerSite m) route) => FormResult a -> route -> (a -> WriterT [Message] m ()) -> m ()
formResultModal res finalDest handler = maybeT_ $ do
messages <- case res of
FormMissing -> mzero
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs >> mzero
FormSuccess val -> lift . execWriterT $ handler val
isModal <- hasCustomHeader HeaderIsModal
if
| isModal -> sendResponse $ toJSON messages
| otherwise -> do
forM_ messages $ \Message{..} -> addMessage messageClass messageContent
redirect finalDest

View File

@ -7,12 +7,6 @@ import Import.NoFoundation
lipsum :: WidgetT site IO () lipsum :: WidgetT site IO ()
lipsum = $(widgetFile "widgets/lipsum") lipsum = $(widgetFile "widgets/lipsum")
modalParameter :: Text
modalParameter = "_modal"
siteModalId :: MonadHandler m => m (Maybe Text)
siteModalId = lookupGetParam modalParameter
modal :: WidgetT site IO () -> Either (Route site) (WidgetT site IO ()) -> WidgetT site IO () modal :: WidgetT site IO () -> Either (Route site) (WidgetT site IO ()) -> WidgetT site IO ()
modal modalTrigger modalContent = do modal modalTrigger modalContent = do
let modalDynamic = isLeft modalContent let modalDynamic = isLeft modalContent

View File

@ -52,6 +52,8 @@ import Text.Shakespeare.Text (st)
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import Data.Universe
----------- -----------
@ -476,3 +478,37 @@ tellSessionJson key val = modifySessionJson key $ Just . (`mappend` val) . fromM
getSessionJson :: (PathPiece k, FromJSON v, MonadHandler m) => k -> m (Maybe v) getSessionJson :: (PathPiece k, FromJSON v, MonadHandler m) => k -> m (Maybe v)
-- ^ `lookupSessionJson` followed by `deleteSession` -- ^ `lookupSessionJson` followed by `deleteSession`
getSessionJson key = lookupSessionJson key <* deleteSession (toPathPiece key) getSessionJson key = lookupSessionJson key <* deleteSession (toPathPiece key)
--------------------
-- GET Parameters --
--------------------
data GlobalGetParam = GetReferer
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe GlobalGetParam
instance Finite GlobalGetParam
nullaryPathPiece ''GlobalGetParam (camelToPathPiece' 1)
lookupGlobalGetParam :: (MonadHandler m, PathPiece result) => GlobalGetParam -> m (Maybe result)
lookupGlobalGetParam ident = (>>= fromPathPiece) <$> lookupGetParam (toPathPiece ident)
hasGlobalGetParam :: MonadHandler m => GlobalGetParam -> m Bool
hasGlobalGetParam ident = isJust <$> lookupGetParam (toPathPiece ident)
---------------------------------
-- Custom HTTP Request-Headers --
---------------------------------
data CustomHeader = HeaderIsModal
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Universe CustomHeader
instance Finite CustomHeader
nullaryPathPiece ''CustomHeader (intercalate "-" . drop 1 . splitCamel)
lookupCustomHeader :: (MonadHandler m, PathPiece result) => CustomHeader -> m (Maybe result)
lookupCustomHeader ident = (>>= fromPathPiece . decodeUtf8) <$> lookupHeader (CI.mk . encodeUtf8 $ toPathPiece ident)
hasCustomHeader :: MonadHandler m => CustomHeader -> m Bool
hasCustomHeader ident = isJust <$> lookupHeader (CI.mk . encodeUtf8 $ toPathPiece ident)

View File

@ -1,6 +1,9 @@
module Utils.Message module Utils.Message
( MessageClass(..) ( MessageClass(..)
, UnknownMessageClass(..)
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, Message(..)
, messageI, messageIHamlet, messageFile, messageWidget
) where ) where
@ -17,6 +20,10 @@ import Text.Hamlet
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift) import Language.Haskell.TH.Syntax (Lift)
import Text.Blaze (preEscapedText)
import Text.Blaze.Html.Renderer.Text (renderHtml)
import Text.HTML.SanitizeXSS (sanitizeBalance)
data MessageClass = Error | Warning | Info | Success data MessageClass = Error | Warning | Info | Success
deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift) deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift)
@ -31,13 +38,47 @@ deriveJSON defaultOptions
nullaryPathPiece ''MessageClass camelToPathPiece nullaryPathPiece ''MessageClass camelToPathPiece
derivePersistField "MessageClass" derivePersistField "MessageClass"
newtype UnknownMessageClass = UnknownMessageClass Text
deriving (Eq, Ord, Read, Show, Generic, Typeable)
addMessage :: MonadHandler m => MessageClass-> Html -> m () instance Exception UnknownMessageClass
data Message = Message
{ messageClass :: MessageClass
, messageContent :: Html
}
instance Eq Message where
a == b = ((==) `on` messageClass) a b && ((==) `on` renderHtml . messageContent) a b
instance Ord Message where
a `compare` b = (compare `on` messageClass) a b `mappend` (compare `on` renderHtml . messageContent) a b
instance ToJSON Message where
toJSON Message{..} = object
[ "class" .= messageClass
, "content" .= renderHtml messageContent
]
instance FromJSON Message where
parseJSON = withObject "Message" $ \o -> do
messageClass <- o .: "class"
messageContent <- preEscapedText . sanitizeBalance <$> o .: "content"
return Message{..}
addMessage :: MonadHandler m => MessageClass -> Html -> m ()
addMessage mc = ClassyPrelude.Yesod.addMessage (toPathPiece mc) addMessage mc = ClassyPrelude.Yesod.addMessage (toPathPiece mc)
addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m () addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m ()
addMessageI mc = ClassyPrelude.Yesod.addMessageI (toPathPiece mc) addMessageI mc = ClassyPrelude.Yesod.addMessageI (toPathPiece mc)
messageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m Message
messageI messageClass msg = do
messageContent <- toHtml . ($ msg) <$> getMessageRender
return Message{..}
addMessageIHamlet :: ( MonadHandler m addMessageIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg , RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site , HandlerSite m ~ site
@ -46,9 +87,20 @@ addMessageIHamlet mc iHamlet = do
mr <- getMessageRender mr <- getMessageRender
ClassyPrelude.Yesod.addMessage (toPathPiece mc) =<< withUrlRenderer (iHamlet $ toHtml . mr) ClassyPrelude.Yesod.addMessage (toPathPiece mc) =<< withUrlRenderer (iHamlet $ toHtml . mr)
messageIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site
) => MessageClass -> HtmlUrlI18n msg (Route site) -> m Message
messageIHamlet mc iHamlet = do
mr <- getMessageRender
Message mc <$> withUrlRenderer (iHamlet $ toHtml . mr)
addMessageFile :: MessageClass -> FilePath -> ExpQ addMessageFile :: MessageClass -> FilePath -> ExpQ
addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|] addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|]
messageFile :: MessageClass -> FilePath -> ExpQ
messageFile mc tPath = [e|messageIHamlet mc $(ihamletFile tPath)|]
addMessageWidget :: forall m site. addMessageWidget :: forall m site.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ site , HandlerSite m ~ site
@ -58,3 +110,12 @@ addMessageWidget :: forall m site.
addMessageWidget mc wgt = do addMessageWidget mc wgt = do
PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt
addMessageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site)) addMessageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site))
messageWidget :: forall m site.
( MonadHandler m
, HandlerSite m ~ site
, Yesod site
) => MessageClass -> WidgetT site IO () -> m Message
messageWidget mc wgt = do
PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt
messageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site))

View File

@ -17,10 +17,4 @@ $newline never
<script> <script>
document.body.classList.remove('no-js'); document.body.classList.remove('no-js');
document.addEventListener('DOMContentLoaded', function() {
console.log('Setup body');
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body }, bubbles: true, cancelable: true }));
});
^{pageBody pc} ^{pageBody pc}

View File

@ -27,13 +27,13 @@ $if not isModal
<!-- prime page actions --> <!-- prime page actions -->
^{pageactionprime} ^{pageactionprime}
<!-- alerts --> <!-- alerts -->
<div .alerts> <div #alerts .alerts>
$forall (status, msg) <- mmsgs $forall (status, msg) <- mmsgs
$with status2 <- bool status "info" (status == "") $with status2 <- bool status "info" (status == "")
<div class="alert alert-#{status2}"> <div class="alert alert-#{status2}">
<div .alert__content> <div .alert__content>
#{msg} #{msg}
<!-- actual content --> <!-- actual content -->
^{widget} ^{widget}

View File

@ -1,4 +1,4 @@
_{MsgHelpIntroduction} _{MsgHelpIntroduction}
<form method=post action=@{HelpR} enctype=#{formEnctype}> <form method=post action=@{HelpR} enctype=#{formEnctype} :isModal:data-ajax-submit>
^{formWidget} ^{formWidget}

View File

@ -5,24 +5,24 @@
var ALERT_INVISIBLE_CLASS = 'alert--invisible'; var ALERT_INVISIBLE_CLASS = 'alert--invisible';
var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible'; var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible';
var alertsShowingToggler = false;
window.utils.alerts = function(alertsEl) { window.utils.alerts = function(alertsEl) {
var alerts = Array.from(alertsEl.querySelectorAll('.alert')); var toggler = alertsEl.querySelector('.alerts__toggler');
var toggler;
var showingToggler = false;
function makeToggler() { function makeToggler() {
toggler = document.createElement('DIV'); toggler = document.createElement('DIV');
toggler.classList.add('alerts__toggler'); toggler.classList.add('alerts__toggler');
toggler.addEventListener('click', function() { toggler.addEventListener('click', function() {
alerts.forEach(function(alert) { Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) {
alert.classList.remove(ALERT_INVISIBLE_CLASS); alert.classList.remove(ALERT_INVISIBLE_CLASS);
toggler.classList.remove(TOGGLER_INVISIBLE_CLASS); toggler.classList.remove(TOGGLER_INVISIBLE_CLASS);
}); });
checkToggler(); checkToggler();
}); });
alertsEl.appendChild(toggler); alertsEl.appendChild(toggler);
alertsEl.classList.add('js-initialized');
} }
function makeAlert(alertEl) { function makeAlert(alertEl) {
@ -47,6 +47,8 @@
closeAlert(alertEl); closeAlert(alertEl);
}, autoDecay * 1000); }, autoDecay * 1000);
} }
alertEl.classList.add('js-initialized');
} }
function closeAlert(alertEl) { function closeAlert(alertEl) {
@ -56,30 +58,42 @@
function checkToggler() { function checkToggler() {
var hidden = true; var hidden = true;
alerts.forEach(function(alert) { Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) {
if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) { if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) {
hidden = false; hidden = false;
} }
}); });
if (!showingToggler) { if (!alertsShowingToggler) {
showingToggler = true; alertsShowingToggler = true;
window.setTimeout(function() { window.setTimeout(function() {
toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden); toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden);
showingToggler = false; alertsShowingToggler = false;
}, 120); }, 120);
} }
} }
makeToggler(); if (!alertsEl.classList.contains('js-initialized') || !toggler)
alerts.map(makeAlert); makeToggler();
Array.from(alertsEl.querySelectorAll('.alert:not(.js-initialized)')).map(makeAlert);
} }
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'alerts')
return;
// setup alerts // setup alerts
var alertsEl = e.detail.scope.querySelector('.alerts'); if (e.detail.scope.classList.contains('alerts')) {
if (alertsEl) window.utils.alerts(e.detail.scope);
window.utils.alerts(alertsEl); } 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 }))
}); });

View File

@ -1,5 +1,8 @@
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
"use strict"; "use strict";
if (e.detail.module && e.detail.module !== 'datepicker')
return;
var config = { var config = {
dtLocal: { dtLocal: {
@ -34,3 +37,7 @@ document.addEventListener('setup', function(e) {
flatpickr(el, config.dtLocal); 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

@ -114,6 +114,8 @@
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'inputs')
return;
// initialize checkboxes // initialize checkboxes
Array.from(e.detail.scope.querySelectorAll('input[type="checkbox"]')).forEach(function(inp) { Array.from(e.detail.scope.querySelectorAll('input[type="checkbox"]')).forEach(function(inp) {
@ -135,3 +137,7 @@ document.addEventListener('setup', function(e) {
window.utils.reactiveFileCheckbox(inp); window.utils.reactiveFileCheckbox(inp);
}); });
}); });
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'inputs' }, bubbles: true, cancelable: true }));
});

View File

@ -87,6 +87,25 @@ input[type*="time"] {
font-family: var(--font-base); font-family: var(--font-base);
line-height: 1.5; line-height: 1.5;
padding: 4px 13px; padding: 4px 13px;
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
&[disabled] {
background-color: #f5f5f5;
color: #7a7a7a;
box-shadow: none;
border-color: #dbdbdb;
}
&[readonly] {
background-color: #f5f5f5;
box-shadow: none;
border-color: #dbdbdb;
}
} }
input[type="number"] { input[type="number"] {
@ -100,19 +119,6 @@ input[type*="time"],
width: 250px; width: 250px;
} }
input[type="text"]:focus,
input[type="password"]:focus,
input[type="url"]:focus,
input[type="number"]:focus,
input[type="email"]:focus {
/* border-bottom-color: var(--color-light);
background-color: transparent;
box-shadow: 0 0 13px var(--color-lighter); */
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
/* BUTTON STYLE SEE default-layout.lucius */ /* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */ /* TEXTAREAS */
@ -132,12 +138,25 @@ textarea {
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
vertical-align: top; vertical-align: top;
}
textarea:focus { &:focus {
border-color: #3273dc; border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25); box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0; outline: 0;
}
&[disabled] {
background-color: #f3f3f3;
color: #7a7a7a;
box-shadow: none;
border-color: #dbdbdb;
}
&[readonly] {
background-color: #f5f5f5;
box-shadow: none;
border-color: #dbdbdb;
}
} }
/* OPTIONS */ /* OPTIONS */

View File

@ -10,6 +10,10 @@
// var origParent = modal.parentNode; // var origParent = modal.parentNode;
function open(event) { function open(event) {
if (!modal.classList.contains('js-modal-initialized')) {
return;
}
// disable modals for narrow screens // disable modals for narrow screens
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
@ -17,7 +21,7 @@
modal.classList.add('modal--open'); modal.classList.add('modal--open');
overlay.classList.add('modal__overlay'); overlay.classList.add('modal__overlay');
// document.body.insertBefore(modal, null); // document.body.insertBefore(modal, null);
document.body.insertBefore(overlay, modal); document.body.insertBefore(overlay, modal);
overlay.classList.add('modal__overlay--open'); overlay.classList.add('modal__overlay--open');
if ('closeable' in modal.dataset) { if ('closeable' in modal.dataset) {
@ -37,58 +41,39 @@
} }
function close(event) { function close(event) {
if (typeof event === 'undefined' || event.target === closer || event.target === overlay) { overlay.remove();
overlay.remove(); // origParent.insertBefore(modal, null);
// origParent.insertBefore(modal, null); modal.classList.remove('modal--open');
modal.classList.remove('modal--open'); closer.removeEventListener('click', close, false);
closer.removeEventListener('click', close, false);
}
}; };
function setup() { function setup() {
document.body.insertBefore(modal, null); document.body.insertBefore(modal, null);
// every modal can be openend via document-wide event, see openOnEvent // every modal can be openend via document-wide event, see openOnEvent
document.addEventListener('modal-open', openOnEvent, false); document.addEventListener('modal-open', openOnEvent, false);
// if modal has trigger assigned to it open modal on click
if (trigger) {
trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false);
}
if ('dynamic' in modal.dataset) { if ('dynamic' in modal.dataset) {
var dynamicContentURL = trigger.getAttribute('href'); function fetchModal(url, init) {
function responseHtml(body) {
var i = dynamicContentURL.indexOf('?');
if (i === -1) {
dynamicContentURL = dynamicContentURL + "?" + #{String modalParameter};
} else {
dynamicContentURL = dynamicContentURL.slice(0,i) + "?" + #{String modalParameter} + "&" + dynamicContentURL.slice(i + 1);
}
console.log(dynamicContentURL);
if (dynamicContentURL.length > 0) {
fetch(dynamicContentURL, {
credentials: 'same-origin',
}).then(function(response) {
return response.text();
}).then(function(body) {
var modalContent = document.createElement('div'); var modalContent = document.createElement('div');
modalContent.classList.add('modal__content');
modalContent.innerHTML = body; modalContent.innerHTML = body;
var contentBody = modalContent.querySelector('.main__content-body'); var contentBody = modalContent.querySelector('.main__content-body');
var scriptTags = []; var scriptTags = [];
if (contentBody) { if (contentBody) {
modalContent.querySelectorAll('script').forEach(function(scriptTag) { modalContent.querySelectorAll('script').forEach(function(scriptTag) {
if (Array.from(document.body.querySelectorAll('script')).some(function(haystack) { var existsAlready = Array.from(document.body.querySelectorAll('script')).some(function(haystack) {
if (haystack.text === scriptTag.text && haystack.getAttribute('src') === scriptTag.getAttribute('src')) { if (haystack.text === scriptTag.text && haystack.getAttribute('src') === scriptTag.getAttribute('src')) {
scriptTags.push(haystack);
return true; return true;
} else {
return false;
} }
return false; });
})) { return } if (existsAlready)
return;
console.log(scriptTag);
var scriptClone = document.createElement('script'); var scriptClone = document.createElement('script');
if (scriptTag.text) if (scriptTag.text)
scriptClone.text = striptTag.text; scriptClone.text = striptTag.text;
@ -122,9 +107,25 @@
document.head.insertBefore(linkTag, null); document.head.insertBefore(linkTag, null);
}); });
var modalAlertsEl = modalContent.querySelector('#alerts');
var alertsEl = document.body.querySelector('#alerts');
if (alertsEl && modalAlertsEl) {
var modalAlerts = Array.from(modalAlertsEl.childNodes);
modalAlerts.forEach(function(alertEl) {
alertsEl.insertBefore(alertEl, alertsEl.querySelector('.alerts__toggler'));
});
if (modalAlerts.length !== 0)
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: alertsEl } }));
contentBody.removeChild(modalAlertsEl);
}
modalContent = contentBody; modalContent = contentBody;
} }
modalContent.classList.add('modal__content');
var nudgeAttr = function(attr, x) { var nudgeAttr = function(attr, x) {
var oldVal = x.getAttribute(attr); var oldVal = x.getAttribute(attr);
@ -139,36 +140,161 @@
modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); }); modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); });
}); });
modal.appendChild(modalContent); modal.querySelectorAll('.modal__content').forEach(function(prev) { modal.removeChild(prev); });
modal.insertBefore(modalContent, null);
var triggerContentLoad = function() { var triggerContentLoad = function() {
console.log('contentReady', modal);
document.dispatchEvent(new CustomEvent('setup', { document.dispatchEvent(new CustomEvent('setup', {
detail: { scope: modalContent }, detail: { scope: modal },
bubbles: true, bubbles: true,
cancelable: true cancelable: true
})); }));
} }
if (scriptTags.length !== 0) { scriptTags.forEach(function(t) { t.addEventListener('load', triggerContentLoad); });
scriptTags.forEach(function(t) { t.onload = triggerContentLoad; }); triggerContentLoad();
return 'html';
}
function responseJson(data) {
var alertsEl = document.querySelector('#alerts');
if (!alertsEl)
return null;
for (var i = 0; i < data.length; i++) {
var alert = document.createElement('div');
alert.classList.add('alert', 'alert-' + data[i].class);
var alertContent = document.createElement('div');
alertContent.classList.add('alert__content');
alertContent.innerHTML = data[i].content;
alert.appendChild(alertContent);
alertsEl.insertBefore(alert, alertsEl.querySelector('.alerts__toggler'));
} }
triggerContentLoad(); document.dispatchEvent(new CustomEvent('setup', { detail: { scope: alertsEl }, bubbles: true, cancelable: true }));
return 'json';
}
return fetch(url, init).then(function(response) {
var contentType = response.headers.get('Content-Type')
if (contentType && contentType.includes('text/html')) {
return response.text().then(responseHtml);
} else if (contentType && contentType.includes('application/json')) {
return response.json().then(responseJson);
} else {
console.log(response);
return null;
}
}); });
} };
modal.addEventListener('modal-fetch', function(event) {
var dynamicContentURL = (event.detail && event.detail.url) || trigger.getAttribute('href');
var fetchInit = (event.detail && event.detail.init) || {
credentials: 'same-origin',
headers: {
#{String (toPathPiece HeaderIsModal)}: 'True'
}
};
if (dynamicContentURL.length > 0) {
fetchModal(dynamicContentURL, fetchInit).then((event.detail && event.detail.then) || (function(){}));
}
});
modal.dispatchEvent(new CustomEvent('modal-fetch', {
detail: {
then: (function() {
if (!trigger)
return;
trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false);
})
}
}));
} else if (trigger) { // if modal has trigger assigned to it open modal on click
trigger.classList.add('modal__trigger');
trigger.addEventListener('click', open, false);
} }
// tell further modals, that this one already got initialized // tell further modals, that this one already got initialized
modal.classList.add('js-modal-initialized'); modal.classList.add('js-modal-initialized');
modal.addEventListener('modal-close', close);
} }
setup(); setup();
}; };
window.utils.ajaxSubmit = function(modal, form) {
function doSubmit(event) {
event.preventDefault();
var modalContent = modal.querySelector('.modal__content');
modalContent.style.pointerEvents = 'none';
modalContent.style.opacity = 0.5;
modal.dispatchEvent(new CustomEvent('modal-fetch', {
detail: {
url: form.target,
init: {
credentials: 'same-origin',
headers: {
#{String (toPathPiece HeaderIsModal)}: 'True'
},
method: form.method,
body: new FormData(form)
},
then: (function(typ) {
modal.dispatchEvent(new CustomEvent('modal-close'));
modalContent.style.pointerEvents = 'auto';
modalContent.style.opacity = 1;
if (typ === 'json') {
modal.dispatchEvent(new CustomEvent('modal-fetch'));
}
})
},
bubbles: true,
cancelable: true
}));
};
form.addEventListener('submit', doSubmit);
form.classList.add('js-ajax-initialized');
};
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'modal')
return;
Array.from(e.detail.scope.querySelectorAll('.js-modal:not(.js-modal-initialized)')).map(function(modal) { Array.from(e.detail.scope.querySelectorAll('.js-modal:not(.js-modal-initialized)')).forEach(function(modal) {
new utils.modal(modal); window.utils.modal(modal);
}); });
if (e.detail.scope.classList.contains('js-modal')) {
Array.from(e.detail.scope.querySelectorAll('form[data-ajax-submit]:not(.js-ajax-initialized)')).forEach(function(form) {
window.utils.ajaxSubmit(e.detail.scope, form);
});
} else {
Array.from(e.detail.scope.querySelectorAll('.js-modal')).map(function(modal) {
Array.from(modal.querySelectorAll('form[data-ajax-submit]:not(.js-ajax-initialized)')).forEach(function(form) {
window.utils.ajaxSubmit(modal, form);
});
});
};
}, false); }, false);
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'modal' }, bubbles: true, cancelable: true }))
});

View File

@ -1,4 +1,4 @@
div.modal { .modal {
position: fixed; position: fixed;
left: 50%; left: 50%;
top: 50%; top: 50%;
@ -11,8 +11,7 @@ div.modal {
border-radius: 2px; border-radius: 2px;
z-index: -1; z-index: -1;
color: var(--color-font); color: var(--color-font);
padding: 20px; padding: 0 65px 0 20px;
padding-right: 65px;
overflow: auto; overflow: auto;
transition: all .15s ease; transition: all .15s ease;
pointer-events: none; pointer-events: none;
@ -24,6 +23,10 @@ div.modal {
z-index: 200; z-index: 200;
transform: translate(-50%, -50%) scale(1, 1); transform: translate(-50%, -50%) scale(1, 1);
} }
.modal__content {
margin: 20px 0;
}
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {

View File

@ -7,6 +7,8 @@
*/ */
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
var LSNAME = 'SHOW_HIDE'; var LSNAME = 'SHOW_HIDE';
@ -50,3 +52,7 @@ document.addEventListener('setup', function(e) {
addEventHandler(el); addEventHandler(el);
}); });
}); });
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
});

View File

@ -53,7 +53,6 @@
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
// JS-TOOLTIPS NOT USED CURRENTLY. // JS-TOOLTIPS NOT USED CURRENTLY.
// initialize tooltips set via `data-tooltip` // initialize tooltips set via `data-tooltip`

View File

@ -6,6 +6,9 @@
function setupAsync(wrapper) { function setupAsync(wrapper) {
var table = wrapper.querySelector('#' + #{String $ dbtIdent}); var table = wrapper.querySelector('#' + #{String $ dbtIdent});
if (!table)
return;
var ths = Array.from(table.querySelectorAll('th.sortable')); var ths = Array.from(table.querySelectorAll('th.sortable'));
var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination'); var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination');

View File

@ -18,9 +18,15 @@
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'asidenav')
return;
var asidenavEl = e.detail.scope.querySelector('.main__aside'); var asidenavEl = e.detail.scope.querySelector('.main__aside');
window.utils.aside(asidenavEl); window.utils.aside(asidenavEl);
}); });
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'asidenav' }, bubbles: true, cancelable: true }))
});

View File

@ -69,6 +69,8 @@
})(); })();
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
if (e.detail.module && e.detail.module !== 'showHide')
return;
console.log('form setup', e.detail.scope); console.log('form setup', e.detail.scope);
@ -95,5 +97,8 @@ document.addEventListener('setup', function(e) {
}); });
return done; return done;
} }
});
document.addEventListener('DOMContentLoaded', function() {
document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'showHide' }, bubbles: true, cancelable: true }))
}); });

View File

@ -1,5 +1,6 @@
<div .modal.js-modal #modal-#{modalId} data-trigger=#{triggerId} data-closeable :modalDynamic:data-dynamic> <div .modal.js-modal #modal-#{modalId} data-trigger=#{triggerId} data-closeable :modalDynamic:data-dynamic>
$case modalContent $case modalContent
$of Right content $of Right content
^{content} <div .modal__content>
^{content}
$of Left _ $of Left _

View File

@ -17,7 +17,7 @@ $newline never
$of NavbarAside $of NavbarAside
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active> <li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal $if menuItemModal
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable=true data-dynamic=True> <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<a .navbar__link-wrapper href=#{route} ##{menuIdent}> <a .navbar__link-wrapper href=#{route} ##{menuIdent}>
<i .fas.fa-#{fromMaybe "none" menuItemIcon}> <i .fas.fa-#{fromMaybe "none" menuItemIcon}>
<div .navbar__link-label>_{SomeMessage menuItemLabel} <div .navbar__link-label>_{SomeMessage menuItemLabel}
@ -29,14 +29,14 @@ $newline never
$of NavbarRight $of NavbarRight
<li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active> <li .navbar__list-item :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal $if menuItemModal
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable=true data-dynamic=True> <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<a .navbar__link-wrapper href=#{route} ##{menuIdent}> <a .navbar__link-wrapper href=#{route} ##{menuIdent}>
<i .fas.fa-#{fromMaybe "none" menuItemIcon}> <i .fas.fa-#{fromMaybe "none" menuItemIcon}>
<div .navbar__link-label>_{SomeMessage menuItemLabel} <div .navbar__link-label>_{SomeMessage menuItemLabel}
$of NavbarSecondary $of NavbarSecondary
<li .navbar__list-item.navbar__list-item--secondary :highlight (urlRoute menuItemRoute):.navbar__list-item--active> <li .navbar__list-item.navbar__list-item--secondary :highlight (urlRoute menuItemRoute):.navbar__list-item--active>
$if menuItemModal $if menuItemModal
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable=true data-dynamic=True> <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<a .navbar__link-wrapper href=#{route} ##{menuIdent}> <a .navbar__link-wrapper href=#{route} ##{menuIdent}>
<i .fas.fa-#{fromMaybe "none" menuItemIcon}> <i .fas.fa-#{fromMaybe "none" menuItemIcon}>
<div .navbar__link-label>_{SomeMessage menuItemLabel} <div .navbar__link-label>_{SomeMessage menuItemLabel}

View File

@ -7,11 +7,11 @@ $if hasPageActions
$of PageActionPrime $of PageActionPrime
<li .pagenav__list-item> <li .pagenav__list-item>
$if menuItemModal $if menuItemModal
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable=true data-dynamic=True> <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel} <a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of PageActionSecondary $of PageActionSecondary
<li .pagenav__list-item> <li .pagenav__list-item>
$if menuItemModal $if menuItemModal
<div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable=true data-dynamic=True> <div .modal.js-modal #modal-#{menuIdent} data-trigger=#{menuIdent} data-closeable data-dynamic>
<a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel} <a .pagenav__link-wrapper href=#{route} ##{menuIdent}>_{SomeMessage menuItemLabel}
$of _ $of _