Fully ajaxified modals

This commit is contained in:
Gregor Kleen 2018-11-22 17:25:56 +01:00
parent a627b7be72
commit 213f3a39cc
9 changed files with 202 additions and 61 deletions

View File

@ -261,27 +261,20 @@ postHelpR = do
((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,9 +589,16 @@ multiActionA FieldSettings{..} acts defAction = formToAForm $ do
} }
]) ])
formResultModal :: MonadHandler m => FormResult a -> (a -> m ()) -> m () formResultModal :: (MonadHandler m, RedirectUrl (HandlerSite m) route) => FormResult a -> route -> (a -> WriterT [Message] m ()) -> m ()
formResultModal res handler = do formResultModal res finalDest handler = maybeT_ $ do
formResult res handler messages <- case res of
whenM (hasCustomHeader HeaderIsModal) $ do FormMissing -> mzero
messages <- mapM (\(cl, co) -> maybe (throwM $ UnknownMessageClass cl) (return . flip Message co) $ fromPathPiece cl) =<< getMessages FormFailure errs -> return $ map (Message Error . toHtml) errs
sendResponse $ toJSON messages 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

@ -1,8 +1,9 @@
module Utils.Message module Utils.Message
( MessageClass(..) ( MessageClass(..)
, UnknownMessageClass(..) , UnknownMessageClass(..)
, Message(..)
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget , addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, Message(..)
, messageI, messageIHamlet, messageFile, messageWidget
) where ) where
@ -67,12 +68,17 @@ instance FromJSON Message where
return Message{..} return Message{..}
addMessage :: MonadHandler m => MessageClass-> Html -> m () 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
@ -81,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
@ -93,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

@ -28,7 +28,7 @@ $if not isModal
^{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}">

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,22 +58,23 @@
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);
} }
})(); })();
@ -79,7 +82,11 @@
document.addEventListener('setup', function(e) { document.addEventListener('setup', function(e) {
// 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);
}
}); });

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,7 +41,7 @@
} }
function close(event) { 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(); overlay.remove();
// origParent.insertBefore(modal, null); // origParent.insertBefore(modal, null);
modal.classList.remove('modal--open'); modal.classList.remove('modal--open');
@ -46,7 +50,7 @@
}; };
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);
@ -57,21 +61,9 @@
} }
if ('dynamic' in modal.dataset) { if ('dynamic' in modal.dataset) {
var dynamicContentURL = trigger.getAttribute('href'); function fetchModal(url, init) {
function responseHtml(body) {
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) {
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');
@ -122,6 +114,7 @@
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);
@ -136,11 +129,12 @@
modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); }); modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); });
}); });
modal.querySelectorAll('.modal__content').forEach(function(prev) { modal.removeChild(prev); });
modal.appendChild(modalContent); modal.appendChild(modalContent);
var triggerContentLoad = function() { var triggerContentLoad = function() {
document.dispatchEvent(new CustomEvent('setup', { document.dispatchEvent(new CustomEvent('setup', {
detail: { scope: modalContent }, detail: { scope: modal },
bubbles: true, bubbles: true,
cancelable: true cancelable: true
})); }));
@ -151,14 +145,110 @@
} }
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'));
}
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 // 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();
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) { document.addEventListener('setup', function(e) {
@ -167,5 +257,17 @@ document.addEventListener('setup', function(e) {
new utils.modal(modal); 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); }, false);

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

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