Merge branch 'http-client-html-helpers' into 'master'

Http client html helpers

See merge request !225
This commit is contained in:
Felix Hamann 2019-05-20 23:26:49 +02:00
commit a4f8aaec81
8 changed files with 179 additions and 95 deletions

View File

@ -1334,6 +1334,7 @@ siteLayout' headingOverride widget = do
addScript $ StaticR js_polyfills_urlPolyfill_js addScript $ StaticR js_polyfills_urlPolyfill_js
-- JavaScript services -- JavaScript services
addScript $ StaticR js_services_utilRegistry_js addScript $ StaticR js_services_utilRegistry_js
addScript $ StaticR js_services_htmlHelpers_js
addScript $ StaticR js_services_httpClient_js addScript $ StaticR js_services_httpClient_js
addScript $ StaticR js_services_i18n_js addScript $ StaticR js_services_i18n_js
-- JavaScript utils -- JavaScript utils

View File

@ -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,
}
})();
})();

View File

@ -11,21 +11,23 @@
} }
} }
function _fetch(url, method, additionalHeaders, body) { function _fetch(options) {
var requestOptions = { var requestOptions = {
credentials: 'same-origin', credentials: 'same-origin',
headers: { }, headers: { },
method: method, method: options.method,
body: body, body: options.body,
}; };
Object.keys(additionalHeaders).forEach(function(headerKey) { Object.keys(options.headers).forEach(function(headerKey) {
requestOptions.headers[headerKey] = additionalHeaders[headerKey]; requestOptions.headers[headerKey] = options.headers[headerKey];
}); });
return fetch(url, requestOptions).then( return fetch(options.url, requestOptions).then(
function(response) { function(response) {
_responseInterceptors.forEach(function(interceptor) { interceptor(response); }); _responseInterceptors.forEach(function(interceptor) {
interceptor(response, options);
});
return Promise.resolve(response); return Promise.resolve(response);
}, },
function(error) { 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 { return {
get: function(url, headers) { get: function(args) {
return _fetch(url, 'GET', headers); args.method = 'GET';
return _fetch(args);
}, },
post: function(url, headers, body) { post: function(args) {
return _fetch(url, 'POST', headers, body); args.method = 'POST';
return _fetch(args);
}, },
addResponseInterceptor: addResponseInterceptor, 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);
})(); })();

View File

@ -96,21 +96,21 @@
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE; headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
} }
HttpClient.post(url, headers, body) HttpClient.post({
.then(function(response) { url: url,
if (response.headers.get("content-type").indexOf("application/json") !== -1) {// checking response header headers: headers,
return response.json(); body: body,
} else { accept: HttpClient.ACCEPT.JSON,
throw new TypeError('Unexpected Content-Type. Expected Content-Type: "application/json". Requested URL:' + url + '"'); }).then(function(response) {
} return response.json();
}).then(function(response) { }).then(function(response) {
processResponse(response[0]); processResponse(response[0]);
}).catch(function(error) { }).catch(function(error) {
var failureMessage = I18n.get('asyncFormFailure'); var failureMessage = I18n.get('asyncFormFailure');
processResponse({ content: failureMessage }); processResponse({ content: failureMessage });
element.classList.remove(ASYNC_FORM_LOADING_CLASS); element.classList.remove(ASYNC_FORM_LOADING_CLASS);
}); });
} }
return init(); return init();

View File

