diff --git a/src/Handler/Home.hs b/src/Handler/Home.hs index c90238b0e..430583bf8 100644 --- a/src/Handler/Home.hs +++ b/src/Handler/Home.hs @@ -261,27 +261,20 @@ postHelpR = do ((res,formWidget),formEnctype) <- runFormPost $ renderAForm FormStandard $ helpForm mReferer mUid - case res of - FormSuccess HelpForm{..} -> do - now <- liftIO getCurrentTime - hfReferer' <- traverse toTextUrl hfReferer - queueJob' JobHelpRequest - { jSender = hfUserId - , jHelpRequest = hfRequest - , jRequestTime = now - , jReferer = hfReferer' - } - -- redirect $ HelpR - addMessageI Success MsgHelpSent - return () - {-selectRep $ do - provideJson () - provideRep (redirect $ HelpR :: Handler Html) -} - FormMissing -> return () - FormFailure errs -> mapM_ (addMessage Error . toHtml) errs + formResultModal res HelpR $ \HelpForm{..} -> do + now <- liftIO getCurrentTime + hfReferer' <- traverse toTextUrl hfReferer + queueJob' JobHelpRequest + { jSender = hfUserId + , jHelpRequest = hfRequest + , jRequestTime = now + , jReferer = hfReferer' + } + tell . pure =<< messageI Success MsgHelpSent defaultLayout $ do setTitle "Hilfe" -- TODO: International + isModal <- hasCustomHeader HeaderIsModal $(widgetFile "help") diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index 77065cfaf..32a1d28d9 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -1,6 +1,7 @@ module Handler.Utils.Form ( module Handler.Utils.Form , module Utils.Form + , MonadWriter(..) ) where import Utils.Form @@ -35,6 +36,7 @@ import qualified Data.Set as Set import Data.Map (Map, (!)) import qualified Data.Map as Map +import Control.Monad.Trans.Writer (execWriterT, WriterT) import Control.Monad.Writer.Class import Data.Scientific (Scientific) @@ -587,9 +589,16 @@ multiActionA FieldSettings{..} acts defAction = formToAForm $ do } ]) -formResultModal :: MonadHandler m => FormResult a -> (a -> m ()) -> m () -formResultModal res handler = do - formResult res handler - whenM (hasCustomHeader HeaderIsModal) $ do - messages <- mapM (\(cl, co) -> maybe (throwM $ UnknownMessageClass cl) (return . flip Message co) $ fromPathPiece cl) =<< getMessages - sendResponse $ toJSON messages +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 -> return $ map (Message Error . toHtml) errs + FormSuccess val -> lift . execWriterT $ handler val + + isModal <- hasCustomHeader HeaderIsModal + if + | isModal -> sendResponse $ toJSON messages + | otherwise -> do + forM_ messages $ \Message{..} -> addMessage messageClass messageContent + redirect finalDest diff --git a/src/Utils/Message.hs b/src/Utils/Message.hs index 724850167..2d819bc32 100644 --- a/src/Utils/Message.hs +++ b/src/Utils/Message.hs @@ -1,8 +1,9 @@ module Utils.Message ( MessageClass(..) , UnknownMessageClass(..) - , Message(..) , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget + , Message(..) + , messageI, messageIHamlet, messageFile, messageWidget ) where @@ -67,12 +68,17 @@ instance FromJSON Message where return Message{..} -addMessage :: MonadHandler m => MessageClass-> Html -> m () +addMessage :: MonadHandler m => MessageClass -> Html -> m () addMessage mc = ClassyPrelude.Yesod.addMessage (toPathPiece mc) addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m () 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 , RenderMessage (HandlerSite m) msg , HandlerSite m ~ site @@ -81,9 +87,20 @@ addMessageIHamlet mc iHamlet = do mr <- getMessageRender 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 mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|] +messageFile :: MessageClass -> FilePath -> ExpQ +messageFile mc tPath = [e|messageIHamlet mc $(ihamletFile tPath)|] + addMessageWidget :: forall m site. ( MonadHandler m , HandlerSite m ~ site @@ -93,3 +110,12 @@ addMessageWidget :: forall m site. addMessageWidget mc wgt = do PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt 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)) diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index 9cae5ae39..c889c20b8 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -28,7 +28,7 @@ $if not isModal ^{pageactionprime} -
+
$forall (status, msg) <- mmsgs $with status2 <- bool status "info" (status == "")
diff --git a/templates/help.hamlet b/templates/help.hamlet index 6d4b32bca..3fe70ebe6 100644 --- a/templates/help.hamlet +++ b/templates/help.hamlet @@ -1,4 +1,4 @@ _{MsgHelpIntroduction} -
+ ^{formWidget} diff --git a/templates/standalone/alerts.julius b/templates/standalone/alerts.julius index 242ca3362..e4a69d07b 100644 --- a/templates/standalone/alerts.julius +++ b/templates/standalone/alerts.julius @@ -5,24 +5,24 @@ var ALERT_INVISIBLE_CLASS = 'alert--invisible'; var TOGGLER_INVISIBLE_CLASS = 'alerts__toggler--visible'; + var alertsShowingToggler = false; window.utils.alerts = function(alertsEl) { - var alerts = Array.from(alertsEl.querySelectorAll('.alert')); - var toggler; - var showingToggler = false; + var toggler = alertsEl.querySelector('.alerts__toggler'); function makeToggler() { toggler = document.createElement('DIV'); toggler.classList.add('alerts__toggler'); toggler.addEventListener('click', function() { - alerts.forEach(function(alert) { + Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) { alert.classList.remove(ALERT_INVISIBLE_CLASS); toggler.classList.remove(TOGGLER_INVISIBLE_CLASS); }); checkToggler(); }); alertsEl.appendChild(toggler); + alertsEl.classList.add('js-initialized'); } function makeAlert(alertEl) { @@ -47,6 +47,8 @@ closeAlert(alertEl); }, autoDecay * 1000); } + + alertEl.classList.add('js-initialized'); } function closeAlert(alertEl) { @@ -56,22 +58,23 @@ function checkToggler() { var hidden = true; - alerts.forEach(function(alert) { + Array.from(alertsEl.querySelectorAll('.alert')).forEach(function(alert) { if (hidden && !alert.classList.contains(ALERT_INVISIBLE_CLASS)) { hidden = false; } }); - if (!showingToggler) { - showingToggler = true; + if (!alertsShowingToggler) { + alertsShowingToggler = true; window.setTimeout(function() { toggler.classList.toggle(TOGGLER_INVISIBLE_CLASS, hidden); - showingToggler = false; + alertsShowingToggler = false; }, 120); } } - makeToggler(); - alerts.map(makeAlert); + if (!alertsEl.classList.contains('js-initialized') || !toggler) + makeToggler(); + Array.from(alertsEl.querySelectorAll('.alert:not(.js-initialized)')).map(makeAlert); } })(); @@ -79,7 +82,11 @@ document.addEventListener('setup', function(e) { // setup alerts - var alertsEl = e.detail.scope.querySelector('.alerts'); - if (alertsEl) - window.utils.alerts(alertsEl); + 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); + } }); diff --git a/templates/standalone/modal.julius b/templates/standalone/modal.julius index 5ca78ae33..81fdc3a46 100644 --- a/templates/standalone/modal.julius +++ b/templates/standalone/modal.julius @@ -10,6 +10,10 @@ // var origParent = modal.parentNode; function open(event) { + if (!modal.classList.contains('js-modal-initialized')) { + return; + } + // disable modals for narrow screens if (event) { event.preventDefault(); @@ -17,7 +21,7 @@ modal.classList.add('modal--open'); overlay.classList.add('modal__overlay'); // document.body.insertBefore(modal, null); - document.body.insertBefore(overlay, modal); + document.body.insertBefore(overlay, modal); overlay.classList.add('modal__overlay--open'); if ('closeable' in modal.dataset) { @@ -37,7 +41,7 @@ } function close(event) { - if (typeof event === 'undefined' || event.target === closer || event.target === overlay) { + if (typeof event === 'undefined' || event.target === closer || event.target === overlay || event.target === modal) { overlay.remove(); // origParent.insertBefore(modal, null); modal.classList.remove('modal--open'); @@ -46,7 +50,7 @@ }; function setup() { - document.body.insertBefore(modal, null); + document.body.insertBefore(modal, null); // every modal can be openend via document-wide event, see openOnEvent document.addEventListener('modal-open', openOnEvent, false); @@ -57,21 +61,9 @@ } if ('dynamic' in modal.dataset) { - var dynamicContentURL = trigger.getAttribute('href'); - - console.log(dynamicContentURL); - - if (dynamicContentURL.length > 0) { - fetch(dynamicContentURL, { - credentials: 'same-origin', - headers: { - #{String (toPathPiece HeaderIsModal)}: 'True' - } - }).then(function(response) { - return response.text(); - }).then(function(body) { + function fetchModal(url, init) { + function responseHtml(body) { var modalContent = document.createElement('div'); - modalContent.classList.add('modal__content'); modalContent.innerHTML = body; var contentBody = modalContent.querySelector('.main__content-body'); @@ -122,6 +114,7 @@ modalContent = contentBody; } + modalContent.classList.add('modal__content'); var nudgeAttr = function(attr, x) { var oldVal = x.getAttribute(attr); @@ -136,11 +129,12 @@ modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); }); }); + modal.querySelectorAll('.modal__content').forEach(function(prev) { modal.removeChild(prev); }); modal.appendChild(modalContent); var triggerContentLoad = function() { document.dispatchEvent(new CustomEvent('setup', { - detail: { scope: modalContent }, + detail: { scope: modal }, bubbles: true, cancelable: true })); @@ -151,14 +145,110 @@ } 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')); + } + + 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'); + console.log(dynamicContentURL); + + 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')); } + // tell further modals, that this one already got initialized modal.classList.add('js-modal-initialized'); + + modal.addEventListener('modal-close', close); } setup(); }; + + window.utils.ajaxSubmit = function(modal, form) { + function doSubmit(event) { + event.preventDefault(); + + console.log('ajaxSubmit', modal, form); + + 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) { + modalContent.style.pointerEvents = 'auto'; + modalContent.style.opacity = 1; + + if (typ === 'json') { + modal.dispatchEvent(new CustomEvent('modal-close')); + 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) { @@ -167,5 +257,17 @@ document.addEventListener('setup', function(e) { new 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)')).map(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)')).map(function(form) { + window.utils.ajaxSubmit(modal, form); + }); + }); + }; + }, false); diff --git a/templates/table/layout.julius b/templates/table/layout.julius index 1ed9c1f88..25ded585f 100644 --- a/templates/table/layout.julius +++ b/templates/table/layout.julius @@ -6,6 +6,9 @@ function setupAsync(wrapper) { var table = wrapper.querySelector('#' + #{String $ dbtIdent}); + if (!table) + return; + var ths = Array.from(table.querySelectorAll('th.sortable')); var pagination = wrapper.querySelector('#' + #{String $ dbtIdent} + '-pagination'); diff --git a/templates/widgets/modal.hamlet b/templates/widgets/modal.hamlet index 8ffb8a53a..767fde13b 100644 --- a/templates/widgets/modal.hamlet +++ b/templates/widgets/modal.hamlet @@ -1,5 +1,6 @@
$case modalContent $of Right content - ^{content} +
+ ^{content} $of Left _