From b65b44bf8ac48a28f92d99d1881e05034a7914d1 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Tue, 19 Feb 2019 23:25:28 +0100 Subject: [PATCH 01/14] move modal from standalone to static --- src/Foundation.hs | 3 +- .../css/utils/modal.scss | 0 static/js/utils/modal.js | 85 +++++ templates/standalone/modal.hamlet | 1 - templates/standalone/modal.julius | 300 ------------------ templates/widgets/modal/modal.julius | 6 + 6 files changed, 93 insertions(+), 302 deletions(-) rename templates/standalone/modal.lucius => static/css/utils/modal.scss (100%) create mode 100644 static/js/utils/modal.js delete mode 100644 templates/standalone/modal.hamlet delete mode 100644 templates/standalone/modal.julius create mode 100644 templates/widgets/modal/modal.julius diff --git a/src/Foundation.hs b/src/Foundation.hs index a5a8e1f56..c77890f1b 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1018,6 +1018,7 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_utils_asyncTable_js addScript $ StaticR js_utils_form_js addScript $ StaticR js_utils_inputs_js + addScript $ StaticR js_utils_modal_js addScript $ StaticR js_utils_setup_js addScript $ StaticR js_utils_showHide_js addScript $ StaticR js_utils_tabber_js @@ -1025,12 +1026,12 @@ siteLayout' headingOverride widget = do addStylesheet $ StaticR css_utils_asidenav_scss addStylesheet $ StaticR css_utils_form_scss addStylesheet $ StaticR css_utils_inputs_scss + addStylesheet $ StaticR css_utils_modal_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") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") diff --git a/templates/standalone/modal.lucius b/static/css/utils/modal.scss similarity index 100% rename from templates/standalone/modal.lucius rename to static/css/utils/modal.scss diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js new file mode 100644 index 000000000..b3b874ee3 --- /dev/null +++ b/static/js/utils/modal.js @@ -0,0 +1,85 @@ +(function() { + 'use strict'; + + window.utils = window.utils || {}; + + var JS_INITIALIZED_CLASS = 'js-initialized'; + var MODAL_OPEN_CLASS = 'modal--open'; + var MODAL_TRIGGER_CLASS = 'modal__trigger'; + var MODAL_CLOSABLE_FLAG = 'closeable'; + var OVERLAY_CLASS = 'modal__overlay'; + var OVERLAY_OPEN_CLASS = 'modal__overlay--open'; + var CLOSER_CLASS = 'modal__closer'; + + window.utils.modal = function(modalElement, options) { + + if (!modalElement || modalElement.classList.contains(JS_INITIALIZED_CLASS)) { + return; + } + + var overlayElement = document.createElement('div'); + var closerElement = document.createElement('div'); + var triggerElement = document.querySelector('#' + modalElement.dataset.trigger); + + function open(event) { + event.preventDefault(); + + modalElement.classList.add(MODAL_OPEN_CLASS); + overlayElement.classList.add(OVERLAY_CLASS); + document.body.insertBefore(overlayElement, modalElement); + overlayElement.classList.add(OVERLAY_OPEN_CLASS); + } + + function close(event) { + event.preventDefault(); + overlayElement.classList.remove(OVERLAY_OPEN_CLASS); + modalElement.classList.remove(MODAL_OPEN_CLASS); + }; + + function setup() { + document.body.insertBefore(modalElement, null); + + var form = modalElement.querySelector('form'); + if (form) { + setupForm(form); + } + + if (MODAL_CLOSABLE_FLAG in modalElement.dataset) { + modalElement.insertBefore(closerElement, null); + closerElement.classList.add(CLOSER_CLASS); + closerElement.addEventListener('click', close, false); + overlayElement.addEventListener('click', close, false); + } + + triggerElement.classList.add(MODAL_TRIGGER_CLASS); + triggerElement.addEventListener('click', open, false); + modalElement.classList.add(JS_INITIALIZED_CLASS); + } + + function setupForm(form) { + form.addEventListener('submit', function(event) { + event.preventDefault(); + + var url = form.getAttribute('action'); + var httpRequestOptions = { + method: form.method, + credentials: 'same-origin', + headers: { + 'Is-Modal': 'True' + }, + body: new FormData(form), + }; + + return fetch(url, httpRequestOptions).then(function(response) { + return response.json(); + }).then(function(response) { + // TODO: process json response once backend returns json + }).catch(function(error) { + console.error('could not fetch or process response from ' + url, error); + }); + }); + } + + setup(); + }; +})(); diff --git a/templates/standalone/modal.hamlet b/templates/standalone/modal.hamlet deleted file mode 100644 index 6e29713b0..000000000 --- a/templates/standalone/modal.hamlet +++ /dev/null @@ -1 +0,0 @@ - diff --git a/templates/standalone/modal.julius b/templates/standalone/modal.julius deleted file mode 100644 index f624d1b46..000000000 --- a/templates/standalone/modal.julius +++ /dev/null @@ -1,300 +0,0 @@ -(function() { - 'use strict'; - - window.utils = window.utils || {}; - - window.utils.modal = function(modal) { - var overlay = document.createElement('div'); - var closer = document.createElement('div'); - var trigger = document.querySelector('#' + modal.dataset.trigger); - // var origParent = modal.parentNode; - - function open(event) { - if (!modal.classList.contains('js-modal-initialized')) { - return; - } - - // disable modals for narrow screens - if (event) { - event.preventDefault(); - } - modal.classList.add('modal--open'); - overlay.classList.add('modal__overlay'); - // document.body.insertBefore(modal, null); - document.body.insertBefore(overlay, modal); - overlay.classList.add('modal__overlay--open'); - - if ('closeable' in modal.dataset) { - closer.classList.add('modal__closer'); - modal.insertBefore(closer, null); - closer.addEventListener('click', close, false); - overlay.addEventListener('click', close, false); - } - } - - // you can open this modal via event - // example: document.dispatchEvent(new CustomEvent('modal-open', { details: { for: 'modal-[id]' }})) - function openOnEvent(event) { - if (event.detail.for === modal.getAttribute('id')) { - open(); - } - } - - function close(event) { - overlay.remove(); - // origParent.insertBefore(modal, null); - modal.classList.remove('modal--open'); - closer.removeEventListener('click', close, false); - }; - - function setup() { - document.body.insertBefore(modal, null); - - // every modal can be openend via document-wide event, see openOnEvent - document.addEventListener('modal-open', openOnEvent, false); - - if ('dynamic' in modal.dataset) { - function fetchModal(url, init) { - function responseHtml(body) { - var modalContent = document.createElement('div'); - modalContent.innerHTML = body; - - var contentBody = modalContent.querySelector('.main__content-body'); - var scriptTags = []; - if (contentBody) { - modalContent.querySelectorAll('script').forEach(function(scriptTag) { - var existsAlready = Array.from(document.body.querySelectorAll('script')).some(function(haystack) { - if (haystack.text === scriptTag.text && haystack.getAttribute('src') === scriptTag.getAttribute('src')) { - scriptTags.push(haystack); - return true; - } else { - return false; - } - }); - if (existsAlready) - return; - - var scriptClone = document.createElement('script'); - if (scriptTag.text) - scriptClone.text = scriptTag.text; - if (scriptTag.hasAttributes()) { - var attrs = scriptTag.attributes; - for (var i = attrs.length - 1; i >= 0; i--) { - scriptClone.setAttribute(attrs[i].name, attrs[i].value); - } - } - - document.body.insertBefore(scriptClone, null); - scriptTags.push(scriptClone); - }); - - modalContent.querySelectorAll('style').forEach(function(styleTag) { - if (Array.from(document.head.querySelectorAll('style')).some(function(haystack) { - return haystack.innerText === styleTag.innerText; - })) { return } - - document.head.insertBefore(styleTag, null); - }); - - modalContent.querySelectorAll('link').forEach(function(linkTag) { - if (linkTag.getAttribute('rel') !== 'stylesheet') - return; - - if (Array.from(document.head.querySelectorAll('link')).some(function(haystack) { - return haystack.getAttribute('href') === linkTag.getAttribute('href'); - })) { return } - - - 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.classList.add('modal__content'); - - var nudgeAttr = function(attr, x) { - var oldVal = x.getAttribute(attr); - var newVal = modal.getAttribute('id') + '__' + oldVal; - - // console.log(oldVal, newVal); - x.setAttribute(attr, newVal); - }; - - var idAttrs = ['id', 'for', 'data-conditional-id']; - idAttrs.map(function(attr) { - modalContent.querySelectorAll('[' + attr + ']').forEach(function(x) { nudgeAttr(attr, x); }); - }); - - modal.querySelectorAll('.modal__content').forEach(function(prev) { modal.removeChild(prev); }); - modal.insertBefore(modalContent, null); - - var triggerContentLoad = function() { - console.log('contentReady', modal); - - document.dispatchEvent(new CustomEvent('setup', { - detail: { scope: modal }, - bubbles: true, - cancelable: true - })); - } - - scriptTags.forEach(function(t) { t.addEventListener('load', 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'); - - 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 - modal.classList.add('js-modal-initialized'); - - modal.addEventListener('modal-close', close); - } - - 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) { - if (e.detail.module && e.detail.module !== 'modal') - return; - - Array.from(e.detail.scope.querySelectorAll('.js-modal:not(.js-modal-initialized)')).forEach(function(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); - -document.addEventListener('DOMContentLoaded', function() { - document.dispatchEvent(new CustomEvent('setup', { detail: { scope: document.body, module: 'modal' }, bubbles: true, cancelable: true })) -}); diff --git a/templates/widgets/modal/modal.julius b/templates/widgets/modal/modal.julius new file mode 100644 index 000000000..0d607cc89 --- /dev/null +++ b/templates/widgets/modal/modal.julius @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + var modalElements = Array.from(document.querySelectorAll('.modal')); + modalElements.forEach(function(modal) { + window.utils.setup('modal', modal); + }); +}); From 99394bf16643f2429b341b7ed423c1052be26002 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Tue, 19 Feb 2019 23:26:24 +0100 Subject: [PATCH 02/14] =?UTF-8?q?add=20documentation=20to=20js=20util=20?= =?UTF-8?q?=C2=BBsetup=C2=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/utils/setup.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index b71e226a5..b7296586f 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -5,9 +5,16 @@ var registeredSetupListeners = {}; - window.utils.setup = function(utilType, scope, options) { +/** + * setup function to initiate a util (utilName) on a scope (sope) with options (options). + * + * A util has to define an itself at `window.utils` as shown for the `autoSubmit` util: + * `window.utils.autoSubmit = function(wrapper, options) { ... };` + */ - if (!utilType || !scope) { + window.utils.setup = function(utilName, scope, options) { + + if (!utilName || !scope) { return; } @@ -15,44 +22,44 @@ var listener = function(event) { - if (event.detail.targetUtil !== utilType) { + if (event.detail.targetUtil !== utilName) { return false; } if (options.setupFunction) { options.setupFunction(scope, options); } else { - var util = window.utils[utilType]; + var util = window.utils[utilName]; if (!util) { - throw new Error('"' + utilType + '" is not a known js util'); + throw new Error('"' + utilName + '" is not a known js util'); } util(scope, options); } }; - if (registeredSetupListeners[utilType] && !options.singleton) { - registeredSetupListeners[utilType].push(listener); + if (registeredSetupListeners[utilName] && !options.singleton) { + registeredSetupListeners[utilName].push(listener); } else { - window.utils.teardown(utilType); - registeredSetupListeners[utilType] = [ listener ]; + window.utils.teardown(utilName); + registeredSetupListeners[utilName] = [ listener ]; } document.addEventListener('setup', listener); document.dispatchEvent(new CustomEvent('setup', { - detail: { targetUtil: utilType, module: 'none' }, + detail: { targetUtil: utilName, module: 'none' }, bubbles: true, cancelable: true, })); }; - window.utils.teardown = function(utilType) { - if (registeredSetupListeners[utilType]) { - registeredSetupListeners[utilType].forEach(function(listener) { + window.utils.teardown = function(utilName) { + if (registeredSetupListeners[utilName]) { + registeredSetupListeners[utilName].forEach(function(listener) { document.removeEventListener('setup', listener); }); - delete registeredSetupListeners[utilType]; + delete registeredSetupListeners[utilName]; } } })(); From a91bc3388fae0c681638c6eea923033de1ab65d8 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Tue, 19 Feb 2019 23:38:04 +0100 Subject: [PATCH 03/14] add comment to remind about unique identifiers --- templates/widgets/modal/modal.julius | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/widgets/modal/modal.julius b/templates/widgets/modal/modal.julius index 0d607cc89..14a53bde7 100644 --- a/templates/widgets/modal/modal.julius +++ b/templates/widgets/modal/modal.julius @@ -1,4 +1,5 @@ document.addEventListener('DOMContentLoaded', function() { + // TODO: replace for loop with one precise query for this specific modal instance var modalElements = Array.from(document.querySelectorAll('.modal')); modalElements.forEach(function(modal) { window.utils.setup('modal', modal); From b8eb78d11afc4519b8457ac1df48eb89eceee12c Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Wed, 20 Feb 2019 08:43:43 +0100 Subject: [PATCH 04/14] add data-ajax-submit to email-test-modal --- src/Handler/Admin.hs | 2 +- static/js/utils/modal.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index a159489fe..3a53de344 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -77,7 +77,7 @@ postAdminTestR = do (FormFailure errs) -> forM_ errs $ addMessage Error . toHtml let emailWidget' = [whamlet| -
+ ^{emailWidget} |] diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index b3b874ee3..5aa898014 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -10,6 +10,7 @@ var OVERLAY_CLASS = 'modal__overlay'; var OVERLAY_OPEN_CLASS = 'modal__overlay--open'; var CLOSER_CLASS = 'modal__closer'; + var AJAX_SUBMIT_FLAG = 'ajaxSubmit' window.utils.modal = function(modalElement, options) { @@ -40,7 +41,7 @@ document.body.insertBefore(modalElement, null); var form = modalElement.querySelector('form'); - if (form) { + if (form && AJAX_SUBMIT_FLAG in form.dataset) { setupForm(form); } @@ -75,7 +76,7 @@ }).then(function(response) { // TODO: process json response once backend returns json }).catch(function(error) { - console.error('could not fetch or process response from ' + url, error); + console.error('could not fetch or process response from ' + url, { error }); }); }); } From 695d0a997824176b51e0be75ca77ebfbe8e9da03 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Fri, 22 Feb 2019 23:04:09 +0100 Subject: [PATCH 05/14] fix modals not being initialized after table interaction closes #304 --- static/js/utils/asyncTable.js | 3 +++ static/js/utils/modal.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js index d11f6298e..bc9215374 100644 --- a/static/js/utils/asyncTable.js +++ b/static/js/utils/asyncTable.js @@ -195,6 +195,9 @@ Array.from(wrapper.querySelectorAll('form')).forEach(function(form) { window.utils.setup('form', form); }); + Array.from(wrapper.querySelectorAll('.modal')).forEach(function(modal) { + window.utils.setup('modal', modal); + }); } init(); diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 5aa898014..1e591271a 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -3,7 +3,7 @@ window.utils = window.utils || {}; - var JS_INITIALIZED_CLASS = 'js-initialized'; + var JS_INITIALIZED_CLASS = 'js-modal-initialized'; var MODAL_OPEN_CLASS = 'modal--open'; var MODAL_TRIGGER_CLASS = 'modal__trigger'; var MODAL_CLOSABLE_FLAG = 'closeable'; From f5ee39eafa9ef71fc3259b4c59e9c64784d5be18 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 14:05:59 +0100 Subject: [PATCH 06/14] =?UTF-8?q?more=20documentation=20for=20js=20util=20?= =?UTF-8?q?=C2=BBsetup=C2=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/js/utils/setup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index b7296586f..36e61a9b7 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -8,8 +8,8 @@ /** * setup function to initiate a util (utilName) on a scope (sope) with options (options). * - * A util has to define an itself at `window.utils` as shown for the `autoSubmit` util: - * `window.utils.autoSubmit = function(wrapper, options) { ... };` + * Utils need to be defined as property of `window.utils` and need to accept a scope and (optionally) options. + * Example: `window.utils.autoSubmit = function(scope, options) { ... };` */ window.utils.setup = function(utilName, scope, options) { From 77ecd5c59f90023509f8200a15fee2d88dd5cd03 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 22:27:11 +0100 Subject: [PATCH 07/14] move alerts out of main-content element --- templates/default-layout.hamlet | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/default-layout.hamlet b/templates/default-layout.hamlet index 87870650b..f43282fb0 100644 --- a/templates/default-layout.hamlet +++ b/templates/default-layout.hamlet @@ -29,8 +29,8 @@ $if not isModal ^{widget} - - ^{alerts} + + ^{alerts} $if not isModal From 99949cc4dbb3a44a484d5ec78e7b14fc7ea95da8 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 22:27:45 +0100 Subject: [PATCH 08/14] reimplement dynamic modals --- static/css/utils/modal.scss | 1 + static/js/utils/modal.js | 106 +++++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/static/css/utils/modal.scss b/static/css/utils/modal.scss index 0718a4367..5cac989a3 100644 --- a/static/css/utils/modal.scss +++ b/static/css/utils/modal.scss @@ -13,6 +13,7 @@ color: var(--color-font); padding: 0 65px 0 20px; overflow: auto; + overscroll-behavior: contain; transition: opacity .2s .1s ease-in-out, transform .3s ease-in-out; diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 1e591271a..6d12da8d5 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -6,11 +6,28 @@ var JS_INITIALIZED_CLASS = 'js-modal-initialized'; var MODAL_OPEN_CLASS = 'modal--open'; var MODAL_TRIGGER_CLASS = 'modal__trigger'; + var MODAL_CONTENT_CLASS = 'modal__content'; + var MAIN_CONTENT_CLASS = 'main__content-body' var MODAL_CLOSABLE_FLAG = 'closeable'; + var MODAL_DYNAMIC_FLAG = 'dynamic'; var OVERLAY_CLASS = 'modal__overlay'; var OVERLAY_OPEN_CLASS = 'modal__overlay--open'; var CLOSER_CLASS = 'modal__closer'; - var AJAX_SUBMIT_FLAG = 'ajaxSubmit' + var AJAX_SUBMIT_FLAG = 'ajaxSubmit'; + + // helper function for convenient http requests with appropriate headers + function httpRequest(url, method, body) { + var requestOptions = { + method: method, + credentials: 'same-origin', + headers: { + 'Is-Modal': 'True', + }, + body: body, + }; + + return fetch(url, requestOptions); + } window.utils.modal = function(modalElement, options) { @@ -22,6 +39,18 @@ var closerElement = document.createElement('div'); var triggerElement = document.querySelector('#' + modalElement.dataset.trigger); + function setup() { + document.body.insertBefore(modalElement, null); + + setupForm(); + setupCloser(); + + triggerElement.classList.add(MODAL_TRIGGER_CLASS); + triggerElement.addEventListener('click', open, false); + + modalElement.classList.add(JS_INITIALIZED_CLASS); + } + function open(event) { event.preventDefault(); @@ -29,6 +58,11 @@ overlayElement.classList.add(OVERLAY_CLASS); document.body.insertBefore(overlayElement, modalElement); overlayElement.classList.add(OVERLAY_OPEN_CLASS); + + var modalUrl = triggerElement.getAttribute('href'); + if (modalUrl && MODAL_DYNAMIC_FLAG in modalElement.dataset) { + fillModal(modalUrl); + } } function close(event) { @@ -37,50 +71,60 @@ modalElement.classList.remove(MODAL_OPEN_CLASS); }; - function setup() { - document.body.insertBefore(modalElement, null); - + function setupForm() { var form = modalElement.querySelector('form'); if (form && AJAX_SUBMIT_FLAG in form.dataset) { - setupForm(form); - } + form.addEventListener('submit', function(event) { + event.preventDefault(); + var url = form.getAttribute('action'); + var body = new FormData(form); + + return httpRequest(url, form.method, body).then(function(response) { + return response.json(); + }).then(function(response) { + // TODO: process json response once backend returns json + }).catch(function(error) { + console.error('could not fetch or process response from ' + url, { error }); + }); + }); + } + } + + function setupCloser() { if (MODAL_CLOSABLE_FLAG in modalElement.dataset) { modalElement.insertBefore(closerElement, null); closerElement.classList.add(CLOSER_CLASS); closerElement.addEventListener('click', close, false); overlayElement.addEventListener('click', close, false); } - - triggerElement.classList.add(MODAL_TRIGGER_CLASS); - triggerElement.addEventListener('click', open, false); - modalElement.classList.add(JS_INITIALIZED_CLASS); } - function setupForm(form) { - form.addEventListener('submit', function(event) { - event.preventDefault(); - - var url = form.getAttribute('action'); - var httpRequestOptions = { - method: form.method, - credentials: 'same-origin', - headers: { - 'Is-Modal': 'True' - }, - body: new FormData(form), - }; - - return fetch(url, httpRequestOptions).then(function(response) { - return response.json(); - }).then(function(response) { - // TODO: process json response once backend returns json - }).catch(function(error) { - console.error('could not fetch or process response from ' + url, { error }); - }); + function fillModal(url) { + httpRequest(url, 'GET').then(function(response) { + response.text().then(processResponse); }); } + function processResponse(reponseBody) { + var modalContent = document.createElement('div'); + modalContent.classList.add(MODAL_CONTENT_CLASS); + modalContent.innerHTML = reponseBody; + + var contentBody = modalContent.querySelector('.' + MAIN_CONTENT_CLASS); + + if (contentBody) { + modalContent.innerHTML = contentBody.innerHTML; + } + + var previousModalContent = modalElement.querySelector('.' + MODAL_CONTENT_CLASS); + if (previousModalContent) { + previousModalContent.remove(); + } + modalElement.insertBefore(modalContent, null); + setupForm(); + } + setup(); }; })(); From 3a30aea5df3df93b0a76751870eb8077a16d6cf1 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 23:00:21 +0100 Subject: [PATCH 09/14] =?UTF-8?q?added=20new=20utils=20=C2=BBhttpClient?= =?UTF-8?q?=C2=AB=20and=20=C2=BBasyncForm=C2=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Foundation.hs | 2 ++ static/js/utils/asyncForm.js | 36 +++++++++++++++++++++++++++ static/js/utils/asyncTable.js | 16 ++++++------ static/js/utils/httpClient.js | 32 ++++++++++++++++++++++++ static/js/utils/modal.js | 47 ++++++++++++----------------------- static/js/utils/setup.js | 14 +++++++---- 6 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 static/js/utils/asyncForm.js create mode 100644 static/js/utils/httpClient.js diff --git a/src/Foundation.hs b/src/Foundation.hs index f35c841e7..e3c89b3aa 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1019,8 +1019,10 @@ siteLayout' headingOverride widget = do -- JavaScript utils addScript $ StaticR js_utils_alerts_js addScript $ StaticR js_utils_asidenav_js + addScript $ StaticR js_utils_asyncForm_js addScript $ StaticR js_utils_asyncTable_js addScript $ StaticR js_utils_checkAll_js + addScript $ StaticR js_utils_httpClient_js addScript $ StaticR js_utils_form_js addScript $ StaticR js_utils_inputs_js addScript $ StaticR js_utils_modal_js diff --git a/static/js/utils/asyncForm.js b/static/js/utils/asyncForm.js new file mode 100644 index 000000000..a7086bdc3 --- /dev/null +++ b/static/js/utils/asyncForm.js @@ -0,0 +1,36 @@ +(function collonadeClosure() { + 'use strict'; + + window.utils = window.utils || {}; + + window.utils.asyncForm = function(formElement, options) { + + function setup() { + formElement.addEventListener('submit', function(event) { + event.preventDefault(); + + var url = formElement.getAttribute('action'); + var headers = { }; + var body = new FormData(formElement); + + if (options && options.headers) { + Object.keys(options.headers).forEach(function(headerKey) { + headers[headerKey] = options.headers[headerKey]; + }); + } + + window.utils.httpClient.post(url, headers, body) + .then(function(response) { + return response.json(); + }).then(function(response) { + console.log('asyncForm: got response', response); + // TODO: process json response once backend returns json + }).catch(function(error) { + console.error('could not fetch or process response from ' + url, { error }); + }); + }); + } + + setup(); + }; +})(); diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js index bc9215374..a7f46a134 100644 --- a/static/js/utils/asyncTable.js +++ b/static/js/utils/asyncTable.js @@ -139,14 +139,16 @@ // fetches new sorted table from url with params and replaces contents of current table function updateTableFrom(url, tableOptions) { + if (!window.utils.httpClient) { + throw new Error('httpClient not found!'); + } + tableOptions = tableOptions || {}; - fetch(url, { - credentials: 'same-origin', - headers: { - 'Accept': 'text/html', - [shortCircuitHeader]: tableIdent - } - }).then(function(response) { + var headers = { + 'Accept': 'text/html', + [shortCircuitHeader]: tableIdent + }; + window.utils.httpClient.get(url, headers).then(function(response) { if (!response.ok) { throw new Error('Looks like there was a problem fetching ' + url.href + '. Status Code: ' + response.status); } diff --git a/static/js/utils/httpClient.js b/static/js/utils/httpClient.js new file mode 100644 index 000000000..18d82fbb2 --- /dev/null +++ b/static/js/utils/httpClient.js @@ -0,0 +1,32 @@ +(function collonadeClosure() { + 'use strict'; + + window.utils = window.utils || {}; + + window.utils.httpClient = (function() { + + function _fetch(url, method, additionalHeaders, body) { + var requestOptions = { + credentials: 'same-origin', + headers: { }, + method: method, + body: body, + }; + + Object.keys(additionalHeaders).forEach(function(headerKey) { + requestOptions.headers[headerKey] = additionalHeaders[headerKey]; + }); + + return fetch(url, requestOptions); + } + + return { + get: function(url, headers) { + return _fetch(url, 'GET', headers); + }, + post: function(url, headers, body) { + return _fetch(url, 'POST', headers, body); + }, + } + })(); +})(); diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 6d12da8d5..71a48be4f 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -10,25 +10,14 @@ var MAIN_CONTENT_CLASS = 'main__content-body' var MODAL_CLOSABLE_FLAG = 'closeable'; var MODAL_DYNAMIC_FLAG = 'dynamic'; + var MODAL_HEADERS = { + 'Is-Modal': 'True', + }; var OVERLAY_CLASS = 'modal__overlay'; var OVERLAY_OPEN_CLASS = 'modal__overlay--open'; var CLOSER_CLASS = 'modal__closer'; var AJAX_SUBMIT_FLAG = 'ajaxSubmit'; - // helper function for convenient http requests with appropriate headers - function httpRequest(url, method, body) { - var requestOptions = { - method: method, - credentials: 'same-origin', - headers: { - 'Is-Modal': 'True', - }, - body: body, - }; - - return fetch(url, requestOptions); - } - window.utils.modal = function(modalElement, options) { if (!modalElement || modalElement.classList.contains(JS_INITIALIZED_CLASS)) { @@ -73,21 +62,12 @@ function setupForm() { var form = modalElement.querySelector('form'); - if (form && AJAX_SUBMIT_FLAG in form.dataset) { - form.addEventListener('submit', function(event) { - event.preventDefault(); + if (form) { + window.utils.setup('form', form); - var url = form.getAttribute('action'); - var body = new FormData(form); - - return httpRequest(url, form.method, body).then(function(response) { - return response.json(); - }).then(function(response) { - // TODO: process json response once backend returns json - }).catch(function(error) { - console.error('could not fetch or process response from ' + url, { error }); - }); - }); + if (AJAX_SUBMIT_FLAG in form.dataset) { + window.utils.setup('asyncForm', form, { headers: MODAL_HEADERS }); + } } } @@ -101,9 +81,14 @@ } function fillModal(url) { - httpRequest(url, 'GET').then(function(response) { - response.text().then(processResponse); - }); + if (!window.utils.httpClient) { + throw new Error('httpClient not found! Can\' fetch modal content from ' + url); + } + + window.utils.httpClient.get(url, MODAL_HEADERS) + .then(function(response) { + response.text().then(processResponse); + }); } function processResponse(reponseBody) { diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index 36e61a9b7..7068bc476 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -47,11 +47,15 @@ document.addEventListener('setup', listener); - document.dispatchEvent(new CustomEvent('setup', { - detail: { targetUtil: utilName, module: 'none' }, - bubbles: true, - cancelable: true, - })); + // put a slight delay on the setup event in order to allow for all dependencies to load + window.setTimeout(function() { + document.dispatchEvent(new CustomEvent('setup', { + detail: { targetUtil: utilName, module: 'none' }, + bubbles: true, + cancelable: true, + })); + }, 10); + }; window.utils.teardown = function(utilName) { From b343066a88f0e12d4dc9c3a4e9deacdd7c38becc Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 23:21:57 +0100 Subject: [PATCH 10/14] move help page introduction into form --- templates/help.hamlet | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/help.hamlet b/templates/help.hamlet index 3fe70ebe6..5cc4c96b8 100644 --- a/templates/help.hamlet +++ b/templates/help.hamlet @@ -1,4 +1,3 @@ -_{MsgHelpIntroduction} - +
_{MsgHelpIntroduction} ^{formWidget} From 2f3a735ae285a79e77e05500f6c66e6c8e317919 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sun, 24 Feb 2019 23:23:23 +0100 Subject: [PATCH 11/14] make async forms process json response --- src/Foundation.hs | 1 + static/css/utils/asyncForm.scss | 9 +++++++++ static/js/utils/asyncForm.js | 27 +++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 static/css/utils/asyncForm.scss diff --git a/src/Foundation.hs b/src/Foundation.hs index e3c89b3aa..e4de524d2 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1031,6 +1031,7 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_utils_tabber_js addStylesheet $ StaticR css_utils_alerts_scss addStylesheet $ StaticR css_utils_asidenav_scss + addStylesheet $ StaticR css_utils_asyncForm_scss addStylesheet $ StaticR css_utils_form_scss addStylesheet $ StaticR css_utils_inputs_scss addStylesheet $ StaticR css_utils_modal_scss diff --git a/static/css/utils/asyncForm.scss b/static/css/utils/asyncForm.scss new file mode 100644 index 000000000..eb8dc0635 --- /dev/null +++ b/static/css/utils/asyncForm.scss @@ -0,0 +1,9 @@ +.async-form-response { + margin: 20px 0; +} + +.async-form-loading { + opacity: 0.1; + transition: opacity 400ms ease-in-out; + pointer-events: none; +} diff --git a/static/js/utils/asyncForm.js b/static/js/utils/asyncForm.js index a7086bdc3..14e77c805 100644 --- a/static/js/utils/asyncForm.js +++ b/static/js/utils/asyncForm.js @@ -3,12 +3,21 @@ window.utils = window.utils || {}; + var ASYNC_FORM_RESPONSE_CLASS = 'async-form-response'; + var ASYNC_FORM_LOADING_CLASS = 'async-form-loading'; + var ASYNC_FORM_MIN_DELAY = 600; + window.utils.asyncForm = function(formElement, options) { + var lastRequestTimestamp = 0; + function setup() { formElement.addEventListener('submit', function(event) { event.preventDefault(); + formElement.classList.add(ASYNC_FORM_LOADING_CLASS) + lastRequestTimestamp = Date.now(); + var url = formElement.getAttribute('action'); var headers = { }; var body = new FormData(formElement); @@ -23,14 +32,28 @@ .then(function(response) { return response.json(); }).then(function(response) { - console.log('asyncForm: got response', response); - // TODO: process json response once backend returns json + processResponse(response[0]) }).catch(function(error) { console.error('could not fetch or process response from ' + url, { error }); }); }); } + function processResponse(response) { + var responseElement = document.createElement('div'); + responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS); + responseElement.innerHTML = response.content; + var parentElement = formElement.parentElement; + + // make sure there is a delay between click and response + var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now()); + setTimeout(function() { + parentElement.insertBefore(responseElement, formElement); + formElement.remove(); + }, delay); + + } + setup(); }; })(); From e9f7cdf682515aa4380b695b401b4f5e132b2ae9 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Mon, 25 Feb 2019 20:24:44 +0100 Subject: [PATCH 12/14] move async form initialization to form util --- static/css/utils/asyncForm.scss | 2 +- static/js/utils/form.js | 11 +++++++++-- static/js/utils/modal.js | 7 +------ static/js/utils/setup.js | 14 +++++--------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/static/css/utils/asyncForm.scss b/static/css/utils/asyncForm.scss index eb8dc0635..cd4fe3159 100644 --- a/static/css/utils/asyncForm.scss +++ b/static/css/utils/asyncForm.scss @@ -4,6 +4,6 @@ .async-form-loading { opacity: 0.1; - transition: opacity 400ms ease-in-out; + transition: opacity 800ms ease-in-out; pointer-events: none; } diff --git a/static/js/utils/form.js b/static/js/utils/form.js index 30ba76c96..1e0db4c20 100644 --- a/static/js/utils/form.js +++ b/static/js/utils/form.js @@ -3,9 +3,10 @@ window.utils = window.utils || {}; - var JS_INITIALIZED = 'js-initialized'; + var JS_INITIALIZED = 'js-form-initialized'; var SUBMIT_BUTTON_SELECTOR = '[type="submit"]:not([formnovalidate])'; var AUTOSUBMIT_BUTTON_SELECTOR = '[type="submit"][data-autosubmit]'; + var AJAX_SUBMIT_FLAG = 'ajaxSubmit'; function formValidator(inputs) { var done = true; @@ -23,7 +24,6 @@ if (form.classList.contains(JS_INITIALIZED)) { return false; } - form.classList.add(JS_INITIALIZED); // reactive buttons var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR); @@ -37,6 +37,13 @@ // hide autoSubmit submit button window.utils.setup('autoSubmit', form, options); + + // async form + if (AJAX_SUBMIT_FLAG in form.dataset) { + window.utils.setup('asyncForm', form, options); + } + + form.classList.add(JS_INITIALIZED); }; // registers input-listener for each element in (array) and diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 71a48be4f..7c7cc5953 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -16,7 +16,6 @@ var OVERLAY_CLASS = 'modal__overlay'; var OVERLAY_OPEN_CLASS = 'modal__overlay--open'; var CLOSER_CLASS = 'modal__closer'; - var AJAX_SUBMIT_FLAG = 'ajaxSubmit'; window.utils.modal = function(modalElement, options) { @@ -63,11 +62,7 @@ function setupForm() { var form = modalElement.querySelector('form'); if (form) { - window.utils.setup('form', form); - - if (AJAX_SUBMIT_FLAG in form.dataset) { - window.utils.setup('asyncForm', form, { headers: MODAL_HEADERS }); - } + window.utils.setup('form', form, { headers: MODAL_HEADERS }); } } diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index 7068bc476..36e61a9b7 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -47,15 +47,11 @@ document.addEventListener('setup', listener); - // put a slight delay on the setup event in order to allow for all dependencies to load - window.setTimeout(function() { - document.dispatchEvent(new CustomEvent('setup', { - detail: { targetUtil: utilName, module: 'none' }, - bubbles: true, - cancelable: true, - })); - }, 10); - + document.dispatchEvent(new CustomEvent('setup', { + detail: { targetUtil: utilName, module: 'none' }, + bubbles: true, + cancelable: true, + })); }; window.utils.teardown = function(utilName) { From f798142a2965ef68fc4c14cc262c69fa1196efc4 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Mon, 25 Feb 2019 20:41:10 +0100 Subject: [PATCH 13/14] add submitHandler helper function to asyncform util --- static/js/utils/asyncForm.js | 51 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/static/js/utils/asyncForm.js b/static/js/utils/asyncForm.js index 14e77c805..7917012a9 100644 --- a/static/js/utils/asyncForm.js +++ b/static/js/utils/asyncForm.js @@ -12,31 +12,7 @@ var lastRequestTimestamp = 0; function setup() { - formElement.addEventListener('submit', function(event) { - event.preventDefault(); - - formElement.classList.add(ASYNC_FORM_LOADING_CLASS) - lastRequestTimestamp = Date.now(); - - var url = formElement.getAttribute('action'); - var headers = { }; - var body = new FormData(formElement); - - if (options && options.headers) { - Object.keys(options.headers).forEach(function(headerKey) { - headers[headerKey] = options.headers[headerKey]; - }); - } - - window.utils.httpClient.post(url, headers, body) - .then(function(response) { - return response.json(); - }).then(function(response) { - processResponse(response[0]) - }).catch(function(error) { - console.error('could not fetch or process response from ' + url, { error }); - }); - }); + formElement.addEventListener('submit', submitHandler); } function processResponse(response) { @@ -51,7 +27,32 @@ parentElement.insertBefore(responseElement, formElement); formElement.remove(); }, delay); + } + function submitHandler(event) { + event.preventDefault(); + + formElement.classList.add(ASYNC_FORM_LOADING_CLASS) + lastRequestTimestamp = Date.now(); + + var url = formElement.getAttribute('action'); + var headers = { }; + var body = new FormData(formElement); + + if (options && options.headers) { + Object.keys(options.headers).forEach(function(headerKey) { + headers[headerKey] = options.headers[headerKey]; + }); + } + + window.utils.httpClient.post(url, headers, body) + .then(function(response) { + return response.json(); + }).then(function(response) { + processResponse(response[0]) + }).catch(function(error) { + console.error('could not fetch or process response from ' + url, { error }); + }); } setup(); From 8dd4fd98cbd87b3eb992c2fe36c7db639434b372 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Mon, 25 Feb 2019 20:41:38 +0100 Subject: [PATCH 14/14] close modals with Escape key --- static/js/utils/modal.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 7c7cc5953..63d7471d3 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -34,14 +34,17 @@ setupCloser(); triggerElement.classList.add(MODAL_TRIGGER_CLASS); - triggerElement.addEventListener('click', open, false); + triggerElement.addEventListener('click', openHandler, false); modalElement.classList.add(JS_INITIALIZED_CLASS); } - function open(event) { + function openHandler(event) { event.preventDefault(); + open(); + } + function open() { modalElement.classList.add(MODAL_OPEN_CLASS); overlayElement.classList.add(OVERLAY_CLASS); document.body.insertBefore(overlayElement, modalElement); @@ -51,12 +54,20 @@ if (modalUrl && MODAL_DYNAMIC_FLAG in modalElement.dataset) { fillModal(modalUrl); } + + document.addEventListener('keyup', keyupHandler); } - function close(event) { + function closeHandler(event) { event.preventDefault(); + close(); + } + + function close() { overlayElement.classList.remove(OVERLAY_OPEN_CLASS); modalElement.classList.remove(MODAL_OPEN_CLASS); + + document.removeEventListener('keyup', keyupHandler); }; function setupForm() { @@ -70,8 +81,8 @@ if (MODAL_CLOSABLE_FLAG in modalElement.dataset) { modalElement.insertBefore(closerElement, null); closerElement.classList.add(CLOSER_CLASS); - closerElement.addEventListener('click', close, false); - overlayElement.addEventListener('click', close, false); + closerElement.addEventListener('click', closeHandler, false); + overlayElement.addEventListener('click', closeHandler, false); } } @@ -105,6 +116,12 @@ setupForm(); } + function keyupHandler(event) { + if (event.key === 'Escape') { + close(); + } + } + setup(); }; })();