@ -35,6 +35,7 @@
var pageLinks = []; var pageLinks = [];
var pagesizeForm; var pagesizeForm;
var scrollTable; var scrollTable;
var cssIdPrefix = '';
var tableFilterInputs = { var tableFilterInputs = {
search: [], search: [],
@ -48,12 +49,18 @@
throw new Error('Async Table utility cannot be setup without an element!'); throw new Error('Async Table utility cannot be setup without an element!');
} }
if (element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
return false;
}
// param asyncTableDbHeader // param asyncTableDbHeader
if (element.dataset.asyncTableDbHeader !== undefined) { if (element.dataset.asyncTableDbHeader !== undefined) {
asyncTableHeader = element.dataset.asyncTableDbHeader; 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 // find scrolltable wrapper
scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR); scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
@ -96,7 +103,7 @@
} }
function setupPagination() { function setupPagination() {
var pagination = element.querySelector('#' + asyncTableId + '-pagination'); var pagination = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagination');
if (pagination) { if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) { pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link }; return { element: link };
@ -122,7 +129,7 @@
function setupPageSizeSelect() { function setupPageSizeSelect() {
// pagesize form // pagesize form
pagesizeForm = element.querySelector('#' + asyncTableId + '-pagesize-form'); pagesizeForm = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagesize-form');
if (pagesizeForm) { if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]'); var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
@ -200,11 +207,15 @@
var focusedInput = tableFilterForm.querySelector(':focus, :active'); var focusedInput = tableFilterForm.querySelector(':focus, :active');
// focus previously focused input // focus previously focused input
if (focusedInput) { if (focusedInput && focusedInput.selectionStart !== null) {
var selectionStart = focusedInput.selectionStart; 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) { callback = function(wrapper) {
var toBeFocused = wrapper.querySelector('#' + focusId); var idPrefix = getLocalStorageParameter('cssIdPrefix');
var toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
if (toBeFocused) { if (toBeFocused) {
toBeFocused.focus(); toBeFocused.focus();
toBeFocused.selectionStart = selectionStart; toBeFocused.selectionStart = selectionStart;
@ -319,41 +330,56 @@
[asyncTableHeader]: asyncTableId [asyncTableHeader]: asyncTableId
}; };
HttpClient.get(url, headers).then( HttpClient.get({
response => HttpClient.parseHTML(response, element.id) url: url,
).then(function(data) { headers: headers,
accept: HttpClient.ACCEPT.TEXT_HTML,
}).then(function(response) {
return HtmlHelpers.parseResponse(response);
}).then(function(response) {
setLocalStorageParameter('currentTableUrl', url.href); setLocalStorageParameter('currentTableUrl', url.href);
// reset table // reset table
removeListeners(); removeListeners();
element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
// update table with new // update table with new
updateWrapperContents(data); updateWrapperContents(response);
if (callback && typeof callback === 'function') { if (UtilRegistry) {
callback(element); 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) { }).catch(function(err) {
console.error(err); console.error(err);
}).finally(function() {
element.classList.remove(ASYNC_TABLE_LOADING_CLASS); element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
}); });
} }
function updateWrapperContents(newHtml) { function updateWrapperContents(response) {
var newPage = document.createElement('div'); var newPage = document.createElement('div');
newPage.appendChild(newHtml); newPage.appendChild(response.element);
var newWrapperContents = newPage.querySelector('#' + element.id + '__' + element.id); var newWrapperContents = newPage.querySelector('#' + response.idPrefix + element.id);
element.innerHTML = newWrapperContents.innerHTML; element.innerHTML = newWrapperContents.innerHTML;
if (UtilRegistry) {
UtilRegistry.setupAll(element);
}
} }
return init(); 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) { function setLocalStorageParameter(key, value) {
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {}; var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
if (value !== null) { if (value !== null) {

View File

@ -30,6 +30,10 @@
throw new Error('Check All utility cannot be setup without an element!'); throw new Error('Check All utility cannot be setup without an element!');
} }
if (element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
return false;
}
gatherColumns(); gatherColumns();
setupCheckAllCheckbox(); setupCheckAllCheckbox();

View File

@ -38,6 +38,10 @@
throw new Error('Mass Input utility cannot be setup without an element!'); 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 || '_'; massInputId = element.dataset.massInputIdent || '_';
massInputForm = element.closest('form'); massInputForm = element.closest('form');
@ -121,17 +125,19 @@
if (enctype !== 'multipart/form-data') if (enctype !== 'multipart/form-data')
headers['Content-Type'] = enctype; headers['Content-Type'] = enctype;
requestFn( requestFn({
url, url: url,
headers, headers: headers,
requestBody, body: requestBody,
).then(response => HttpClient.parseHTML(response, element.id) accept: HttpClient.ACCEPT.TEXT_HTML,
).then(function(response) { }).then(function(response) {
processResponse(response); return HtmlHelpers.parseResponse(response);
if (isAddCell) { }).then(function(response) {
reFocusAddCell(); processResponse(response.element);
} if (isAddCell) {
}); reFocusAddCell();
}
});
} }
}; };
} }
@ -162,9 +168,9 @@
button.removeEventListener('click', massInputFormSubmitHandler); button.removeEventListener('click', massInputFormSubmitHandler);
} }
function processResponse(response) { function processResponse(responseElement) {
element.innerHTML = ""; element.innerHTML = "";
element.appendChild(response); element.appendChild(responseElement);
reset(); reset();

View File

@ -167,15 +167,18 @@
throw new Error('HttpClient not found! Can\'t fetch modal content from ' + url); throw new Error('HttpClient not found! Can\'t fetch modal content from ' + url);
} }
HttpClient.get(url, MODAL_HEADERS) HttpClient.get({
.then(response => HttpClient.parseHTML(response, element.id)) url: url,
.then(processResponse); headers: MODAL_HEADERS,
accept: HttpClient.ACCEPT.TEXT_HTML,
}).then(function(response) {
return HtmlHelpers.parseResponse(response);
}).then(function(response) {
processResponse(response.element);
});
} }
function processResponse(responseFrag) { function processResponse(responseElement) {
var responseElement = document.createElement('div');
responseElement.appendChild(responseFrag);
var modalContent = document.createElement('div'); var modalContent = document.createElement('div');
modalContent.classList.add(MODAL_CONTENT_CLASS); modalContent.classList.add(MODAL_CONTENT_CLASS);