feat(frontend): split up util registry

split up setup of utils (into (DOM) setup and (event listener) start
steps); moved event listener registration of datepicker and async-table
util to start method(s); small diverse fixes and refactoring.
FIXME: enter in datepicker inputs still cause HTTP request loop
This commit is contained in:
Sarah Vaupel 2019-11-20 17:44:39 +01:00 committed by Gregor Kleen
parent bfd35dbc5c
commit 67e472fa5e
3 changed files with 47 additions and 41 deletions

View File

@ -19,6 +19,8 @@ export class UtilRegistry {
* element: HTMLElement | element the util is applied to
* destroy: Function | function to destroy the util and remove any listeners
*
* (optional) start function for registering event listeners
*
* @param util Object Utility that should be added to the registry
*/
register(util) {
@ -52,6 +54,7 @@ export class UtilRegistry {
}
this._registeredUtils.forEach((util) => this.setup(util, scope));
this._activeUtilInstances.forEach((instance) => typeof instance.start === 'function' && instance.start());
}
setup(util, scope = document.body) {

View File

@ -81,9 +81,6 @@ export class AsyncTable {
throw new Error('Async Table cannot be set up without a scrolltable element!');
}
this._setupSortableHeaders();
this._setupPagination();
this._setupPageSizeSelect();
this._setupTableFilter();
this._processStorage();
@ -95,11 +92,18 @@ export class AsyncTable {
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
}
start() {
this._startSortableHeaders();
this._startPagination();
this._startPageSizeSelect();
this._startTableFilter();
}
destroy() {
console.log('TBD: Destroy AsyncTable');
}
_setupSortableHeaders() {
_startSortableHeaders() {
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
.map((th) => ({ element: th }));
@ -112,7 +116,7 @@ export class AsyncTable {
});
}
_setupPagination() {
_startPagination() {
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
if (pagination) {
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
@ -136,7 +140,7 @@ export class AsyncTable {
}
}
_setupPageSizeSelect() {
_startPageSizeSelect() {
// pagesize form
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');
@ -150,6 +154,12 @@ export class AsyncTable {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) {
this._gatherTableFilterInputs(tableFilterForm);
}
}
_startTableFilter() {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) {
this._addTableFilterEventListeners(tableFilterForm);
}
}
@ -170,20 +180,7 @@ export class AsyncTable {
}
_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) => {
[...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => {
input.submitLockObserver = new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
// if the submit lock has been released, trigger an update and disconnect this observer
@ -196,7 +193,8 @@ export class AsyncTable {
});
const debouncedInput = debounce(() => {
const submitLocked = input.getAttribute(ATTR_SUBMIT_LOCKED) === 'true';
const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED);
const submitLocked = submitLockedAttr === 'true' || submitLockedAttr === null;
if (!submitLocked && (input.value.length === 0 || input.value.length > 2)) {
this._updateFromTableFilter(tableFilterForm);
} else if (submitLocked) {
@ -208,6 +206,7 @@ export class AsyncTable {
});
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
input.addEventListener('input', () => {
// set flag to ignore any currently pending requests (not debounced)
@ -262,7 +261,7 @@ export class AsyncTable {
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values
const formData = Datepicker.unformatAll(this._massInputForm, new FormData(tableFilterForm));
const formData = Datepicker.unformatAll(tableFilterForm, new FormData(tableFilterForm));
for (var k of url.searchParams.keys()) {
url.searchParams.delete(k);

View File

@ -100,13 +100,13 @@ export class Datepicker {
// store the previously set type to select the input format
this.elementType = this._element.getAttribute('type');
// manually set the type attribute to text because datepicker handles displaying the date
this._element.setAttribute('type', 'text');
// get all relevant config options for this datepicker type
const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
const datepickerConfig = DATEPICKER_CONFIG[this.elementType];
// manually set the type attribute to text because datepicker handles displaying the date
this._element.setAttribute('type', 'text');
// additional position config (optional data-datepicker-position attribute in html) that can specialize the global config
const datepickerPosition = this._element.dataset.datepickerPosition;
if (datepickerPosition) {
@ -155,23 +155,9 @@ export class Datepicker {
// mark the form input element as initialized
this._element.classList.add(DATEPICKER_INITIALIZED_CLASS);
}
// create a mutation observer that observes the datepicker instance class and sets
// the datepicker-open DOM attribute of the input element if the datepicker has been opened
const datepickerInstanceObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) {
this._element.setAttribute(ATTR_DATEPICKER_OPEN, true);
break;
}
}
});
datepickerInstanceObserver.observe(this.datepickerInstance.dt, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
});
start() {
const setDatepickerDate = () => {
// try to parse the current input element value with fancy and internal format string
const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]);
@ -190,6 +176,24 @@ export class Datepicker {
// change the selected date in the tail.datetime instance if the value of the input element is changed
this._element.addEventListener('change', setDatepickerDate, { once: true });
// create a mutation observer that observes the datepicker instance class and sets
// the datepicker-open DOM attribute of the input element if the datepicker has been opened
const datepickerInstanceObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) {
this._element.setAttribute(ATTR_DATEPICKER_OPEN, true);
break;
}
}
});
datepickerInstanceObserver.observe(this.datepickerInstance.dt, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
});
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
window.addEventListener('focusout', event => {
const hasFocus = event.relatedTarget !== null;