From 67e472fa5e09ed2068d477ef12b12adb0ca98c4f Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Wed, 20 Nov 2019 17:44:39 +0100 Subject: [PATCH] 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 --- .../services/util-registry/util-registry.js | 3 ++ frontend/src/utils/async-table/async-table.js | 43 +++++++++---------- frontend/src/utils/form/datepicker.js | 42 ++++++++++-------- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index c6e866adf..29cfb380b 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -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) { diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index dbb223232..31ab5f9bb 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -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); diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index ee7b3f5a1..1f2603cca 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -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;