fradrive/frontend/src/utils/async-table/async-table.js
2019-07-03 11:59:02 +02:00

377 lines
12 KiB
JavaScript

import { Utility } from '../../core/utility';
import { HttpClient } from '../../services/http-client/http-client';
import * as debounce from 'lodash.debounce';
import './async-table-filter.scss';
import './async-table.scss';
const INPUT_DEBOUNCE = 600;
const HEADER_HEIGHT = 80;
const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
const ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
const ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
const ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
const ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
@Utility({
selector: '[uw-async-table]',
})
export class AsyncTable {
_element;
_app;
_asyncTableHeader;
_asyncTableId;
_ths = [];
_pageLinks = [];
_pagesizeForm;
_scrollTable;
_cssIdPrefix = '';
_tableFilterInputs = {
search: [],
input: [],
change: [],
select: [],
};
_ignoreRequest = false;
constructor(element, app) {
if (!element) {
throw new Error('Async Table utility cannot be setup without an element!');
}
if (!app) {
throw new Error('Async Table utility cannot be setup without an app!');
}
this._element = element;
this._app = app;
if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
return false;
}
// param asyncTableDbHeader
if (this._element.dataset.asyncTableDbHeader !== undefined) {
this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
}
const table = this._element.querySelector('table');
if (!table) {
throw new Error('Async Table utility needs a <table> in its element!');
}
const rawTableId = table.id;
this._cssIdPrefix = findCssIdPrefix(rawTableId);
this._asyncTableId = rawTableId.replace(this._cssIdPrefix, '');
// find scrolltable wrapper
this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
if (!this._scrollTable) {
throw new Error('Async Table cannot be set up without a scrolltable element!');
}
this._setupSortableHeaders();
this._setupPagination();
this._setupPageSizeSelect();
this._setupTableFilter();
this._processLocalStorage();
// clear currentTableUrl from previous requests
setLocalStorageParameter('currentTableUrl', null);
// mark initialized
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
}
destroy() {
console.log('TBD: Destroy AsyncTable');
}
_setupSortableHeaders() {
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable'))
.map((th) => ({ element: th }));
this._ths.forEach((th) => {
th.clickHandler = (event) => {
setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft);
this._linkClickHandler(event);
};
th.element.addEventListener('click', th.clickHandler);
});
}
_setupPagination() {
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
if (pagination) {
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
.map((link) => ({ element: link }));
this._pageLinks.forEach((link) => {
link.clickHandler = (event) => {
const tableBoundingRect = this._scrollTable.getBoundingClientRect();
if (tableBoundingRect.top < HEADER_HEIGHT) {
const scrollTo = {
top: (this._scrollTable.offsetTop || 0) - HEADER_HEIGHT,
left: this._scrollTable.offsetLeft || 0,
behavior: 'smooth',
};
setLocalStorageParameter('scrollTo', scrollTo);
}
this._linkClickHandler(event);
};
link.element.addEventListener('click', link.clickHandler);
});
}
}
_setupPageSizeSelect() {
// pagesize form
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');
if (this._pagesizeForm) {
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
pagesizeSelect.addEventListener('change', this._changePagesizeHandler);
}
}
_setupTableFilter() {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) {
this._gatherTableFilterInputs(tableFilterForm);
this._addTableFilterEventListeners(tableFilterForm);
}
}
_gatherTableFilterInputs(tableFilterForm) {
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach((input) => {
this._tableFilterInputs.search.push(input);
});
Array.from(tableFilterForm.querySelectorAll('input[type="text"]')).forEach((input) => {
this._tableFilterInputs.input.push(input);
});
Array.from(tableFilterForm.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach((input) => {
this._tableFilterInputs.change.push(input);
});
Array.from(tableFilterForm.querySelectorAll('select')).forEach((input) => {
this._tableFilterInputs.select.push(input);
});
}
_addTableFilterEventListeners(tableFilterForm) {
this._tableFilterInputs.search.forEach((input) => {
const debouncedInput = debounce(() => {
if (input.value.length === 0 || input.value.length > 2) {
this._updateFromTableFilter(tableFilterForm);
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
input.addEventListener('input', () => {
// set flag to ignore any currently pending requests (not debounced)
this._ignoreRequest = true;
});
});
this._tableFilterInputs.input.forEach((input) => {
const debouncedInput = debounce(() => {
if (input.value.length === 0 || input.value.length > 2) {
this._updateFromTableFilter(tableFilterForm);
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
input.addEventListener('input', () => {
// set flag to ignore any currently pending requests (not debounced)
this._ignoreRequest = true;
});
});
this._tableFilterInputs.change.forEach((input) => {
input.addEventListener('change', () => {
this._updateFromTableFilter(tableFilterForm);
});
});
this._tableFilterInputs.select.forEach((input) => {
input.addEventListener('change', () => {
this._updateFromTableFilter(tableFilterForm);
});
});
tableFilterForm.addEventListener('submit', (event) =>{
event.preventDefault();
this._updateFromTableFilter(tableFilterForm);
});
}
_updateFromTableFilter(tableFilterForm) {
const url = this._serializeTableFilterToURL(tableFilterForm);
let callback = null;
const focusedInput = tableFilterForm.querySelector(':focus, :active');
// focus previously focused input
if (focusedInput && focusedInput.selectionStart !== null) {
const selectionStart = focusedInput.selectionStart;
// remove the following part of the id to get rid of the random
// (yet somewhat structured) prefix we got from nudging.
const prefix = findCssIdPrefix(focusedInput.id);
const focusId = focusedInput.id.replace(prefix, '');
callback = function(wrapper) {
const idPrefix = getLocalStorageParameter('cssIdPrefix');
const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
if (toBeFocused) {
toBeFocused.focus();
toBeFocused.selectionStart = selectionStart;
}
};
}
this._ignoreRequest = false;
this._updateTableFrom(url, callback);
}
_serializeTableFilterToURL(tableFilterForm) {
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
const formData = new FormData(tableFilterForm);
for (var k of url.searchParams.keys()) {
url.searchParams.delete(k);
}
for (var kv of formData.entries()) {
url.searchParams.append(kv[0], kv[1]);
}
return url;
}
_processLocalStorage() {
const scrollTo = getLocalStorageParameter('scrollTo');
if (scrollTo && this._scrollTable) {
window.scrollTo(scrollTo);
}
setLocalStorageParameter('scrollTo', null);
const horizPos = getLocalStorageParameter('horizPos');
if (horizPos && this._scrollTable) {
this._scrollTable.scrollLeft = horizPos;
}
setLocalStorageParameter('horizPos', null);
}
_removeListeners() {
this._ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
this._pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (this._pagesizeForm) {
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
pagesizeSelect.removeEventListener('change', this._changePagesizeHandler);
}
}
_linkClickHandler = (event) => {
event.preventDefault();
let url = this._getClickDestination(event.target);
if (!url.match(/^http/)) {
url = window.location.origin + window.location.pathname + url;
}
this._updateTableFrom(url);
}
_getClickDestination(el) {
if (!el.matches('a') && !el.querySelector('a')) {
return '';
}
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
}
_changePagesizeHandler = () => {
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
const formData = new FormData(this._pagesizeForm);
for (var k of url.searchParams.keys()) {
url.searchParams.delete(k);
}
for (var kv of formData.entries()) {
url.searchParams.append(kv[0], kv[1]);
}
this._updateTableFrom(url.href);
}
// fetches new sorted element from url with params and replaces contents of current element
_updateTableFrom(url, callback) {
this._element.classList.add(ASYNC_TABLE_LOADING_CLASS);
const headers = {
'Accept': HttpClient.ACCEPT.TEXT_HTML,
[this._asyncTableHeader]: this._asyncTableId,
};
this._app.httpClient.get({
url: url,
headers: headers,
}).then(
(response) => this._app.htmlHelpers.parseResponse(response),
).then((response) => {
// check if request should be ignored
if (this._ignoreRequest) {
return false;
}
setLocalStorageParameter('currentTableUrl', url.href);
// reset table
this._removeListeners();
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
// update table with new
this._element.innerHTML = response.element.innerHTML;
this._app.utilRegistry.setupAll(this._element);
if (callback && typeof callback === 'function') {
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
callback(this._element);
setLocalStorageParameter('cssIdPrefix', '');
}
}).catch((err) => console.error(err)
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
}
}
// returns any random nudged prefix found in the given id
function findCssIdPrefix(id) {
const matcher = /r\d*?__/;
const maybePrefix = id.match(matcher);
if (maybePrefix && maybePrefix[0]) {
return maybePrefix[0];
}
return '';
}
function setLocalStorageParameter(key, value) {
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
if (value !== null) {
currentLSState[key] = value;
} else {
delete currentLSState[key];
}
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
}
function getLocalStorageParameter(key) {
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
return currentLSState[key];
}