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:
parent
bfd35dbc5c
commit
67e472fa5e
@ -19,6 +19,8 @@ export class UtilRegistry {
|
|||||||
* element: HTMLElement | element the util is applied to
|
* element: HTMLElement | element the util is applied to
|
||||||
* destroy: Function | function to destroy the util and remove any listeners
|
* 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
|
* @param util Object Utility that should be added to the registry
|
||||||
*/
|
*/
|
||||||
register(util) {
|
register(util) {
|
||||||
@ -52,6 +54,7 @@ export class UtilRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._registeredUtils.forEach((util) => this.setup(util, scope));
|
this._registeredUtils.forEach((util) => this.setup(util, scope));
|
||||||
|
this._activeUtilInstances.forEach((instance) => typeof instance.start === 'function' && instance.start());
|
||||||
}
|
}
|
||||||
|
|
||||||
setup(util, scope = document.body) {
|
setup(util, scope = document.body) {
|
||||||
|
|||||||
@ -81,9 +81,6 @@ export class AsyncTable {
|
|||||||
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setupSortableHeaders();
|
|
||||||
this._setupPagination();
|
|
||||||
this._setupPageSizeSelect();
|
|
||||||
this._setupTableFilter();
|
this._setupTableFilter();
|
||||||
|
|
||||||
this._processStorage();
|
this._processStorage();
|
||||||
@ -95,11 +92,18 @@ export class AsyncTable {
|
|||||||
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this._startSortableHeaders();
|
||||||
|
this._startPagination();
|
||||||
|
this._startPageSizeSelect();
|
||||||
|
this._startTableFilter();
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
console.log('TBD: Destroy AsyncTable');
|
console.log('TBD: Destroy AsyncTable');
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupSortableHeaders() {
|
_startSortableHeaders() {
|
||||||
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
|
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
|
||||||
.map((th) => ({ element: th }));
|
.map((th) => ({ element: th }));
|
||||||
|
|
||||||
@ -112,7 +116,7 @@ export class AsyncTable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupPagination() {
|
_startPagination() {
|
||||||
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
|
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
|
||||||
if (pagination) {
|
if (pagination) {
|
||||||
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
|
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
|
||||||
@ -136,7 +140,7 @@ export class AsyncTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupPageSizeSelect() {
|
_startPageSizeSelect() {
|
||||||
// pagesize form
|
// pagesize form
|
||||||
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-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);
|
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
||||||
if (tableFilterForm) {
|
if (tableFilterForm) {
|
||||||
this._gatherTableFilterInputs(tableFilterForm);
|
this._gatherTableFilterInputs(tableFilterForm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startTableFilter() {
|
||||||
|
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
||||||
|
if (tableFilterForm) {
|
||||||
this._addTableFilterEventListeners(tableFilterForm);
|
this._addTableFilterEventListeners(tableFilterForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,20 +180,7 @@ export class AsyncTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_addTableFilterEventListeners(tableFilterForm) {
|
_addTableFilterEventListeners(tableFilterForm) {
|
||||||
this._tableFilterInputs.search.forEach((input) => {
|
[...this._tableFilterInputs.search, ...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.input.forEach((input) => {
|
|
||||||
input.submitLockObserver = new MutationObserver((mutations, observer) => {
|
input.submitLockObserver = new MutationObserver((mutations, observer) => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
// if the submit lock has been released, trigger an update and disconnect this observer
|
// 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 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)) {
|
if (!submitLocked && (input.value.length === 0 || input.value.length > 2)) {
|
||||||
this._updateFromTableFilter(tableFilterForm);
|
this._updateFromTableFilter(tableFilterForm);
|
||||||
} else if (submitLocked) {
|
} else if (submitLocked) {
|
||||||
@ -208,6 +206,7 @@ export class AsyncTable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, INPUT_DEBOUNCE);
|
}, INPUT_DEBOUNCE);
|
||||||
|
|
||||||
input.addEventListener('input', debouncedInput);
|
input.addEventListener('input', debouncedInput);
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
// set flag to ignore any currently pending requests (not debounced)
|
// 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);
|
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href);
|
||||||
|
|
||||||
// create new FormData and format any date values
|
// 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()) {
|
for (var k of url.searchParams.keys()) {
|
||||||
url.searchParams.delete(k);
|
url.searchParams.delete(k);
|
||||||
|
|||||||
@ -100,13 +100,13 @@ export class Datepicker {
|
|||||||
// store the previously set type to select the input format
|
// store the previously set type to select the input format
|
||||||
this.elementType = this._element.getAttribute('type');
|
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
|
// get all relevant config options for this datepicker type
|
||||||
const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
|
const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
|
||||||
const datepickerConfig = DATEPICKER_CONFIG[this.elementType];
|
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
|
// additional position config (optional data-datepicker-position attribute in html) that can specialize the global config
|
||||||
const datepickerPosition = this._element.dataset.datepickerPosition;
|
const datepickerPosition = this._element.dataset.datepickerPosition;
|
||||||
if (datepickerPosition) {
|
if (datepickerPosition) {
|
||||||
@ -155,23 +155,9 @@ export class Datepicker {
|
|||||||
|
|
||||||
// mark the form input element as initialized
|
// mark the form input element as initialized
|
||||||
this._element.classList.add(DATEPICKER_INITIALIZED_CLASS);
|
this._element.classList.add(DATEPICKER_INITIALIZED_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
// create a mutation observer that observes the datepicker instance class and sets
|
start() {
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setDatepickerDate = () => {
|
const setDatepickerDate = () => {
|
||||||
// try to parse the current input element value with fancy and internal format string
|
// 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]);
|
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
|
// 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 });
|
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
|
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
|
||||||
window.addEventListener('focusout', event => {
|
window.addEventListener('focusout', event => {
|
||||||
const hasFocus = event.relatedTarget !== null;
|
const hasFocus = event.relatedTarget !== null;
|
||||||
|
|||||||
Reference in New Issue
Block a user