From 7756862aeb5a52865ba14967a910ffc980a32211 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sat, 2 Mar 2019 22:27:03 +0100 Subject: [PATCH 01/32] add table filter js utility --- src/Foundation.hs | 1 + static/js/utils/asyncTable.js | 44 +++--- static/js/utils/asyncTableFilter.js | 154 +++++++++++++++++++ static/js/utils/form.js | 15 +- templates/table/layout-filter-default.hamlet | 4 +- 5 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 static/js/utils/asyncTableFilter.js diff --git a/src/Foundation.hs b/src/Foundation.hs index e4de524d2..d76f6328f 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1021,6 +1021,7 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_utils_asidenav_js addScript $ StaticR js_utils_asyncForm_js addScript $ StaticR js_utils_asyncTable_js + addScript $ StaticR js_utils_asyncTableFilter_js addScript $ StaticR js_utils_checkAll_js addScript $ StaticR js_utils_httpClient_js addScript $ StaticR js_utils_form_js diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js index a7f46a134..e9d4ca49d 100644 --- a/static/js/utils/asyncTable.js +++ b/static/js/utils/asyncTable.js @@ -5,6 +5,8 @@ var HEADER_HEIGHT = 80; var RESET_OPTIONS = [ 'scrollTo' ]; + var TABLE_FILTER_FORM_CLASS = 'table-filter-form'; + var JS_INITIALIZED_CLASS = 'js-async-table-initialized'; window.utils.asyncTable = function(wrapper, options) { @@ -42,17 +44,24 @@ // pagesize form pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form'); + // filter + var filterForm = wrapper.querySelector('.' + TABLE_FILTER_FORM_CLASS); + if (filterForm) { + options.updateTableFrom = updateTableFrom; + window.utils.setup('asyncTableFilter', filterForm, options); + } + // take options into account - if (options && options.scrollTo) { + if (options.scrollTo) { window.scrollTo(options.scrollTo); } - if (options && options.horizPos && scrollTable) { + if (options.horizPos && scrollTable) { scrollTable.scrollLeft = options.horizPos; } setupListeners(); - wrapper.classList.add('js-initialized'); + wrapper.classList.add(JS_INITIALIZED_CLASS); } function setupListeners() { @@ -117,28 +126,16 @@ } function changePagesizeHandler(event) { - var currentTableUrl = options.currentUrl || window.location.href; - var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value); - url = new URL(getUrlWithResetPagenumber(url)); + var pagesizeParamKey = tableIdent + '-pagesize'; + var pageParamKey = tableIdent + '-page'; + var url = new URL(options.currentUrl || window.location.href); + url.searchParams.set(pagesizeParamKey, event.target.value); + url.searchParams.set(pageParamKey, 0); 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) { + function updateTableFrom(url, tableOptions, callback) { if (!window.utils.httpClient) { throw new Error('httpClient not found!'); } @@ -157,6 +154,9 @@ tableOptions.currentUrl = url.href; removeListeners(); updateWrapperContents(data, tableOptions); + if (callback && typeof callback === 'function') { + callback(wrapper); + } }).catch(function(err) { console.error(err); }); @@ -165,7 +165,7 @@ function updateWrapperContents(newHtml, tableOptions) { tableOptions = tableOptions || {}; wrapper.innerHTML = newHtml; - wrapper.classList.remove("js-initialized"); + wrapper.classList.remove(JS_INITIALIZED_CLASS); // setup the wrapper and its components to behave async again window.utils.teardown('asyncTable'); diff --git a/static/js/utils/asyncTableFilter.js b/static/js/utils/asyncTableFilter.js new file mode 100644 index 000000000..b9bdf4a88 --- /dev/null +++ b/static/js/utils/asyncTableFilter.js @@ -0,0 +1,154 @@ +(function () { + 'use strict'; + + window.utils = window.utils || {}; + + var JS_INITIALIZED_CLASS = 'js-async-table-filter-initialized'; + + // debounce function, taken from Underscore.js + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + }; + + window.utils.asyncTableFilter = function(formElement, options) { + if (!options || !options.updateTableFrom) { + return false; + } + + if (formElement.matches('.' + JS_INITIALIZED_CLASS)) { + return false; + } + + var formId = formElement.querySelector('[name="_formid"]').value; + var inputs = { + search: [], + input: [], + change: [], + select: [], + } + + function setup() { + gatherInputs(); + addEventListeners(); + } + + function gatherInputs() { + Array.from(formElement.querySelectorAll('input[type="search"]')).forEach(function(input) { + inputs.search.push(input); + }); + + Array.from(formElement.querySelectorAll('input[type="text"]')).forEach(function(input) { + inputs.input.push(input); + }); + + Array.from(formElement.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) { + inputs.change.push(input); + }); + + Array.from(formElement.querySelectorAll('select')).forEach(function(input) { + inputs.select.push(input); + }); + } + + function addEventListeners() { + inputs.search.forEach(function(input) { + var debouncedInput = debounce(function() { + if (input.value.length === 0 || input.value.length > 2) { + updateTable(); + } + }, 400); + input.addEventListener('input', debouncedInput); + }); + + inputs.input.forEach(function(input) { + var debouncedInput = debounce(function() { + if (input.value.length === 0 || input.value.length > 2) { + updateTable(); + } + }, 400); + input.addEventListener('input', debouncedInput); + }); + + inputs.change.forEach(function(input) { + input.addEventListener('change', function() { + updateTable(); + }); + }); + + inputs.select.forEach(function(input) { + input.addEventListener('change', function() { + updateTable(); + }); + }); + + formElement.addEventListener('submit', function(event) { + event.preventDefault(); + updateTable(); + }); + } + + function updateTable() { + var url = serializeFormToURL(); + var callback = null; + + var focusedSearch = inputs.search.reduce(function(acc, input) { + return acc || (input.matches(':focus') && input); + }, null); + // focus search input + if (focusedSearch) { + var selectionStart = focusedSearch.selectionStart; + callback = function(wrapper) { + var search = wrapper.querySelector('input[type="search"]'); + if (search) { + search.focus(); + search.selectionStart = selectionStart; + } + }; + } + options.updateTableFrom(url, options, callback); + } + + function serializeFormToURL() { + var url = new URL(options.currentUrl || window.location.href); + url.searchParams.set('_formid', formId); + url.searchParams.set('_hasdata', 'true'); + + inputs.search.forEach(function(input) { + url.searchParams.set(input.name, input.value); + }); + + inputs.input.forEach(function(input) { + url.searchParams.set(input.name, input.value); + }); + + inputs.change.forEach(function(input) { + if (input.checked) { + url.searchParams.set(input.name, input.value); + } + }); + + inputs.select.forEach(function(select) { + var options = Array.from(select.querySelectorAll('option')); + var selected = options.find(function(option) { return option.selected }); + if (selected) { + url.searchParams.set(select.name, selected.value); + } + }); + + return url; + } + + setup(); + } +})(); diff --git a/static/js/utils/form.js b/static/js/utils/form.js index 1e0db4c20..a8d987c80 100644 --- a/static/js/utils/form.js +++ b/static/js/utils/form.js @@ -26,10 +26,7 @@ } // reactive buttons - var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR); - if (submitBtn) { - window.utils.setup('reactiveButton', form, { button: submitBtn }); - } + window.utils.setup('reactiveButton', form); // conditonal fieldsets var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]')); @@ -43,18 +40,20 @@ window.utils.setup('asyncForm', form, options); } + // inputs + window.utils.setup('inputs', form, options); + form.classList.add(JS_INITIALIZED); }; // registers input-listener for each element in (array) and // enables