From ebb930762986f4c6cc479d2a36536b4d8b50af24 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sat, 16 Feb 2019 19:57:33 +0100 Subject: [PATCH] move js for asyncTable to static --- src/Foundation.hs | 10 +- static/css/utils/inputs.scss | 308 ++++++++++++++++++++++++++++++++++ static/js/utils/asyncTable.js | 205 ++++++++++++++++++++++ static/js/utils/setup.js | 8 +- templates/table/layout.julius | 182 +------------------- 5 files changed, 528 insertions(+), 185 deletions(-) create mode 100644 static/css/utils/inputs.scss create mode 100644 static/js/utils/asyncTable.js diff --git a/src/Foundation.hs b/src/Foundation.hs index 463c9b497..ee315c9a9 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -990,8 +990,8 @@ siteLayout' headingOverride widget = do pc <- widgetToPageContent $ do -- 3rd party - addScript $ StaticR js_vendor_zepto_js addScript $ StaticR js_vendor_flatpickr_js + addScript $ StaticR js_vendor_zepto_js addStylesheet $ StaticR css_vendor_flatpickr_css addStylesheet $ StaticR css_vendor_fontawesome_css -- fonts @@ -1000,16 +1000,18 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_polyfills_fetchPolyfill_js addScript $ StaticR js_polyfills_urlPolyfill_js -- JavaScript utils - addScript $ StaticR js_utils_setup_js - addScript $ StaticR js_utils_tabber_js addScript $ StaticR js_utils_alerts_js addScript $ StaticR js_utils_asidenav_js + addScript $ StaticR js_utils_asyncTable_js addScript $ StaticR js_utils_inputs_js + addScript $ StaticR js_utils_setup_js addScript $ StaticR js_utils_showHide_js - addStylesheet $ StaticR css_utils_tabber_scss + addScript $ StaticR js_utils_tabber_js addStylesheet $ StaticR css_utils_alerts_scss addStylesheet $ StaticR css_utils_asidenav_scss + addStylesheet $ StaticR css_utils_inputs_scss addStylesheet $ StaticR css_utils_showHide_scss + addStylesheet $ StaticR css_utils_tabber_scss addStylesheet $ StaticR css_utils_tooltip_scss -- widgets $(widgetFile "default-layout") diff --git a/static/css/utils/inputs.scss b/static/css/utils/inputs.scss new file mode 100644 index 000000000..b67b5ee65 --- /dev/null +++ b/static/css/utils/inputs.scss @@ -0,0 +1,308 @@ +/* GENERAL STYLES FOR FORMS */ + +/* FORM GROUPS */ +.form-group { + position: relative; + display: flex; + display: grid; + grid-template-columns: 1fr 3fr; + grid-gap: 5px; + justify-content: flex-start; + align-items: flex-start; + padding: 4px 0; + border-left: 2px solid transparent; + + + .form-group { + margin-top: 13px; + } +} + +.form-group__label { + font-weight: 600; + padding-top: 6px; +} + +.form-group--required { + + .form-group__label::after { + content: ' *'; + color: var(--color-error); + } +} + +.form-group--optional { + .form-group__label::after { + content: ''; + } +} + +.form-group--submit .form-group__input { + grid-column: 2; +} + +@media (max-width: 768px) { + .form-group--submit .form-group__input { + grid-column: 1; + } +} + +.form-group--has-error { + background-color: rgba(255, 0, 0, 0.1); + + input, textarea { + border-color: var(--color-error) !important; + } +} + +@media (max-width: 768px) { + .form-group { + grid-template-columns: 1fr; + align-items: baseline; + margin-top: 17px; + flex-direction: column; + } +} + +/* TEXT INPUTS */ +input[type="text"], +input[type="search"], +input[type="password"], +input[type="url"], +input[type="number"], +input[type="email"], +input[type*="date"], +input[type*="time"], +select { + /* from bulma.css */ + color: #363636; + border-color: #dbdbdb; + background-color: #f3f3f3; + box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); + + width: 100%; + max-width: 600px; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 4px; + font-size: 1rem; + font-family: var(--font-base); + line-height: 1.5; + padding: 4px 13px; +} + +input[type="number"] { + width: 100px; +} + +input[type*="date"], +input[type*="time"], +.flatpickr-input[type="text"] { + width: 50%; + width: 250px; +} + +/* BUTTON STYLE SEE default-layout.lucius */ + +/* TEXTAREAS */ +textarea { + width: 100%; + height: 170px; + max-width: 600px; + line-height: 1.5; + color: #363636; + background-color: #f3f3f3; + padding: 4px 13px; + font-size: 1rem; + font-family: var(--font-base); + -webkit-appearance: none; + appearance: none; + border: 1px solid #dbdbdb; + border-radius: 2px; + box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); + vertical-align: top; +} + +/* SHARED STATE RELATED STYLES */ + +input, +select, +textarea { + &:focus { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50,115,220,.25); + outline: 0; + } + + &[disabled] { + background-color: #f3f3f3; + color: #7a7a7a; + box-shadow: none; + border-color: #dbdbdb; + } + + &[readonly] { + background-color: #f5f5f5; + border-color: #dbdbdb; + } +} + +/* OPTIONS */ +select, +option { + font-size: 1rem; + line-height: 1.5; + padding: 4px 13px; + border: 1px solid #dbdbdb; + border-radius: 2px; + outline: 0; + color: #363636; + min-width: 200px; + background-color: #f3f3f3; + box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); +} + +@media (max-width: 425px) { + + select, option { + width: 100%; + } +} + +/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */ +input[type="checkbox"] { + position: relative; + height: 20px; + width: 20px; + -webkit-appearance: none; + appearance: none; + cursor: pointer; +} +input[type="checkbox"]::before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + background-color: var(--color-lighter); + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; +} +input[type="checkbox"]:checked::before { + background-color: var(--color-light); +} +input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 20px; +} + +/* CUSTOM CHECKBOXES AND RADIO BOXES */ +/* Completely replaces legacy checkbox and radiobox */ +.checkbox, +.radio { + position: relative; + display: inline-block; + + [type="checkbox"], + [type="radio"] { + position: fixed; + top: -1px; + left: -1px; + width: 1px; + height: 1px; + overflow: hidden; + } + + label { + display: block; + height: 24px; + width: 24px; + background-color: #f3f3f3; + box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05); + border: 2px solid var(--color-primary); + border-radius: 4px; + color: white; + cursor: pointer; + } + + label::before, + label::after { + position: absolute; + display: block; + top: 12px; + left: 8px; + height: 2px; + width: 8px; + background-color: var(--color-font); + } + + :checked + label { + background-color: var(--color-primary); + } + + [type="checkbox"]:focus + label, + [type="radio"]:focus + label { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50,115,220,.25); + outline: 0; + } + + :checked + label::before, + :checked + label::after { + content: ''; + } + + :checked + label::before { + background-color: white; + transform: rotate(45deg); + left: 4px; + } + + :checked + label::after { + background-color: white; + transform: rotate(-45deg); + top: 11px; + width: 13px; + } + + [disabled] + label { + pointer-events: none; + border: none; + opacity: 0.6; + filter: grayscale(1); + } +} + +.radio::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + border-radius: 4px; + border: 2px solid white; + z-index: -1; +} + +/* CUSTOM FILE INPUT */ +.file-input__label { + cursor: pointer; + display: inline-block; + background-color: var(--color-primary); + color: white; + padding: 10px 17px; + border-radius: 3px; +} + +.file-input__input--hidden { + display: none; +} diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js new file mode 100644 index 000000000..5067b74aa --- /dev/null +++ b/static/js/utils/asyncTable.js @@ -0,0 +1,205 @@ +(function collonadeClosure() { + 'use strict'; + + window.utils = window.utils || {}; + + var HEADER_HEIGHT = 80; + var RESET_OPTIONS = [ 'scrollTo' ]; + + window.utils.asyncTable = function(wrapper, options) { + + options = options || {}; + var tableIdent = options.dbtIdent; + var shortCircuitHeader = options ? options.headerDBTableShortcircuit : null; + + var ths = []; + var pageLinks = []; + var pagesizeForm; + var scrollTable; + + function init() { + var table = wrapper.querySelector('#' + tableIdent); + + if (!table) { + return; + } + + scrollTable = wrapper.querySelector('.scrolltable'); + + // sortable table headers + ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) { + return { element: th }; + }); + + // pagination links + var pagination = wrapper.querySelector('#' + tableIdent + '-pagination'); + if (pagination) { + pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) { + return { element: link }; + }); + } + + // pagesize form + pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form'); + + // take options into account + if (options && options.scrollTo) { + window.scrollTo(options.scrollTo); + } + + if (options && options.horizPos && scrollTable) { + scrollTable.scrollLeft = options.horizPos; + } + + setupListeners(); + wrapper.classList.add('js-initialized'); + } + + function setupListeners() { + ths.forEach(function(th) { + th.clickHandler = function(event) { + var boundClickHandler = clickHandler.bind(this); + var horizPos = (scrollTable || {}).scrollLeft; + boundClickHandler(event, { horizPos }); + }; + th.element.addEventListener('click', th.clickHandler); + }); + + pageLinks.forEach(function(link) { + link.clickHandler = function(event) { + var boundClickHandler = clickHandler.bind(this); + var tableBoundingRect = scrollTable.getBoundingClientRect(); + var tableOptions = {}; + if (tableBoundingRect.top < HEADER_HEIGHT) { + tableOptions.scrollTo = { + top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT, + left: scrollTable.offsetLeft || 0, + behavior: 'smooth', + }; + } + boundClickHandler(event, tableOptions); + } + link.element.addEventListener('click', link.clickHandler); + }); + + if (pagesizeForm) { + var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]'); + pagesizeSelect.addEventListener('change', changePagesizeHandler); + } + } + + function removeListeners() { + ths.forEach(function(th) { + th.element.removeEventListener('click', th.clickHandler); + }); + + pageLinks.forEach(function(link) { + link.element.removeEventListener('click', link.clickHandler); + }); + + if (pagesizeForm) { + var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]') + pagesizeSelect.removeEventListener('change', changePagesizeHandler); + } + } + + function clickHandler(event, tableOptions) { + event.preventDefault(); + var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this)); + updateTableFrom(url, tableOptions); + } + + function getClickDestination(el) { + if (!el.querySelector('a')) { + return ''; + } + return el.querySelector('a').getAttribute('href'); + } + + function changePagesizeHandler(event) { + var currentTableUrl = options.currentUrl || window.location.href; + var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value); + url = new URL(getUrlWithResetPagenumber(url)); + updateTableFrom(url); + } + + function getUrlWithUpdatedPagesize(url, pagesize) { + if (url.indexOf('pagesize') >= 0) { + return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize); + } else if (url.indexOf('?') >= 0) { + return url += '&' + tableIdent + '-pagesize=' + pagesize; + } + + return url += '?' + tableIdent + '-pagesize=' + pagesize; + } + + function getUrlWithResetPagenumber(url) { + return url.replace(/-page=\d+/, '-page=0'); + } + + // fetches new sorted table from url with params and replaces contents of current table + function updateTableFrom(url, tableOptions) { + tableOptions = tableOptions || {}; + fetch(url, { + credentials: 'same-origin', + headers: { + 'Accept': 'text/html', + [shortCircuitHeader]: tableIdent + } + }).then(function(response) { + if (!response.ok) { + throw new Error('Looks like there was a problem fetching ' + url.href + '. Status Code: ' + response.status); + } + return response.text(); + }).then(function(data) { + tableOptions.currentUrl = url.href; + removeListeners(); + updateWrapperContents(data, tableOptions); + }).catch(function(err) { + console.error(err); + }); + } + + function updateWrapperContents(newHtml, tableOptions) { + tableOptions = tableOptions || {}; + wrapper.innerHTML = newHtml; + wrapper.classList.remove("js-initialized"); + + // setup the wrapper and its components to behave async again + window.utils.teardown('asyncTable'); + // merge global options and table specific options + var resetOptions = {}; + Object.keys(options) + .filter(function(key) { + return !RESET_OPTIONS.includes(key); + }) + .forEach(function(key) { + resetOptions[key] = options[key]; + }); + var combinedOptions = {}; + combinedOptions = Object.keys(tableOptions) + .filter(function(key) { + return tableOptions.hasOwnProperty(key); + }) + .map(function(key) { + return { key, value: tableOptions[key] } + }) + .reduce(function(cumulatedOpts, opt) { + cumulatedOpts[opt.key] = opt.value; + return cumulatedOpts; + }, resetOptions); + + window.utils.setup('asyncTable', wrapper, combinedOptions); + + // make sure to hide any new submit buttons + document.dispatchEvent(new CustomEvent('setup', { + detail: { + scope: wrapper, + module: 'autoSubmit' + } + })); + } + + init(); + }; +})(); diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index 5c9c336e4..a9978c46f 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -11,13 +11,15 @@ return; } + options = options || {}; + var listener = function(event) { if (event.detail.targetUtil !== utilType) { return false; } - if (options && options.setupFunction) { + if (options.setupFunction) { options.setupFunction(scope, options); } else { var util = window.utils[utilType]; @@ -29,9 +31,10 @@ } }; - if (registeredSetupListeners[utilType]) { + if (registeredSetupListeners[utilType] && !options.singleton) { registeredSetupListeners[utilType].push(listener); } else { + window.utils.teardown(utilType); registeredSetupListeners[utilType] = [ listener ]; } @@ -49,6 +52,7 @@ registeredSetupListeners[utilType].forEach(function(listener) { document.removeEventListener('setup', listener); }); + delete registeredSetupListeners[utilType]; } } })(); diff --git a/templates/table/layout.julius b/templates/table/layout.julius index 296f03721..3bc39312d 100644 --- a/templates/table/layout.julius +++ b/templates/table/layout.julius @@ -1,186 +1,10 @@ -(function collonadeClosure() { - 'use strict'; - - window.utils = window.utils || {}; - - window.utils.asyncTable = function(wrapper, options) { - - var tableIdent = wrapper.dataset.dbtIdent; - var shortCircuitHeader = #{String (toPathPiece HeaderDBTableShortcircuit)}; - - var ths = []; - var pageLinks = []; - var pagesizeForm; - var scrollTable; - - function init() { - var table = wrapper.querySelector('#' + tableIdent); - if (!table) { - return; - } - - scrollTable = wrapper.querySelector('.scrolltable'); - - // sortable table headers - ths = Array.from(table.querySelectorAll('th.sortable')).map(function(th) { - return { element: th }; - }); - - // pagination links - var pagination = wrapper.querySelector('#' + tableIdent + '-pagination'); - if (pagination) { - pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) { - return { element: link }; - }); - } - - // pagesize form - pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form'); - - // take options into account - if (options && options.scrollTo) { - window.scrollTo(options.scrollTo); - } - - if (options && options.horizPos && scrollTable) { - scrollTable.scrollLeft = options.horizPos; - } - - setupListeners(); - wrapper.classList.add('js-initialized'); - } - - function setupListeners() { - ths.forEach(function(th) { - th.clickHandler = function(event) { - var boundClickHandler = clickHandler.bind(this); - var horizPos = (scrollTable || {}).scrollLeft; - boundClickHandler(event, { horizPos }); - }; - th.element.addEventListener('click', th.clickHandler); - }); - - pageLinks.forEach(function(link) { - link.clickHandler = function(event) { - var boundClickHandler = clickHandler.bind(this); - var wrapperBoundingRect = wrapper.getBoundingClientRect(); - var options = {}; - if (wrapperBoundingRect.top < 160) { - options.scrollTo = { - top: (wrapper.offsetTop || 0) - 60, - left: wrapper.offsetLeft || 0, - behavior: 'smooth', - }; - } - boundClickHandler(event, options); - } - link.element.addEventListener('click', link.clickHandler); - }); - - if (pagesizeForm) { - var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]'); - pagesizeSelect.addEventListener('change', changePagesizeHandler); - } - } - - function removeListeners() { - ths.forEach(function(th) { - th.element.removeEventListener('click', th.clickHandler); - }); - - pageLinks.forEach(function(link) { - link.element.removeEventListener('click', link.clickHandler); - }); - - if (pagesizeForm) { - var pagesizeSelect = pagesizeForm.querySelector('[name=' + tableIdent + '-pagesize]') - pagesizeSelect.removeEventListener('change', changePagesizeHandler); - } - } - - function clickHandler(event, options) { - event.preventDefault(); - var url = new URL(window.location.origin + window.location.pathname + getClickDestination(this)); - updateTableFrom(url, options); - } - - function getClickDestination(el) { - if (!el.querySelector('a')) { - return ''; - } - return el.querySelector('a').getAttribute('href'); - } - - function changePagesizeHandler(event) { - var currentTableUrl = wrapper.dataset.currentUrl || window.location.href; - var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value); - url = getUrlWithResetPagenumber(url); - updateTableFrom(url); - } - - function getUrlWithUpdatedPagesize(url, pagesize) { - if (url.indexOf('pagesize') >= 0) { - return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize); - } else if (url.indexOf('?') >= 0) { - return url += '&' + tableIdent + '-pagesize=' + pagesize; - } - - return url += '?' + tableIdent + '-pagesize=' + pagesize; - } - - function getUrlWithResetPagenumber(url) { - return url.replace(/-page=\d+/, '-page=0'); - } - - function updateWrapperContents(newHtml, options) { - wrapper.innerHTML = newHtml; - wrapper.classList.remove("js-initialized"); - - // setup the wrapper and its components to behave async again - window.utils.asyncTable(wrapper, options); - - // make sure to hide any new submit buttons - document.dispatchEvent(new CustomEvent('setup', { - detail: { - scope: wrapper, - module: 'autoSubmit' - } - })); - } - - // fetches new sorted table from url with params and replaces contents of current table - function updateTableFrom(url, options) { - - fetch(url, { - credentials: 'same-origin', - headers: { - 'Accept': 'text/html', - [shortCircuitHeader]: tableIdent - } - }).then(function(response) { - if (!response.ok) { - throw new Error('Looks like there was a problem fetching ' + url + '. Status Code: ' + response.status); - } - return response.text(); - }).then(function(data) { - wrapper.dataset.currentUrl = url; - removeListeners(); - updateWrapperContents(data, options); - }).catch(function(err) { - console.error(err); - }); - } - - init(); - }; -})(); - document.addEventListener('DOMContentLoaded', function() { var dbtIdent = #{String $ dbtIdent}; + var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)}; var selector = '#' + dbtIdent + '-table-wrapper:not(.js-initialized)'; var wrapper = document.querySelector(selector); + if (wrapper) { - wrapper.dataset.dbtIdent = dbtIdent; - window.utils.asyncTable(wrapper); + window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent }); } });