diff --git a/src/Foundation.hs b/src/Foundation.hs index 4dfdfc166..62f9dccf8 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1334,6 +1334,7 @@ siteLayout' headingOverride widget = do addScript $ StaticR js_polyfills_urlPolyfill_js -- JavaScript services addScript $ StaticR js_services_utilRegistry_js + addScript $ StaticR js_services_htmlHelpers_js addScript $ StaticR js_services_httpClient_js addScript $ StaticR js_services_i18n_js -- JavaScript utils diff --git a/static/js/services/htmlHelpers.js b/static/js/services/htmlHelpers.js new file mode 100644 index 000000000..30477314c --- /dev/null +++ b/static/js/services/htmlHelpers.js @@ -0,0 +1,50 @@ +(function () { + 'use strict'; + + window.HtmlHelpers = (function() { + + // `parseResponse` takes a raw HttpClient response and an options object. + // Returns an object with `element` being an contextual fragment of the + // HTML in the response and `ifPrefix` being the prefix that was used to + // "unique-ify" the ids of the received HTML. + // Original Response IDs can optionally be kept by adding `keepIds: true` + // to the `options` object. + function parseResponse(response, options) { + options = options || {}; + + return response.text().then(function (responseText) { + var docFrag = document.createRange().createContextualFragment(responseText); + var idPrefix; + if (!options.keepIds) { + idPrefix = _getIdPrefix(); + _prefixIds(docFrag, idPrefix); + } + return Promise.resolve({ idPrefix: idPrefix, element: docFrag }); + }, + function (error) { + return Promise.reject(error); + }).catch(function (error) { console.error(error); }); + } + + function _prefixIds(element, idPrefix) { + var idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger']; + + idAttrs.forEach(function(attr) { + Array.from(element.querySelectorAll('[' + attr + ']')).forEach(function(input) { + var value = idPrefix + input.getAttribute(attr); + input.setAttribute(attr, value); + }); + }); + } + + function _getIdPrefix() { + // leading 'r'(andom) to overcome the fact that IDs + // starting with a numeric value are not valid in CSS + return 'r' + Math.floor(Math.random() * 100000) + '__'; + } + + return { + parseResponse: parseResponse, + } + })(); +})(); diff --git a/static/js/services/httpClient.js b/static/js/services/httpClient.js index a69d788fd..f65fb0e3f 100644 --- a/static/js/services/httpClient.js +++ b/static/js/services/httpClient.js @@ -11,21 +11,23 @@ } } - function _fetch(url, method, additionalHeaders, body) { + function _fetch(options) { var requestOptions = { credentials: 'same-origin', headers: { }, - method: method, - body: body, + method: options.method, + body: options.body, }; - Object.keys(additionalHeaders).forEach(function(headerKey) { - requestOptions.headers[headerKey] = additionalHeaders[headerKey]; + Object.keys(options.headers).forEach(function(headerKey) { + requestOptions.headers[headerKey] = options.headers[headerKey]; }); - return fetch(url, requestOptions).then( + return fetch(options.url, requestOptions).then( function(response) { - _responseInterceptors.forEach(function(interceptor) { interceptor(response); }); + _responseInterceptors.forEach(function(interceptor) { + interceptor(response, options); + }); return Promise.resolve(response); }, function(error) { @@ -36,43 +38,35 @@ }); } - function parseHTML(response, idPrefix) { - if (!idPrefix) { - idPrefix = Math.floor(Math.random() * 100000); - } - - var contentType = response.headers.get("content-type"); - if (contentType.indexOf("text/html") === -1) { - throw new Error('Server returned ' + contentType + ' when HTML was expected'); - } - - return response.text().then(function (responseText) { - var docFrag = document.createRange().createContextualFragment(responseText); - - var idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger']; - idAttrs.forEach(function(attr) { - Array.from(docFrag.querySelectorAll('[' + attr + ']')).forEach(function(input) { - var value = idPrefix + '__' + input.getAttribute(attr); - input.setAttribute(attr, value); - }); - }); - - return Promise.resolve(docFrag); - }, - function (error) { - return Promise.reject(error); - }).catch(function (error) { console.error(error); }); - } - return { - get: function(url, headers) { - return _fetch(url, 'GET', headers); + get: function(args) { + args.method = 'GET'; + return _fetch(args); }, - post: function(url, headers, body) { - return _fetch(url, 'POST', headers, body); + post: function(args) { + args.method = 'POST'; + return _fetch(args); }, addResponseInterceptor: addResponseInterceptor, - parseHTML: parseHTML, + ACCEPT: { + TEXT_HTML: 'text/html', + JSON: 'application/json', + }, } })(); + + // HttpClient ships with its own little interceptor to throw an error + // if the response does not match the expected content-type + function contentTypeInterceptor(response, options) { + if (!options || !options.accept) { + return; + } + + var contentType = response.headers.get("content-type"); + if (!contentType.match(options.accept)) { + throw new Error('Server returned with "' + contentType + '" when "' + options.accept + '" was expected'); + } + } + + HttpClient.addResponseInterceptor(contentTypeInterceptor); })(); diff --git a/static/js/utils/asyncForm.js b/static/js/utils/asyncForm.js index eaf6f9221..4750a7e57 100644 --- a/static/js/utils/asyncForm.js +++ b/static/js/utils/asyncForm.js @@ -96,21 +96,21 @@ headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE; } - HttpClient.post(url, headers, body) - .then(function(response) { - if (response.headers.get("content-type").indexOf("application/json") !== -1) {// checking response header - return response.json(); - } else { - throw new TypeError('Unexpected Content-Type. Expected Content-Type: "application/json". Requested URL:' + url + '"'); - } - }).then(function(response) { - processResponse(response[0]); - }).catch(function(error) { - var failureMessage = I18n.get('asyncFormFailure'); - processResponse({ content: failureMessage }); + HttpClient.post({ + url: url, + headers: headers, + body: body, + accept: HttpClient.ACCEPT.JSON, + }).then(function(response) { + return response.json(); + }).then(function(response) { + processResponse(response[0]); + }).catch(function(error) { + var failureMessage = I18n.get('asyncFormFailure'); + processResponse({ content: failureMessage }); - element.classList.remove(ASYNC_FORM_LOADING_CLASS); - }); + element.classList.remove(ASYNC_FORM_LOADING_CLASS); + }); } return init(); diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js index 78d9ae90c..8877c7991 100644 --- a/static/js/utils/asyncTable.js +++ b/static/js/utils/asyncTable.js @@ -35,6 +35,7 @@ var pageLinks = []; var pagesizeForm; var scrollTable; + var cssIdPrefix = ''; var tableFilterInputs = { search: [], @@ -48,12 +49,18 @@ throw new Error('Async Table utility cannot be setup without an element!'); } + if (element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) { + return false; + } + // param asyncTableDbHeader if (element.dataset.asyncTableDbHeader !== undefined) { asyncTableHeader = element.dataset.asyncTableDbHeader; } - asyncTableId = element.querySelector('table').id; + var rawTableId = element.querySelector('table').id; + cssIdPrefix = findCssIdPrefix(rawTableId); + asyncTableId = rawTableId.replace(cssIdPrefix, ''); // find scrolltable wrapper scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR); @@ -96,7 +103,7 @@ } function setupPagination() { - var pagination = element.querySelector('#' + asyncTableId + '-pagination'); + var pagination = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagination'); if (pagination) { pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) { return { element: link }; @@ -122,7 +129,7 @@ function setupPageSizeSelect() { // pagesize form - pagesizeForm = element.querySelector('#' + asyncTableId + '-pagesize-form'); + pagesizeForm = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagesize-form'); if (pagesizeForm) { var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]'); @@ -200,11 +207,15 @@ var focusedInput = tableFilterForm.querySelector(':focus, :active'); // focus previously focused input - if (focusedInput) { + if (focusedInput && focusedInput.selectionStart !== null) { var selectionStart = focusedInput.selectionStart; - var focusId = focusedInput.id; + // remove the following part of the id to get rid of the random + // (yet somewhat structured) prefix we got from nudging. + var prefix = findCssIdPrefix(focusedInput.id); + var focusId = focusedInput.id.replace(prefix, ''); callback = function(wrapper) { - var toBeFocused = wrapper.querySelector('#' + focusId); + var idPrefix = getLocalStorageParameter('cssIdPrefix'); + var toBeFocused = wrapper.querySelector('#' + idPrefix + focusId); if (toBeFocused) { toBeFocused.focus(); toBeFocused.selectionStart = selectionStart; @@ -319,41 +330,56 @@ [asyncTableHeader]: asyncTableId }; - HttpClient.get(url, headers).then( - response => HttpClient.parseHTML(response, element.id) - ).then(function(data) { + HttpClient.get({ + url: url, + headers: headers, + accept: HttpClient.ACCEPT.TEXT_HTML, + }).then(function(response) { + return HtmlHelpers.parseResponse(response); + }).then(function(response) { setLocalStorageParameter('currentTableUrl', url.href); // reset table removeListeners(); element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); // update table with new - updateWrapperContents(data); + updateWrapperContents(response); - if (callback && typeof callback === 'function') { - callback(element); + if (UtilRegistry) { + UtilRegistry.setupAll(element); } - element.classList.remove(ASYNC_TABLE_LOADING_CLASS); + if (callback && typeof callback === 'function') { + setLocalStorageParameter('cssIdPrefix', response.idPrefix); + callback(element); + setLocalStorageParameter('cssIdPrefix', ''); + } }).catch(function(err) { console.error(err); + }).finally(function() { element.classList.remove(ASYNC_TABLE_LOADING_CLASS); }); } - function updateWrapperContents(newHtml) { + function updateWrapperContents(response) { var newPage = document.createElement('div'); - newPage.appendChild(newHtml); - var newWrapperContents = newPage.querySelector('#' + element.id + '__' + element.id); + newPage.appendChild(response.element); + var newWrapperContents = newPage.querySelector('#' + response.idPrefix + element.id); element.innerHTML = newWrapperContents.innerHTML; - - if (UtilRegistry) { - UtilRegistry.setupAll(element); - } } return init(); }; + // returns any random nudged prefix found in the given id + function findCssIdPrefix(id) { + var matcher = /r\d*?__/; + var maybePrefix = id.match(matcher); + if (maybePrefix && maybePrefix[0]) { + return maybePrefix[0] + } + return ''; + } + function setLocalStorageParameter(key, value) { var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; if (value !== null) { diff --git a/static/js/utils/checkAll.js b/static/js/utils/checkAll.js index 5a15e0ac7..1c207be34 100644 --- a/static/js/utils/checkAll.js +++ b/static/js/utils/checkAll.js @@ -30,6 +30,10 @@ throw new Error('Check All utility cannot be setup without an element!'); } + if (element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) { + return false; + } + gatherColumns(); setupCheckAllCheckbox(); diff --git a/static/js/utils/massInput.js b/static/js/utils/massInput.js index 4f0400b6a..8dbccdca3 100644 --- a/static/js/utils/massInput.js +++ b/static/js/utils/massInput.js @@ -38,6 +38,10 @@ throw new Error('Mass Input utility cannot be setup without an element!'); } + if (element.classList.contains(MASS_INPUT_INITIALIZED_CLASS)) { + return false; + } + massInputId = element.dataset.massInputIdent || '_'; massInputForm = element.closest('form'); @@ -120,18 +124,20 @@ if (enctype !== 'multipart/form-data') headers['Content-Type'] = enctype; - - requestFn( - url, - headers, - requestBody, - ).then(response => HttpClient.parseHTML(response, element.id) - ).then(function(response) { - processResponse(response); - if (isAddCell) { - reFocusAddCell(); - } - }); + + requestFn({ + url: url, + headers: headers, + body: requestBody, + accept: HttpClient.ACCEPT.TEXT_HTML, + }).then(function(response) { + return HtmlHelpers.parseResponse(response); + }).then(function(response) { + processResponse(response.element); + if (isAddCell) { + reFocusAddCell(); + } + }); } }; } @@ -162,9 +168,9 @@ button.removeEventListener('click', massInputFormSubmitHandler); } - function processResponse(response) { + function processResponse(responseElement) { element.innerHTML = ""; - element.appendChild(response); + element.appendChild(responseElement); reset(); diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index e0ba9c7c1..6710c0854 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -167,15 +167,18 @@ throw new Error('HttpClient not found! Can\'t fetch modal content from ' + url); } - HttpClient.get(url, MODAL_HEADERS) - .then(response => HttpClient.parseHTML(response, element.id)) - .then(processResponse); + HttpClient.get({ + url: url, + headers: MODAL_HEADERS, + accept: HttpClient.ACCEPT.TEXT_HTML, + }).then(function(response) { + return HtmlHelpers.parseResponse(response); + }).then(function(response) { + processResponse(response.element); + }); } - function processResponse(responseFrag) { - var responseElement = document.createElement('div'); - responseElement.appendChild(responseFrag); - + function processResponse(responseElement) { var modalContent = document.createElement('div'); modalContent.classList.add(MODAL_CONTENT_CLASS);