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); + }); +});