diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a504a9a..5e9f80e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,65 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [25.22.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.3...v25.22.4) (2021-10-26) + + +### Bug Fixes + +* **routes:** make access to workflows free ([29c54db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29c54db06f01659a3a6419009964a85cd11d5441)) + +## [25.22.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.2...v25.22.3) (2021-10-21) + + +### Bug Fixes + +* **navigation:** always link workflows nav to instances ([adf9709](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adf9709567d9a320f2c17d3c5dde940c2f9d8862)) + +## [25.22.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.1...v25.22.2) (2021-10-13) + +## [25.22.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.22.0...v25.22.1) (2021-10-02) + + +### Bug Fixes + +* **course-admins:** display course admins as admins instead of assistants ([f1fe444](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1fe4447fbe7e96e55aaf284c7083338b5135ab6)) + +## [25.22.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.21.0...v25.22.0) (2021-08-30) + + +### Features + +* **event-manager:** added method to register a list of listeners ([1a8fb23](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1a8fb230441f73b9b1fe593f4df1005a06d9628e)) +* **event-manager:** mutation observers can be managed via the event manager ([34b4f48](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34b4f48386c8fff4569b12eb3f7919e7c77d33c0)) +* **http-client:** added possibility to remove specific interceptors ([0823df3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0823df33b5f157af290aca9a65f1df429048e53b)) +* **tutoriumsdaten:** application restore ([d4a73e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4a73e699a399b02cadcea03e614c789671ee6d1)) +* **tutoriumsdaten:** firts draft ([e972788](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e972788f540a9ce6c3fdf841313057b62a579d72)) +* **tutoriumsdaten:** termin ([ebcb234](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ebcb23429ff09c56ba4a00a6cb8f082ee83e1fe8)) +* **util_registry:** impelmented destroyAll(scope) method in the utilRegistry ([f1ef2e5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1ef2e5ec776ad8a1fb7737eb0ed4f79218afd61)) +* implemented an event manager ([c1c3536](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c1c35369d1ae017971e6d8cbafc06844f02fd00d)) + + +### Bug Fixes + +* **async-form:** destroy all after response is processed ([14a16c7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14a16c7283483ff22ce22b070a430a61d24a7f35)) +* **communication-recipients:** fixed undefined error with context and a few minor issues ([03ac803](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03ac80342e0f3fcb1db2adf34f86a2c0d8fabf0f)) +* **enter-is-tab.js:** implemented destroy method in enter-is-tab Util ([d1b9952](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1b995269060c670d85f715671bfc9947b9f3e9a)) +* **hide-columns:** removed clear storage from destroy method ([b3b0d65](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3b0d6585068ecbc665e819b31c94f4d96a0fec5)) +* **hide-colums:** small fix ([50a3ac1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50a3ac1790f26c1e6f983d3687352d7811173110)) +* **http-client:** strict equality check ([5078b56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5078b56c16513f3b289bc4824ce0c7914b1a220d)) +* **interactive-fieldset:** small fix ([4c2c683](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c2c68327e75f5f51271853159c232fdd7bba21e)) +* **navigate-away-promp:** removed unnecessary destroyAll ([7a07159](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a0715906c8336ed64bbfabdf614746ade9f661f)) +* **password:** added cleanUP ([204ce39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/204ce39f7c2f9c405971aa59da068a5389dda417)) +* **show-hide:** storage manager is not cleared ([9453689](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/945368972da36f4ff0da99f38c71ea9a1c7157d7)) +* **storage-manager:** clear is working without options as well ([f1c50e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f1c50e137f4f11140c1171dd9d86bc5511b9e771)) +* **tooltips:** correct regex match ([c8d36ea](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8d36ea52dbef0a2e1aaec6aedaf7edd524975dc)) +* **tooltips:** removed else case ([8d0241e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8d0241e727efb5051e5848f2a0c835d7a8d3ae6b)) +* **util-registry:** filtering activeUtilInstances when a util is destroyed ([5b4ac75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b4ac7587438e7adb21e0ff3d9d629b6ff00263a)) +* **util-registry:** handle negative indices correctly ([cbc03f5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbc03f57c56df9c96a84da15a68d26904b75a65f)) +* fixed a few minor issues ([6320cd9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6320cd927a84445f056dc782fb440d276cb26009)) +* prompt not shwowing up after submit/close ([abe8415](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/abe84156d508dca8fce549b24d5902d24afc0dbf)) +* smaller fixes and typos ([1f978e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1f978e65a82a91fb728a7ee2970a4fd9e6beb521)) + ## [25.21.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.20.2...v25.21.0) (2021-08-20) diff --git a/frontend/src/lib/event-manager/event-manager.js b/frontend/src/lib/event-manager/event-manager.js new file mode 100644 index 000000000..a69965a6f --- /dev/null +++ b/frontend/src/lib/event-manager/event-manager.js @@ -0,0 +1,103 @@ + +export const EVENT_TYPE = { + CLICK : 'click', + KEYDOWN : 'keydown', + INVALID : 'invalid', + CHANGE : 'change', + MOUSE_OVER : 'mouseover', + MOUSE_OUT : 'mouseout', + SUBMIT : 'submit', + INPUT : 'input', + FOCUS_OUT : 'focusout', + BEFOREUNLOAD : 'beforeunload', + HASH_CHANGE : 'hashchange', +}; + + + +export class EventManager { + _registeredListeners; + _mutationObservers; + + + constructor() { + this._registeredListeners = []; + this._mutationObservers = []; + } + + registerNewListener(eventWrapper) { + this._debugLog('registerNewListener', eventWrapper); + let element = eventWrapper.element; + element.addEventListener(eventWrapper.eventType, eventWrapper.eventHandler, eventWrapper.options); + this._registeredListeners.push(eventWrapper); + } + + registerListeners(eventWrappers) { + eventWrappers.forEach((eventWrapper) => this.registerNewListener(eventWrapper)); + } + + registerNewMutationObserver(callback, domNode, config) { + let observer = new MutationObserver(callback); + observer.observe(domNode, config); + this._mutationObservers.push(observer); + } + + removeAllEventListenersFromUtil() { + this._debugLog('removeAllEventListenersFromUtil',); + for (let eventWrapper of this._registeredListeners) { + let element = eventWrapper.element; + element.removeEventListener(eventWrapper.eventType, eventWrapper.eventHandler); + } + this._registeredListeners = []; + } + + removeAllObserversFromUtil() { + this._mutationObservers.forEach((observer) => observer.disconnect()); + this.mutationObservers = []; + } + + cleanUp() { + this.removeAllObserversFromUtil(); + this.removeAllEventListenersFromUtil(); + } + + + + _debugLog() {} + //_debugLog(fName, ...args) { + // console.log(`[DEBUGLOG] EventManager.${fName}`, { args: args, instance: this }); + //} +} + +export class EventWrapper { + _eventType; + _eventHandler; + _element; + _options + + constructor(_eventType, _eventHandler, _element, _options) { + if(!_eventType || !_eventHandler || !_element) { + throw new Error('Not enough arguments!'); + } + this._eventType = _eventType; + this._eventHandler = _eventHandler; + this._element = _element; + this._options = _options; + } + + get eventType() { + return this._eventType; + } + + get eventHandler() { + return this._eventHandler; + } + + get element() { + return this._element; + } + + get options() { + return this._options; + } +} \ No newline at end of file diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index f2ee68589..d9ef366ec 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -186,14 +186,14 @@ export class StorageManager { } } - clear(options) { + clear(options=this._options) { this._debugLog('clear', options); if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) { throw new Error('StorageManager.clear called with unsupported location option'); } - const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING; + const locations = ((options !== undefined) && options.location !== undefined)? [options.location] : this._location_shadowing; for (const location of locations) { switch (location) { @@ -204,7 +204,10 @@ export class StorageManager { case LOCATION.WINDOW: return this._clearWindow(); case LOCATION.HISTORY: - return this._clearHistory(options && options.history); + if(options && options.history) + return this._clearHistory(options.history); + else + return; default: console.error('StorageManager.clear cannot clear with unsupported location'); } diff --git a/frontend/src/services/http-client/http-client.js b/frontend/src/services/http-client/http-client.js index 274f86cdb..81f72485e 100644 --- a/frontend/src/services/http-client/http-client.js +++ b/frontend/src/services/http-client/http-client.js @@ -15,6 +15,18 @@ export class HttpClient { } } + removeResponseInterceptor(interceptor) { + //performs a reference check. if the interceptor is bound, when adding it, the reference of the bound function needs to be the same when removing it later. + + if (typeof interceptor !== 'function') { + throw new Error(`Cannot remove Interceptor ${interceptor}, because it is not of type function`); + } + if(this._responseInterceptors.filter(el => el == interceptor).length === 0) { + throw new Error(`Could not find Response Interceptor ${interceptor}.`); + } + this._responseInterceptors = this._responseInterceptors.filter(el => el !== interceptor); + } + _baseUrl; setBaseUrl(baseUrl) { diff --git a/frontend/src/services/http-client/http-client.spec.js b/frontend/src/services/http-client/http-client.spec.js index a0f76584d..594f7096c 100644 --- a/frontend/src/services/http-client/http-client.spec.js +++ b/frontend/src/services/http-client/http-client.spec.js @@ -75,7 +75,7 @@ describe('HttpClient', () => { expect(httpClient._responseInterceptors.length).toBe(2); }); - describe('get called', () => { + describe('get called and removed', () => { let intercepted1; let intercepted2; const interceptors = { @@ -111,6 +111,14 @@ describe('HttpClient', () => { done(); }); }); + + it('can be removed', () => { + expect(httpClient._responseInterceptors.length).toBe(2); + httpClient.removeResponseInterceptor(interceptors.interceptor1); + expect(httpClient._responseInterceptors.length).toBe(1); + expect(() => {httpClient.removeResponseInterceptor(interceptors.interceptor1);}).toThrow(); + expect(httpClient._responseInterceptors.length).toBe(1); + }); }); }); }); diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 65e9326cc..2b0005a35 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -4,8 +4,8 @@ const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0; export class UtilRegistry { - _registeredUtils = new Array(); - _activeUtilInstances = new Array(); + _registeredUtilClasses = new Array(); //{utilClass} + _activeUtilInstancesWrapped = new Array(); //{utilClass, scope, element, instance} _appInstance; /** @@ -33,7 +33,7 @@ export class UtilRegistry { console.log('registering util "' + util.name + '"'); console.log({ util }); } - this._registeredUtils.push(util); + this._registeredUtilClasses.push(util); } deregister(name, destroy) { @@ -44,7 +44,7 @@ export class UtilRegistry { this._destroyUtilInstances(name); } - this._registeredUtils.splice(utilIndex, 1); + this._registeredUtilClasses.splice(utilIndex, 1); } } @@ -54,7 +54,7 @@ export class UtilRegistry { initAll(scope = document.body) { let startedInstances = new Array(); - const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat(); + const setupInstances = this._registeredUtilClasses.map((util) => this.setup(util, scope)).flat(); const orderedInstances = setupInstances.filter(_isStartOrdered); @@ -97,6 +97,20 @@ export class UtilRegistry { return startedInstances; } + destroyAll(scope = document.body) { + let utilsInScope = this._getUtilInstancesWithinScope(scope); + + utilsInScope.forEach((util) => { + if(DEBUG_MODE > 2) { + console.log('Destroying Util: ', {util}); + } + util.destroy(); + this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.filter(utilWrapped => { + return utilWrapped.element === util._element; + }); + }); + } + setup(util, scope = document.body) { if (DEBUG_MODE > 2) { console.log('setting up util', { util }); @@ -130,12 +144,12 @@ export class UtilRegistry { }); } - this._activeUtilInstances.push(...instances); + this._activeUtilInstancesWrapped.push(...instances); return instances; } find(name) { - return this._registeredUtils.find((util) => util.name === name); + return this._registeredUtilClasses.find((util) => util.name === name); } _findUtilElements(util, scope) { @@ -146,24 +160,33 @@ export class UtilRegistry { } _findUtilIndex(name) { - return this._registeredUtils.findIndex((util) => util.name === name); + return this._registeredUtilClasses.findIndex((util) => util.name === name); + } + + _getUtilInstancesWithinScope(scope) { + let utilInstances = []; + + for (let activeUtilInstance of this._activeUtilInstancesWrapped) { + let util = activeUtilInstance.util; + if(this._findUtilElements(util, scope).length > 0) { + utilInstances.push(activeUtilInstance.instance); + } + } + return utilInstances; } _destroyUtilInstances(name) { - this._activeUtilInstances + this._activeUtilInstancesWrapped .map((util, index) => ({ util: util, index: index, })) - .filter((activeUtil) => activeUtil.util.name === name) + .filter((activeUtil) => activeUtil.util.util.name === name) .forEach((activeUtil) => { // destroy util instance - activeUtil.util.destroy(); - delete this._activeUtilInstances[activeUtil.index]; + activeUtil.util.instance.destroy(); + this._activeUtilInstancesWrapped = this._activeUtilInstancesWrapped.splice(activeUtil.index, 1); }); - - // get rid of now empty array slots - this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); } } diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js index 07b9e2627..18506be99 100644 --- a/frontend/src/services/util-registry/util-registry.spec.js +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -24,24 +24,6 @@ describe('UtilRegistry', () => { }); }); - describe('deregister()', () => { - it('should remove util', () => { - // register util - utilRegistry.register(TestUtil1); - let foundUtil = utilRegistry.find(TestUtil1.name); - expect(foundUtil).toBeTruthy(); - - // deregister util - utilRegistry.deregister(TestUtil1.name); - foundUtil = utilRegistry.find(TestUtil1.name); - expect(foundUtil).toBeFalsy(); - }); - - it('should destroy util instances if requested', () => { - pending('TBD'); - }); - }); - describe('setup()', () => { it('should catch errors thrown by the utility', () => { @@ -107,6 +89,51 @@ describe('UtilRegistry', () => { }); }); + describe('deregister()', () => { + let testScope; + let testElement1; + let testElement2; + + beforeEach(() => { + testScope = document.createElement('div'); + testElement1 = document.createElement('div'); + testElement2 = document.createElement('div'); + testElement1.classList.add('util1'); + testElement2.classList.add('util1'); + testScope.appendChild(testElement1); + testScope.appendChild(testElement2); + }); + + it('should remove util', () => { + // register util + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + // deregister util + utilRegistry.deregister(TestUtil1.name); + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); + }); + + it('should destroy util instances if requested', () => { + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeTruthy(); + + utilRegistry.setup(TestUtil1, testScope); + let firstActiveUtil = utilRegistry._activeUtilInstancesWrapped[0]; + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(2); + expect(utilRegistry._activeUtilInstancesWrapped[0].element).toEqual(testElement1); + + spyOn(firstActiveUtil.instance, 'destroy'); + + utilRegistry.deregister(TestUtil1.name, true); + expect(utilRegistry._activeUtilInstancesWrapped[0]).toBeFalsy(); + expect(firstActiveUtil.instance.destroy).toHaveBeenCalled(); + }); + }); + describe('initAll()', () => { it('should setup all the utilities', () => { spyOn(utilRegistry, 'setup'); @@ -172,6 +199,44 @@ describe('UtilRegistry', () => { }); }); }); + + describe('destroyAll()', () => { + let testScope; + let testElement; + let firstUtil; + + beforeEach( () => { + testScope = document.createElement('div'); + testElement = document.createElement('div'); + testElement.classList.add('util3'); + testScope.appendChild(testElement); + + utilRegistry.register(TestUtil3); + utilRegistry.initAll(testScope); + + firstUtil = utilRegistry._activeUtilInstancesWrapped[0]; + spyOn(firstUtil.instance, 'destroy'); + }); + + it('Util should be destroyed', () => { + utilRegistry.destroyAll(testScope); + expect(utilRegistry._activeUtilInstancesWrapped.length).toBe(0); + expect(firstUtil.instance.destroy).toHaveBeenCalled(); + }); + + it('Util out of scope should not be destroyed', () => { + let outOfScope = document.createElement('div'); + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1); + + utilRegistry.destroyAll(outOfScope); + + expect(utilRegistry._activeUtilInstancesWrapped.length).toEqual(1); + expect(utilRegistry._activeUtilInstancesWrapped[0]).toBe(firstUtil); + expect(firstUtil.instance.destroy).not.toHaveBeenCalled(); + + }); + }); + }); // test utilities @@ -181,6 +246,8 @@ class TestUtil1 { this.element = element; this.app = app; } + + destroy() {} } @Utility({ selector: '#util2' }) @@ -190,6 +257,7 @@ class TestUtil2 { } class TestUtil3 { constructor() {} start() {} + destroy() {} } @Utility({ selector: '#throws' }) diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index dcecc915b..a88fd1f19 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './alerts.sass'; const ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; @@ -32,6 +33,9 @@ export class Alerts { _element; _app; + _eventManager; + _boundResponseInterceptor; + constructor(element, app) { if (!element) { throw new Error('Alerts util has to be called with an element!'); @@ -40,6 +44,9 @@ export class Alerts { this._element = element; this._app = app; + this._eventManager = new EventManager(); + this._boundResponseInterceptor = this._responseInterceptor.bind(this); + if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { return false; } @@ -47,21 +54,30 @@ export class Alerts { this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS); this._alertElements = this._gatherAlertElements(); - if (this._togglerElement) { - this._initToggler(); - } - - this._initAlerts(); - - // register http client interceptor to filter out Alerts Header - this._setupHttpInterceptor(); - // mark initialized this._element.classList.add(ALERTS_INITIALIZED_CLASS); } + start() { + if (this._togglerElement) { + this._initToggler(); + } + this._initAlerts(); + + // register http client interceptor to filter out Alerts Header + this._setupHttpInterceptor(); + } + destroy() { - console.log('TBD: Destroy Alert'); + this._eventManager.cleanUp(); + this._app.httpClient.removeResponseInterceptor(this._boundResponseInterceptor); + + if(this._alertElements) { + this._alertElements.forEach(element => element.remove()); + } + + if(this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) + this._element.classList.remove(ALERTS_INITIALIZED_CLASS); } _gatherAlertElements() { @@ -71,10 +87,12 @@ export class Alerts { } _initToggler() { - this._togglerElement.addEventListener('click', () => { + let clickListenerToggler = new EventWrapper(EVENT_TYPE.CLICK, () => { this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true)); this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); - }); + }, this._togglerElement); + + this._eventManager.registerNewListener(clickListenerToggler); } _initAlerts() { @@ -88,9 +106,11 @@ export class Alerts { } const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); - closeEl.addEventListener('click', () => { + const closeAlertEvent = new EventWrapper(EVENT_TYPE.CLICK, (() => { this._toggleAlert(alertElement); - }); + }).bind(this), closeEl); + + this._eventManager.registerNewListener(closeAlertEvent); if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) { window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000); @@ -118,7 +138,7 @@ export class Alerts { } _setupHttpInterceptor() { - this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this)); + this._app.httpClient.addResponseInterceptor(this._boundResponseInterceptor); } _elevateAlerts() { diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js index 0b4749e97..db14c7361 100644 --- a/frontend/src/utils/alerts/alerts.spec.js +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -1,8 +1,9 @@ -import { Alerts } from './alerts'; +import { Alerts, ALERTS_INITIALIZED_CLASS } from './alerts'; const MOCK_APP = { httpClient: { addResponseInterceptor: () => {}, + removeResponseInterceptor: () => {}, }, }; @@ -19,6 +20,12 @@ describe('Alerts', () => { expect(alerts).toBeTruthy(); }); + it('should destory alerts', () => { + alerts.destroy(); + expect(alerts._eventManager._registeredListeners.length).toBe(0); + expect(alerts._element.classList).not.toContain(ALERTS_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new Alerts(); diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js index 6bf425ffa..324fc3fd8 100644 --- a/frontend/src/utils/asidenav/asidenav.js +++ b/frontend/src/utils/asidenav/asidenav.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './asidenav.sass'; const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite'; @@ -15,6 +16,7 @@ export class Asidenav { _element; _asidenavSubmenus; + _eventManager; constructor(element) { if (!element) { @@ -23,6 +25,8 @@ export class Asidenav { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) { return false; } @@ -35,19 +39,24 @@ export class Asidenav { } destroy() { - this._asidenavSubmenus.forEach((union) => { - union.listItem.removeEventListener(union.hoverHandler); - }); + + this._eventManager.cleanUp(); + + if(this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) + this._element.classList.remove(ASIDENAV_INITIALIZED_CLASS); } _initFavoritesButton() { const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS); if (favoritesBtn) { - favoritesBtn.addEventListener('click', (event) => { + + const favoritesButtonEvent = new EventWrapper(EVENT_TYPE.CLICK, (event) => { favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS); this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS); event.preventDefault(); - }, true); + }, favoritesBtn, true); + + this._eventManager.registerNewListener(favoritesButtonEvent); } } @@ -62,7 +71,8 @@ export class Asidenav { this._asidenavSubmenus.forEach((union) => { union.hoverHandler = this._createMouseoverHandler(union); - union.listItem.addEventListener('mouseover', union.hoverHandler); + let currentHoverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, union.hoverHandler, union.listItem); + this._eventManager.registerNewListener(currentHoverEvent); }); } diff --git a/frontend/src/utils/asidenav/asidenav.spec.js b/frontend/src/utils/asidenav/asidenav.spec.js index bdc7aee68..3bd468b28 100644 --- a/frontend/src/utils/asidenav/asidenav.spec.js +++ b/frontend/src/utils/asidenav/asidenav.spec.js @@ -1,4 +1,4 @@ -import { Asidenav } from './asidenav'; +import { Asidenav, ASIDENAV_INITIALIZED_CLASS } from './asidenav'; describe('Asidenav', () => { @@ -13,6 +13,12 @@ describe('Asidenav', () => { expect(asidenav).toBeTruthy(); }); + it('should destory asidenav', () => { + asidenav.destroy(); + expect(asidenav._eventManager._registeredListeners.length).toBe(0); + expect(asidenav._element.classList).not.toContain(ASIDENAV_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new Asidenav(); diff --git a/frontend/src/utils/async-form/async-form.js b/frontend/src/utils/async-form/async-form.js index 6c0da95c0..482cdb634 100644 --- a/frontend/src/utils/async-form/async-form.js +++ b/frontend/src/utils/async-form/async-form.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { Datepicker } from '../form/datepicker'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './async-form.sass'; const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized'; @@ -20,6 +21,8 @@ export class AsyncForm { _element; _app; + _eventManager; + constructor(element, app) { if (!element) { throw new Error('Async Form Utility cannot be setup without an element!'); @@ -28,17 +31,23 @@ export class AsyncForm { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) { return false; } - this._element.addEventListener('submit', this._submitHandler); + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element); + this._eventManager.registerNewListener(submitEvent); this._element.classList.add(ASYNC_FORM_INITIALIZED_CLASS); } destroy() { - // TODO + this._eventManager.cleanUp(); + + if(this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) + this._element.classList.remove(ASYNC_FORM_INITIALIZED_CLASS); } _processResponse(response) { @@ -51,6 +60,7 @@ export class AsyncForm { setTimeout(() => { parentElement.insertBefore(responseElement, this._element); this._element.remove(); + this._app.utilRegistry.destroyAll(this._element); }, delay); } @@ -91,7 +101,6 @@ export class AsyncForm { ).catch(() => { const failureMessage = this._app.i18n.get('asyncFormFailure'); this._processResponse({ content: failureMessage }); - this._element.classList.remove(ASYNC_FORM_LOADING_CLASS); }); } diff --git a/frontend/src/utils/async-form/async-form.spec.js b/frontend/src/utils/async-form/async-form.spec.js index f01280b8a..aeb7ce4ba 100644 --- a/frontend/src/utils/async-form/async-form.spec.js +++ b/frontend/src/utils/async-form/async-form.spec.js @@ -1,4 +1,4 @@ -import { AsyncForm } from './async-form'; +import { AsyncForm, ASYNC_FORM_INITIALIZED_CLASS } from './async-form'; describe('AsyncForm', () => { @@ -13,6 +13,12 @@ describe('AsyncForm', () => { expect(asyncForm).toBeTruthy(); }); + it('should destroy asyncForm', () => { + asyncForm.destroy(); + expect(asyncForm._eventManager._registeredListeners.length).toBe(0); + expect(asyncForm._element.classList).not.toContain(ASYNC_FORM_INITIALIZED_CLASS); + }); + it('should throw if called without an element', () => { expect(() => { new AsyncForm(); diff --git a/frontend/src/utils/async-table/async-table.js b/frontend/src/utils/async-table/async-table.js index 93d3bcf99..1734de1b1 100644 --- a/frontend/src/utils/async-table/async-table.js +++ b/frontend/src/utils/async-table/async-table.js @@ -2,6 +2,7 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { Datepicker } from '../form/datepicker'; import { HttpClient } from '../../services/http-client/http-client'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import * as debounce from 'lodash.debounce'; import * as throttle from 'lodash.throttle'; import './async-table-filter.sass'; @@ -30,6 +31,8 @@ export class AsyncTable { _element; _app; + _eventManager; + _asyncTableHeader; _asyncTableId; @@ -66,6 +69,8 @@ export class AsyncTable { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) { return false; } @@ -144,7 +149,11 @@ export class AsyncTable { } destroy() { - console.log('TBD: Destroy AsyncTable'); + this._windowStorage.clear(this._windowStorage._options); + this._eventManager.cleanUp(); + this._active = false; + if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) + this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); } _startSortableHeaders() { @@ -156,7 +165,8 @@ export class AsyncTable { this._windowStorage.save('horizPos', (this._scrollTable || {}).scrollLeft); this._linkClickHandler(event); }; - th.element.addEventListener('click', th.clickHandler); + const linkClickEvent = new EventWrapper(EVENT_TYPE.CLICK, th.clickHandler.bind(this), th.element); + this._eventManager.registerNewListener(linkClickEvent); }); } @@ -179,7 +189,9 @@ export class AsyncTable { } this._linkClickHandler(event); }; - link.element.addEventListener('click', link.clickHandler); + + const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, link.clickHandler.bind(this), link.element); + this._eventManager.registerNewListener(clickEvent); }); } } @@ -190,7 +202,8 @@ export class AsyncTable { if (this._pagesizeForm) { const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]'); - pagesizeSelect.addEventListener('change', this._changePagesizeHandler); + const pageSizeChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, this._changePagesizeHandler.bind(this), pagesizeSelect); + this._eventManager.registerNewListener(pageSizeChangeEvent); } } @@ -227,17 +240,7 @@ export class AsyncTable { const debouncedUpdateFromTableFilter = throttle((() => this._updateFromTableFilter(tableFilterForm)).bind(this), FILTER_DEBOUNCE, { leading: true, trailing: false }); [...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => { - const submitLockObserver = new MutationObserver((mutations, observer) => { - for (const mutation of mutations) { - // if the submit lock has been released, trigger an update and disconnect this observer - if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') { - debouncedUpdateFromTableFilter(); - observer.disconnect(); - break; - } - } - }); - this._cancelPendingUpdates.push(() => { submitLockObserver.disconnect(); }); + this._cancelPendingUpdates.push(() => { this._eventManager.removeAllObserversFromUtil();}); const debouncedInput = debounce(() => { const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED); @@ -246,7 +249,16 @@ export class AsyncTable { debouncedUpdateFromTableFilter(); } else if (submitLockedAttr === 'true') { // observe the submit lock of the input element - submitLockObserver.observe(input, { + this._eventManager.registerNewMutationObserver(((mutations, observer) => { + for (const mutation of mutations) { + // if the submit lock has been released, trigger an update and disconnect this observer + if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') { + debouncedUpdateFromTableFilter(); + observer.disconnect(); + break; + } + } + }).bind(this), input, { attributes: true, attributeFilter: [ATTR_SUBMIT_LOCKED], attributeOldValue: true, @@ -254,33 +266,42 @@ export class AsyncTable { } }, INPUT_DEBOUNCE); this._cancelPendingUpdates.push(debouncedInput.cancel); - - input.addEventListener('input', () => { + + const inputHandler =() => { this._ignoreRequest = true; debouncedInput(); - }); + }; + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, inputHandler.bind(this), input ); + this._eventManager.registerNewListener(inputEvent); }); this._tableFilterInputs.change.forEach((input) => { - input.addEventListener('change', () => { + + const changeHandler = () => { //if (this._element.classList.contains(ASYNC_TABLE_LOADING_CLASS)) this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const changeEvent = new EventWrapper(EVENT_TYPE.CHANGE, changeHandler.bind(this), input); + this._eventManager.registerNewListener(changeEvent); }); this._tableFilterInputs.select.forEach((input) => { - input.addEventListener('change', () => { + const selectChangeHandler = () => { this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const selectEvent = new EventWrapper(EVENT_TYPE.CHANGE, selectChangeHandler.bind(this), input); + this._eventManager.registerNewListener(selectEvent); }); - tableFilterForm.addEventListener('submit', (event) =>{ + const submitEventHandler = (event) =>{ event.preventDefault(); this._ignoreRequest = true; debouncedUpdateFromTableFilter(); - }); + }; + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, submitEventHandler.bind(this), tableFilterForm); + this._eventManager.registerNewListener(submitEvent); } _updateFromTableFilter(tableFilterForm) { @@ -425,6 +446,8 @@ export class AsyncTable { this._active = false; this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS); this._element.dataset['currentTableUrl'] = url.href; + + this._app.utilRegistry.destroyAll(this._element); // update table with new this._element.innerHTML = response.element.innerHTML; @@ -440,7 +463,7 @@ export class AsyncTable { } _debugLog() {} - // _debugLog(fName, ...args) { + //_debugLog(fName, ...args) { // console.log(`[DEBUGLOG] AsyncTable.${fName}`, { args: args, instance: this }); // } } diff --git a/frontend/src/utils/async-table/async-table.spec.js b/frontend/src/utils/async-table/async-table.spec.js index 7f008ac49..de5dc9b98 100644 --- a/frontend/src/utils/async-table/async-table.spec.js +++ b/frontend/src/utils/async-table/async-table.spec.js @@ -1,4 +1,4 @@ -import { AsyncTable } from './async-table'; +import { AsyncTable, ASYNC_TABLE_INITIALIZED_CLASS } from './async-table'; const AppTestMock = { httpClient: { @@ -50,4 +50,11 @@ describe('AsyncTable', () => { new AsyncTable(); }).toThrow(); }); + + it('should destroy Async Table', () => { + asyncTable.start(); + asyncTable.destroy(); + expect(asyncTable._eventManager._registeredListeners.length).toBe(0); + expect(asyncTable._element.classList).not.toContain(ASYNC_TABLE_INITIALIZED_CLASS); + }); }); diff --git a/frontend/src/utils/check-all/check-all.js b/frontend/src/utils/check-all/check-all.js index 94115ffee..29e33cff8 100644 --- a/frontend/src/utils/check-all/check-all.js +++ b/frontend/src/utils/check-all/check-all.js @@ -2,6 +2,7 @@ const DEBUG_MODE = /localhost/.test(window.location.href) ? 0 : 0; import { Utility } from '../../core/utility'; import { TableIndices } from '../../lib/table/table'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const CHECKBOX_SELECTOR = '[type="checkbox"]'; @@ -13,6 +14,8 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized'; export class CheckAll { _element; + _eventManager; + _columns = new Array(); _checkAllColumns = new Array(); @@ -27,6 +30,8 @@ export class CheckAll { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) { return false; } @@ -41,12 +46,22 @@ export class CheckAll { let checkboxColumns = this._findCheckboxColumns(); checkboxColumns.forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId]))); - // mark initialized this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS); } + destroy() { + this._eventManager.cleanUp(); + this._checkAllColumns.forEach((column) => { + if (column._checkAllCheckBox !== undefined) + column._checkAllCheckBox.remove(); + }); + + if(this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) + this._element.classList.remove(CHECK_ALL_INITIALIZED_CLASS); + } + _gatherColumns() { for (const rowIndex of Array(this._tableIndices.maxRow + 1).keys()) { for (const colIndex of Array(this._tableIndices.maxCol + 1).keys()) { @@ -86,14 +101,17 @@ class CheckAllColumn { _app; _table; _column; + + _eventManager _checkAllCheckbox; _checkboxId = 'check-all-checkbox-' + Math.floor(Math.random() * 100000); - constructor(table, app, column) { + constructor(table, app, column, eventManager) { this._column = column; this._table = table; this._app = app; + this._eventManager = eventManager; const th = this._column.filter(element => element.tagName == 'TH')[0]; if (!th) @@ -108,7 +126,8 @@ class CheckAllColumn { // set up new checkbox this._app.utilRegistry.initAll(th); - this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput.bind(this)); + const checkBoxInputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._onCheckAllCheckboxInput.bind(this), this._checkAllCheckbox); + this._eventManager.registerNewListener(checkBoxInputEvent); this._setupCheckboxListeners(); } @@ -119,9 +138,10 @@ class CheckAllColumn { _setupCheckboxListeners() { this._column .flatMap(cell => cell.tagName == 'TH' ? new Array() : Array.from(cell.querySelectorAll(CHECKBOX_SELECTOR))) - .forEach(checkbox => - checkbox.addEventListener('input', this._updateCheckAllCheckboxState.bind(this)) - ); + .forEach(checkbox => { + const checkBoxUpdateEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateCheckAllCheckboxState.bind(this), checkbox); + this._eventManager.registerNewListener(checkBoxUpdateEvent); + }); } _updateCheckAllCheckboxState() { diff --git a/frontend/src/utils/check-all/check-all.spec.js b/frontend/src/utils/check-all/check-all.spec.js index 9b8d30f77..431dd5993 100644 --- a/frontend/src/utils/check-all/check-all.spec.js +++ b/frontend/src/utils/check-all/check-all.spec.js @@ -1,4 +1,4 @@ -import { CheckAll } from './check-all'; +import { CheckAll, CHECK_ALL_INITIALIZED_CLASS } from './check-all'; const MOCK_APP = { utilRegistry: { @@ -24,4 +24,11 @@ describe('CheckAll', () => { new CheckAll(); }).toThrow(); }); + + it('should destroy CheckAll', () => { + checkAll.destroy(); + expect(checkAll._eventManager._registeredListeners.length).toBe(0); + expect(checkAll._element.classList).not.toEqual(jasmine.arrayContaining([CHECK_ALL_INITIALIZED_CLASS])); + }); + }); diff --git a/frontend/src/utils/course-teaser/course-teaser.js b/frontend/src/utils/course-teaser/course-teaser.js index 0419dcbda..31d8cf225 100644 --- a/frontend/src/utils/course-teaser/course-teaser.js +++ b/frontend/src/utils/course-teaser/course-teaser.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './course-teaser.sass'; const COURSE_TEASER_INITIALIZED_CLASS = 'course-teaser--initialized'; @@ -12,16 +13,30 @@ const COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron'; export class CourseTeaser { _element; + _eventManager constructor(element) { if (!element) { throw new Error('CourseTeaser utility cannot be setup without an element!'); } + this._eventManager = new EventManager(); if (element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) { return false; } this._element = element; - element.addEventListener('click', e => this._onToggleExpand(e)); + const clickHandler = e => this._onToggleExpand(e); + const clickEvent = new EventWrapper(EVENT_TYPE.CLICK, clickHandler.bind(this), element); + this._eventManager.registerNewListener(clickEvent); + } + + destroy() { + this._eventManager.cleanUp(); + if(this._element.classList.contains(COURSE_TEASER_EXPANDED_CLASS)) { + this._element.classList.remove(COURSE_TEASER_EXPANDED_CLASS); + } + if (this._element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) { + this._element.classList.remove(COURSE_TEASER_INITIALIZED_CLASS); + } } _onToggleExpand(event) { diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js index cf23c911d..f078f9825 100644 --- a/frontend/src/utils/exam-correct/exam-correct.js +++ b/frontend/src/utils/exam-correct/exam-correct.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import { HttpClient } from '../../services/http-client/http-client'; import moment from 'moment'; @@ -58,6 +59,7 @@ export class ExamCorrect { _lastColumnIndex; _storageManager; + _eventManager; constructor(element, app) { if (!element) { @@ -71,6 +73,8 @@ export class ExamCorrect { this._element = element; this._app = app; + this._eventManager = new EventManager(); + // TODO work in progress // this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.SESSION, encryption: { all: { tag: 'exam-correct', exam: this._element.getAttribute('uw-exam-correct') } } }); this._storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.WINDOW }); @@ -88,20 +92,28 @@ export class ExamCorrect { this._resultPassSelect = resultDetailCell && resultDetailCell.querySelector('select.uw-exam-correct__pass'); this._partDeleteBoxes = [...this._element.querySelectorAll('input.uw-exam-correct--delete-exam-part')]; - if (this._sendBtn) - this._sendBtn.addEventListener('click', this._sendCorrectionHandler.bind(this)); - else console.error('ExamCorrect utility could not detect send button!'); + if (this._sendBtn){ + const sendClickEvent = new EventWrapper(EVENT_TYPE.CLICK, this._sendCorrectionHandler.bind(this), this._sendBtn); + this._eventManager.registerNewListener(sendClickEvent); + } else { + console.error('ExamCorrect utility could not detect send button!'); + } - if (this._userInput) - this._userInput.addEventListener('focusout', this._validateUserInput.bind(this)); - else throw new Error('ExamCorrect utility could not detect user input!'); + if (this._userInput) { + const focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT, this._validateUserInput.bind(this), this._userInput); + this._eventManager.registerNewListener(focusOutEvent); + } else { + throw new Error('ExamCorrect utility could not detect user input!'); + } for (let deleteBox of this._partDeleteBoxes) { - deleteBox.addEventListener('change', (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this)); + const deleteBoxChangeEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._updatePartDeleteDisabled(deleteBox); }).bind(this), deleteBox); + this._eventManger.registerNewListener(deleteBoxChangeEvent); } for (let input of [this._userInput, ...this._partInputs]) { - input.addEventListener('keypress', this._inputKeypress.bind(this)); + const inputKeyDownEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, this._inputKeypress.bind(this), input); + this._eventManager.registerNewListener(inputKeyDownEvent); } if (!this._userInputStatus) { @@ -125,23 +137,25 @@ export class ExamCorrect { ); if (this._resultSelect && this._resultGradeSelect) { - this._resultSelect.addEventListener('change', () => { + const resultSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { if (this._resultSelect.value !== 'grade') this._resultGradeSelect.classList.add('grade-hidden'); else this._resultGradeSelect.classList.remove('grade-hidden'); - }); + }).bind(this), this._resultSelect ); + this._eventManager.registerNewListener(resultSelectEvent); if (this._resultSelect.value !== 'grade') this._resultGradeSelect.classList.add('grade-hidden'); } if (this._resultSelect && this._resultPassSelect) { - this._resultSelect.addEventListener('change', () => { + const resultPassSelectEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { if (this._resultSelect.value !== 'pass') this._resultPassSelect.classList.add('pass-hidden'); else this._resultPassSelect.classList.remove('pass-hidden'); - }); + }).bind(this), this._resultSelect); + this._eventManager.registerNewListener(resultPassSelectEvent); if (this._resultSelect.value !== 'pass') this._resultPassSelect.classList.add('pass-hidden'); @@ -158,9 +172,7 @@ export class ExamCorrect { } destroy() { - this._sendBtn.removeEventListener('click', this._sendCorrectionHandler); - this._userInput.removeEventListener('change', this._validateUserInput); - // TODO destroy handlers on user input candidate elements + this._eventManager.cleanUp(); } _updatePartDeleteDisabled(deleteBox) { diff --git a/frontend/src/utils/form/auto-submit-button.js b/frontend/src/utils/form/auto-submit-button.js index bf7544d30..77e942f28 100644 --- a/frontend/src/utils/form/auto-submit-button.js +++ b/frontend/src/utils/form/auto-submit-button.js @@ -9,8 +9,10 @@ const AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden'; selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR, }) export class AutoSubmitButton { + _element; constructor(element) { + this._element = element; if (!element) { throw new Error('Auto Submit Button utility needs to be passed an element!'); } @@ -24,6 +26,7 @@ export class AutoSubmitButton { } destroy() { - // TODO + this._element.classList.remove(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS); + this._element.classList.remove(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS); } } diff --git a/frontend/src/utils/form/auto-submit-button.spec.js b/frontend/src/utils/form/auto-submit-button.spec.js new file mode 100644 index 000000000..b1436c867 --- /dev/null +++ b/frontend/src/utils/form/auto-submit-button.spec.js @@ -0,0 +1,27 @@ +import { AutoSubmitButton, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS, AUTO_SUBMIT_BUTTON_HIDDEN_CLASS } from './auto-submit-button.js'; + +describe('Auto-submit-button', () => { + + let autoSubmitButton; + + beforeEach(() => { + const element = document.createElement('div'); + autoSubmitButton = new AutoSubmitButton(element); + }); + + it('should create', () => { + expect(autoSubmitButton).toBeTruthy(); + }); + + it('should destory auto-submit-button', () => { + autoSubmitButton.destroy(); + expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS); + expect(autoSubmitButton._element.classList).not.toContain(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS); + }); + + it('should throw if called without an element', () => { + expect(() => { + new AutoSubmitButton(); + }).toThrow(); + }); + }); \ No newline at end of file diff --git a/frontend/src/utils/form/auto-submit-input.js b/frontend/src/utils/form/auto-submit-input.js index f442f2960..291a1d3aa 100644 --- a/frontend/src/utils/form/auto-submit-input.js +++ b/frontend/src/utils/form/auto-submit-input.js @@ -1,5 +1,6 @@ import * as debounce from 'lodash.debounce'; import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; export const AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]'; @@ -12,6 +13,8 @@ export class AutoSubmitInput { _element; + _eventManager; + _form; _debouncedHandler; @@ -22,6 +25,8 @@ export class AutoSubmitInput { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) { return false; } @@ -33,12 +38,16 @@ export class AutoSubmitInput { this._debouncedHandler = debounce(this.autoSubmit, 500); - this._element.addEventListener('input', this._debouncedHandler); + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._debouncedHandler.bind(this), this._element); + this._eventManager.registerNewListener(inputEvent); + this._element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS); } destroy() { - this._element.removeEventListener('input', this._debouncedHandler); + this._eventManager.cleanUp(); + if(this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) + this._element.classList.remove(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS); } autoSubmit = () => { diff --git a/frontend/src/utils/form/auto-submit-input.spec.js b/frontend/src/utils/form/auto-submit-input.spec.js new file mode 100644 index 000000000..26c59cdd2 --- /dev/null +++ b/frontend/src/utils/form/auto-submit-input.spec.js @@ -0,0 +1,30 @@ +import { AutoSubmitInput, AUTO_SUBMIT_INPUT_INITIALIZED_CLASS } from './auto-submit-input.js'; + +describe('Auto-submit-input', () => { + + let autoSubmitInput; + + beforeEach(() => { + const form = document.createElement('form'); + const element = document.createElement('input'); + element.setAttribute('type', 'text'); + form.append(element); + autoSubmitInput = new AutoSubmitInput(element); + }); + + it('should create', () => { + expect(autoSubmitInput).toBeTruthy(); + }); + + it('should destory auto-submit-button', () => { + autoSubmitInput.destroy(); + expect(autoSubmitInput._eventManager._registeredListeners.length).toBe(0); + expect(autoSubmitInput._element.classList).not.toEqual(jasmine.arrayContaining([AUTO_SUBMIT_INPUT_INITIALIZED_CLASS])); + }); + + it('should throw if called without an element', () => { + expect(() => { + new AutoSubmitInput(); + }).toThrow(); + }); + }); \ No newline at end of file diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js index 2a9367f47..b6d754e09 100644 --- a/frontend/src/utils/form/communication-recipients.js +++ b/frontend/src/utils/form/communication-recipients.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const MASS_INPUT_SELECTOR = '.massinput'; const RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories'; @@ -14,62 +15,81 @@ const RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checke }) export class CommunicationRecipients { massInputElement; + _element; + + _eventManager; constructor(element) { if (!element) { throw new Error('Communication Recipient utility cannot be setup without an element!'); } - - this.massInputElement = element.closest(MASS_INPUT_SELECTOR); + this._element = element; + this._eventManager = new EventManager(); + this.massInputElement = this._element.closest(MASS_INPUT_SELECTOR); this.setupRecipientCategories(); - const recipientObserver = new MutationObserver(this.setupRecipientCategories.bind(this)); - recipientObserver.observe(this.massInputElement, { childList: true }); + this._eventManager.registerNewMutationObserver(this.setupRecipientCategories.bind(this), this.massInputElement, { childList: true }); } setupRecipientCategories() { - Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory); + Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(this.setupRecipientCategory.bind(this)); } -} -function setupRecipientCategory(recipientCategoryElement) { - const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); - const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); + _removeCheckedCounter() { + let checkedCounters = this._element.querySelectorAll(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR); + checkedCounters.forEach((checkedCounter) => { + checkedCounter.innerHTML = ''; + }); + } - if (categoryOptions) { - const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); - const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); + destroy() { + this._eventManager.cleanUp(); + this._removeCheckedCounter(); + } - // setup category checkbox to toggle all child checkboxes if changed - categoryCheckbox.addEventListener('change', () => { - categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { - checkbox.checked = categoryCheckbox.checked; - }); - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); - // update counter and toggle checkbox initially - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - - // register change listener for individual checkboxes - categoryCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', () => { - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); - }); - - // register change listener for toggle all checkbox - if (toggleAllCheckbox) { - toggleAllCheckbox.addEventListener('change', () => { + setupRecipientCategory(recipientCategoryElement) { + const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); + const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); + + if (categoryOptions) { + const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); + const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); + + // setup category checkbox to toggle all child checkboxes if changed + const categoryToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE,(() => { categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { - checkbox.checked = toggleAllCheckbox.checked; + checkbox.checked = categoryCheckbox.checked; }); updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }).bind(this), categoryCheckbox ); + this._eventManager.registerNewListener(categoryToggleEvent); + + // update counter and toggle checkbox initially + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + + // register change listener for individual checkboxes + categoryCheckboxes.forEach(checkbox => { + const individualToggleEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }).bind(this), checkbox); + this._eventManager.registerNewListener(individualToggleEvent); }); + + // register change listener for toggle all checkbox + if (toggleAllCheckbox) { + const toggleAllEvent = new EventWrapper(EVENT_TYPE.CHANGE, (() => { + categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { + checkbox.checked = toggleAllCheckbox.checked; + }); + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + }).bind(this), toggleAllCheckbox); + this._eventManager.registerNewListener(toggleAllEvent); + } } } } diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index 21b09eb19..c6273c07b 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -2,6 +2,7 @@ import datetime from 'tail.datetime'; import './datepicker.css'; import { Utility } from '../../core/utility'; import moment from 'moment'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import * as defer from 'lodash.defer'; @@ -82,6 +83,8 @@ export class Datepicker { initialValue; _locale; + _eventManager; + _unloadIsDueToSubmit = false; constructor(element) { @@ -102,6 +105,8 @@ export class Datepicker { this._element = element; + this._eventManager = new EventManager(); + // store the previously set type to select the input format this.elementType = this._element.getAttribute('type'); @@ -179,23 +184,22 @@ export class Datepicker { } // reregister change event to prevent event loop - this._element.addEventListener('change', setDatepickerDate, { once: true }); }; // 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 }); - + const changeSelectedDateEvent = new EventWrapper(EVENT_TYPE.CHANGE, setDatepickerDate.bind(this), this._element, { once: true }); + this._eventManager.registerNewListener(changeSelectedDateEvent); // 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) => { + let callback = (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, { + }; + this._eventManager.registerNewMutationObserver(callback.bind(this), this.datepickerInstance.dt, { attributes: true, attributeFilter: ['class'], attributeOldValue: true, @@ -203,38 +207,44 @@ export class Datepicker { // 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 focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT,(event => { const hasFocus = event.relatedTarget !== null; const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget); const focussedIsNotElement = event.relatedTarget !== this._element; const focussedIsInDocument = window.document.contains(event.relatedTarget); if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument) this.closeDatepickerInstance(); - }); + }).bind(this), window ); + this._eventManager.registerNewListener(focusOutEvent); // close the instance on click on any element outside of the datepicker (except the input element itself) - window.addEventListener('click', event => { + const clickOutsideEvent = new EventWrapper(EVENT_TYPE.CLICK, (event => { const targetIsOutside = !this.datepickerInstance.dt.contains(event.target) && event.target !== this.datepickerInstance.dt; const targetIsInDocument = window.document.contains(event.target); const targetIsNotElement = event.target !== this._element; if (targetIsOutside && targetIsInDocument && targetIsNotElement) this.closeDatepickerInstance(); - }); + }).bind(this), window); + this._eventManager.registerNewListener(clickOutsideEvent); // close the instance on escape keydown events - this._element.addEventListener('keydown', event => { + const escapeCloseEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, (event => { if (event.keyCode === KEYCODE_ESCAPE) { this.closeDatepickerInstance(); } - }); + }).bind(this), this._element); + this._eventManager.registerNewListener(escapeCloseEvent); // format the date value of the form input element of this datepicker before form submission - this._element.form.addEventListener('submit', this._submitHandler.bind(this)); + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form); + this._eventManager.registerNewListener(submitEvent); } destroy() { this.datepickerInstance.remove(); + this._eventManager.cleanUp(); + this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS); } diff --git a/frontend/src/utils/form/enter-is-tab.js b/frontend/src/utils/form/enter-is-tab.js index 4e171c87b..b3097d50d 100644 --- a/frontend/src/utils/form/enter-is-tab.js +++ b/frontend/src/utils/form/enter-is-tab.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const ENTER_IS_TAB_INITIALIZED_CLASS = 'enter-as-tab--initialized'; const AREA_SELECTOR = 'input, textarea'; @@ -10,6 +11,8 @@ const AREA_SELECTOR = 'input, textarea'; export class EnterIsTab { _element; + _eventManager; + constructor(element) { if(!element) { @@ -17,6 +20,8 @@ export class EnterIsTab { } this._element = element; + + this._eventManager = new EventManager(); if (this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) { return false; @@ -27,27 +32,32 @@ export class EnterIsTab { start() { - this._element.addEventListener('keydown', (e) => { - if(e.key === 'Enter') { - e.preventDefault(); - let currentInputFieldId = this._element.id; - let inputAreas = document.querySelectorAll(AREA_SELECTOR); - let nextInputArea = null; - for (let i = 0; i < inputAreas.length; i++) { - if(inputAreas[i].id === currentInputFieldId) { - nextInputArea = inputAreas[i+1]; - break; - } - } - - if(nextInputArea) { - nextInputArea.focus(); + let eventWrapper = new EventWrapper(EVENT_TYPE.KEYDOWN, this._captureEnter.bind(this), this._element); + this._eventManager.registerNewListener(eventWrapper); + } + + _captureEnter (e) { + if(e.key === 'Enter') { + e.preventDefault(); + let currentInputFieldId = this._element.id; + let inputAreas = document.querySelectorAll(AREA_SELECTOR); + let nextInputArea = null; + for (let i = 0; i < inputAreas.length; i++) { + if(inputAreas[i].id === currentInputFieldId) { + nextInputArea = inputAreas[i+1]; + break; } } - }); + + if(nextInputArea) { + nextInputArea.focus(); + } + } } destroy() { - console.log('TBD: Destroy EnterIsTab'); + this._eventManager.cleanUp(); + if(this._element.classList.contains(ENTER_IS_TAB_INITIALIZED_CLASS)) + this._element.classList.remove(ENTER_IS_TAB_INITIALIZED_CLASS); } } \ No newline at end of file diff --git a/frontend/src/utils/form/enter-is-tab.md b/frontend/src/utils/form/enter-is-tab.md new file mode 100644 index 000000000..67fd7e2c4 --- /dev/null +++ b/frontend/src/utils/form/enter-is-tab.md @@ -0,0 +1,8 @@ +# Enter is Tab Utility +When the user presses enter on a form that uses this utility, the enter is converted to a tab in order to not send the form. + +## Attribute: +`uw-enter-as-tab` + +## Example usage: + \ No newline at end of file diff --git a/frontend/src/utils/form/form-error-remover.js b/frontend/src/utils/form/form-error-remover.js index 1b1509c2d..f05e96fae 100644 --- a/frontend/src/utils/form/form-error-remover.js +++ b/frontend/src/utils/form/form-error-remover.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized'; const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select'; @@ -13,6 +14,8 @@ export class FormErrorRemover { _element; + _eventManager; + constructor(element) { if (!element) throw new Error('Form Error Remover utility needs to be passed an element!'); @@ -24,6 +27,7 @@ export class FormErrorRemover { return; this._element = element; + this._eventManager = new EventManager(); this._element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS); } @@ -35,11 +39,18 @@ export class FormErrorRemover { const inputElements = Array.from(this._element.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR)); inputElements.forEach((inputElement) => { - inputElement.addEventListener('input', () => { + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, (() => { if (!inputElement.willValidate || inputElement.validity.vaild) { FORM_GROUP_WITH_ERRORS_CLASSES.forEach(c => { this._element.classList.remove(c); }); } - }); + }).bind(this), inputElement); + this._eventManager.registerNewListener(inputEvent); }); } + + destroy() { + this._eventManager.cleanUp(); + this._element.classList.remove(FORM_ERROR_REMOVER_INITIALIZED_CLASS); + } + } diff --git a/frontend/src/utils/form/form-error-reporter.js b/frontend/src/utils/form/form-error-reporter.js index dff3f00d2..ed3012b26 100644 --- a/frontend/src/utils/form/form-error-reporter.js +++ b/frontend/src/utils/form/form-error-reporter.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import * as defer from 'lodash.defer'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FORM_ERROR_REPORTER_INITIALIZED_CLASS = 'form-error-remover--initialized'; @@ -10,12 +11,16 @@ export class FormErrorReporter { _element; _err; + _eventManager; + constructor(element) { if (!element) throw new Error('Form Error Reporter utility needs to be passed an element!'); this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS)) return; @@ -24,11 +29,23 @@ export class FormErrorReporter { start() { if (this._element.willValidate) { - this._element.addEventListener('invalid', this.report.bind(this)); - this._element.addEventListener('change', () => { defer(this.report.bind(this)); } ); + let invalidElementEvent = new EventWrapper(EVENT_TYPE.INVALID, this.report.bind(this), this._element); + this._eventManager.registerNewListener(invalidElementEvent); + + let changedElementEvent = new EventWrapper(EVENT_TYPE.CHANGE, () => { defer(this.report.bind(this)); }, this._element); + this._eventManager.registerNewListener(changedElementEvent); } } + destroy() { + this._eventManager.cleanUp(); + + this._removeError(); + + if(this._element.classList.contains(FORM_ERROR_REPORTER_INITIALIZED_CLASS)) + this._element.classList.remove(FORM_ERROR_REPORTER_INITIALIZED_CLASS); + } + report() { const msg = this._element.validity.valid ? null : this._element.validationMessage; @@ -37,10 +54,7 @@ export class FormErrorReporter { if (!target) return; - if (this._err && this._err.parentNode) { - this._err.parentNode.removeChild(this._err); - this._err = undefined; - } + this._removeError(); if (!msg) { target.classList.remove('standalone-field--has-error', 'form-group--has-error'); @@ -65,4 +79,11 @@ export class FormErrorReporter { } } } + + _removeError() { + if (this._err && this._err.parentNode) { + this._err.parentNode.removeChild(this._err); + this._err = undefined; + } + } } diff --git a/frontend/src/utils/form/interactive-fieldset.js b/frontend/src/utils/form/interactive-fieldset.js index 7e912ab90..2c8ff4eb9 100644 --- a/frontend/src/utils/form/interactive-fieldset.js +++ b/frontend/src/utils/form/interactive-fieldset.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target'; @@ -15,6 +16,8 @@ export class InteractiveFieldset { _element; + _eventManager; + conditionalInput; conditionalValue; target; @@ -28,6 +31,8 @@ export class InteractiveFieldset { this._element = element; + this._eventManager = new EventManager(); + if (this._element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) { return false; } @@ -62,13 +67,11 @@ export class InteractiveFieldset { this.childInputs = Array.from(this._element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR)).filter(child => child.closest('[uw-interactive-fieldset]') === this._element); // add event listener - const observer = new MutationObserver(this._updateVisibility.bind(this)); - observer.observe(this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] }); - this.conditionalInput.addEventListener('input', this._updateVisibility.bind(this)); - + this._eventManager.registerNewMutationObserver(this._updateVisibility.bind(this), this.conditionalInput, { attributes: true, attributeFilter: ['data-interactive-fieldset-hidden'] }); + const inputEvent = new EventWrapper(EVENT_TYPE.INPUT, this._updateVisibility.bind(this), this.conditionalInput); + this._eventManager.registerNewListener(inputEvent); // mark as initialized this._element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS); - } start() { @@ -77,7 +80,8 @@ export class InteractiveFieldset { } destroy() { - // TODO + this._eventManager.cleanUp(); + this._element.classList.remove(INTERACTIVE_FIELDSET_INITIALIZED_CLASS); } _updateVisibility() { diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index 8890e198e..da900ba72 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button'; import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input'; @@ -26,16 +27,21 @@ const NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT = '[uw-no-navigate-away-prompt]'; export class NavigateAwayPrompt { _element; + _app; + + _eventManager; _initFormData; _unloadDueToSubmit = false; - constructor(element) { + constructor(element, app) { if (!element) { throw new Error('Navigate Away Prompt utility needs to be passed an element!'); } this._element = element; + this._app = app; + this._eventManager = new EventManager(); if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { return; @@ -65,15 +71,18 @@ export class NavigateAwayPrompt { return; this._initFormData = new FormData(this._element); - window.addEventListener('beforeunload', this._beforeUnloadHandler.bind(this)); + const beforeUnloadEvent = new EventWrapper(EVENT_TYPE.BEFOREUNLOAD, this._beforeUnloadHandler.bind(this), window); + this._eventManager.registerNewListener(beforeUnloadEvent); - this._element.addEventListener('submit', () => { + const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, (() => { this._unloadDueToSubmit = true; defer(() => { this._unloadDueToSubmit = false; } ); // Restore state after event loop is settled - }); + }).bind(this), this._element); + this._eventManager.registerNewListener(submitEvent); } destroy() { + this._eventManager.cleanUp(); this._element.classList.remove(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } @@ -98,7 +107,7 @@ export class NavigateAwayPrompt { // allow the event to happen if the form was not touched by the // user (i.e. if the current FormData is equal to the initial FormData) // or the unload event was initiated by a form submit - if (!formDataHasChanged || this._unloadDueToSubmit) + if (!formDataHasChanged || this.unloadDueToSubmit) return; // cancel the unload event. This is the standard to force the prompt to appear. diff --git a/frontend/src/utils/form/reactive-submit-button.js b/frontend/src/utils/form/reactive-submit-button.js index e46eed77e..c5bc3c642 100644 --- a/frontend/src/utils/form/reactive-submit-button.js +++ b/frontend/src/utils/form/reactive-submit-button.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized'; @@ -12,12 +13,15 @@ export class ReactiveSubmitButton { _requiredInputs; _submitButton; + _eventManager; + constructor(element) { if (!element) { throw new Error('Reactive Submit Button utility cannot be setup without an element!'); } this._element = element; + this._eventManager = new EventManager(); if (this._element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) { return false; @@ -51,16 +55,18 @@ export class ReactiveSubmitButton { } destroy() { - // TODO + this._eventManager.removeAllEventListenersFromUtil(); + this._element.classList.remove(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS); } setupInputs() { this._requiredInputs.forEach((el) => { const checkbox = el.getAttribute('type') === 'checkbox'; - const eventType = checkbox ? 'change' : 'input'; - el.addEventListener(eventType, () => { + const eventType = checkbox ? EVENT_TYPE.CHANGE : EVENT_TYPE.INPUT; + const valEvent = new EventWrapper(eventType,(() => { this.updateButtonState(); - }); + }).bind(this), el ); + this._eventManager.registerNewListener(valEvent); }); } diff --git a/frontend/src/utils/hide-columns/hide-columns.js b/frontend/src/utils/hide-columns/hide-columns.js index 7661ac92b..79390402a 100644 --- a/frontend/src/utils/hide-columns/hide-columns.js +++ b/frontend/src/utils/hide-columns/hide-columns.js @@ -1,5 +1,7 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; + import './hide-columns.sass'; import { TableIndices } from '../../lib/table/table'; @@ -29,6 +31,7 @@ const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized'; export class HideColumns { _storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL }); + _eventManager; _element; _elementWrapper; @@ -36,8 +39,6 @@ export class HideColumns { _autoHide; - _mutationObserver; - _tableIndices; headerToHider = new Map(); @@ -62,6 +63,7 @@ export class HideColumns { return false; this._element = element; + this._eventManager = new EventManager(); this._tableIndices = new TableIndices(this._element); @@ -82,12 +84,17 @@ export class HideColumns { [...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th)); - this._mutationObserver = new MutationObserver(this._tableMutated.bind(this)); - this._mutationObserver.observe(this._element, { childList: true, subtree: true }); + this._eventManager.registerNewMutationObserver(this._tableMutated.bind(this), this._element, { childList: true, subtree: true }); this._element.classList.add(HIDE_COLUMNS_INITIALIZED); } + destroy() { + this._eventManager.cleanUp(); + this._tableUtilContainer.remove(); + this._element.classList.remove(HIDE_COLUMNS_INITIALIZED); + } + setupHideButton(th) { const preHidden = this.isHiddenTH(th); @@ -104,34 +111,41 @@ export class HideColumns { this.addHeaderHider(th, hider); - th.addEventListener('mouseover', () => { + const mouseOverEvent = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); - }); - th.addEventListener('mouseout', () => { + }).bind(this), th); + this._eventManager.registerNewListener(mouseOverEvent); + + const mouseOutEvent = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } - }); + }).bind(this), th); + this._eventManager.registerNewListener(mouseOutEvent); - hider.addEventListener('click', (event) => { + const hideClickEvent = new EventWrapper(EVENT_TYPE.CLICK, ((event) => { event.preventDefault(); event.stopPropagation(); this.switchColumnDisplay(th); // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); - }); + }).bind(this), hider); + this._eventManager.registerNewListener(hideClickEvent); - hider.addEventListener('mouseover', () => { + const mouseOverHider = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, !currentlyHidden); - }); - hider.addEventListener('mouseout', () => { + }).bind(this), hider); + this._eventManager.registerNewListener(mouseOverHider); + + const mouseOutHider = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { if (hider.classList.contains(TABLE_HIDER_CLASS)) { hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); } const currentlyHidden = this.hiderStatus(th); this.updateHiderIcon(hider, currentlyHidden); - }); + }).bind(this), hider); + this._eventManager.registerNewListener(mouseOutHider); new ResizeObserver(() => { this.repositionHider(hider); }).observe(th); diff --git a/frontend/src/utils/inputs/checkbox.js b/frontend/src/utils/inputs/checkbox.js index 9b1b541ff..ebf721667 100644 --- a/frontend/src/utils/inputs/checkbox.js +++ b/frontend/src/utils/inputs/checkbox.js @@ -9,43 +9,54 @@ const CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized'; selector: 'input[type="checkbox"]:not([uw-no-checkbox]), input[type="radio"]:not([uw-no-radiobox])', }) export class Checkbox { + + _element; + _wrapperEl; + constructor(element) { if (!element) { throw new Error('Checkbox utility cannot be setup without an element!'); } + this._element = element; - const isRadio = element.type === 'radio'; + const isRadio = this._element.type === 'radio'; const box_class = isRadio ? RADIOBOX_CLASS : CHECKBOX_CLASS; - if (isRadio && element.closest('.radio-group')) { + if (isRadio && this._element.closest('.radio-group')) { // Don't initialize radiobox, if radio is part of a group return false; } - if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) { + if (this._element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) { // throw new Error('Checkbox utility already initialized!'); return false; } - if (element.parentElement.classList.contains(box_class)) { + if (this._element.parentElement.classList.contains(box_class)) { // throw new Error('Checkbox element\'s wrapper already has class '' + box_class + ''!'); return false; } - const siblingEl = element.nextSibling; - const parentEl = element.parentElement; + const siblingEl = this._element.nextSibling; + const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add(box_class); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add(box_class); const labelEl = document.createElement('label'); labelEl.setAttribute('for', element.id); - wrapperEl.appendChild(element); - wrapperEl.appendChild(labelEl); + this._wrapperEl.appendChild(element); + this._wrapperEl.appendChild(labelEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); - element.classList.add(CHECKBOX_INITIALIZED_CLASS); + this._element.classList.add(CHECKBOX_INITIALIZED_CLASS); + } + + destroy() { + if (this._wrapperEl !== undefined) + this._wrapperEl.remove(); + this._element.classList.remove(CHECKBOX_INITIALIZED_CLASS); } } diff --git a/frontend/src/utils/inputs/file-input.js b/frontend/src/utils/inputs/file-input.js index e84d2ce26..300a15fa4 100644 --- a/frontend/src/utils/inputs/file-input.js +++ b/frontend/src/utils/inputs/file-input.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './file-input.sass'; const FILE_INPUT_CLASS = 'file-input'; @@ -19,6 +20,8 @@ export class FileInput { _fileList; _label; + _eventManager; + constructor(element, app) { if (!element) { throw new Error('FileInput utility cannot be setup without an element!'); @@ -27,6 +30,8 @@ export class FileInput { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) { throw new Error('FileInput utility already initialized!'); } @@ -40,11 +45,11 @@ export class FileInput { this._label = this._createFileLabel(); this._updateLabel(); - // add change listener - this._element.addEventListener('change', () => { + const changeInputEv = new EventWrapper(EVENT_TYPE.CHANGE,(() => { this._updateLabel(); this._renderFileList(); - }); + }).bind(this), this._element ); + this._eventManager.registerNewListener(changeInputEv); // add util class for styling and mark as initialized this._element.classList.add(FILE_INPUT_CLASS); @@ -52,7 +57,10 @@ export class FileInput { } destroy() { - // TODO + this._eventManager.cleanUp(); + this._fileList.remove(); + this._label.remove(); + this._element.classList.remove(FILE_INPUT_INITIALIZED_CLASS); } _renderFileList() { diff --git a/frontend/src/utils/inputs/file-max-size.js b/frontend/src/utils/inputs/file-max-size.js index 653cca287..d0b8e75f5 100644 --- a/frontend/src/utils/inputs/file-max-size.js +++ b/frontend/src/utils/inputs/file-max-size.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const FILE_MAX_SIZE_INITIALIZED_CLASS = 'file-max-size--initialized'; @@ -9,6 +10,8 @@ export class FileMaxSize { _element; _app; + _eventManager; + constructor(element, app) { if (!element) throw new Error('FileMaxSize utility cannot be setup without an element!'); @@ -16,6 +19,8 @@ export class FileMaxSize { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (this._element.classList.contains(FILE_MAX_SIZE_INITIALIZED_CLASS)) { throw new Error('FileMaxSize utility already initialized!'); } @@ -24,7 +29,13 @@ export class FileMaxSize { } start() { - this._element.addEventListener('change', this._change.bind(this)); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this._change.bind(this), this._element); + this._eventManager.registerNewListener(changeEv); + } + + destroy() { + this._eventManager.cleanUp(); + this._element.classList.remove(FILE_MAX_SIZE_INITIALIZED_CLASS); } _change() { diff --git a/frontend/src/utils/inputs/password.js b/frontend/src/utils/inputs/password.js index 2bb750802..0659ab57e 100644 --- a/frontend/src/utils/inputs/password.js +++ b/frontend/src/utils/inputs/password.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const PASSWORD_INITIALIZED_CLASS = 'password-input--initialized'; @@ -9,6 +10,9 @@ export class Password { _element; _iconEl; _toggleContainerEl; + _wrapperEl; + + _eventManager; constructor(element) { if (!element) @@ -18,25 +22,26 @@ export class Password { return false; this._element = element; + this._eventManager = new EventManager(); this._element.classList.add('password-input__input'); const siblingEl = this._element.nextSibling; const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add('password-input__wrapper'); - wrapperEl.appendChild(this._element); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add('password-input__wrapper'); + this._wrapperEl.appendChild(this._element); this._toggleContainerEl = document.createElement('div'); this._toggleContainerEl.classList.add('password-input__toggle'); - wrapperEl.appendChild(this._toggleContainerEl); + this._wrapperEl.appendChild(this._toggleContainerEl); this._iconEl = document.createElement('i'); this._iconEl.classList.add('fas', 'fa-fw'); this._toggleContainerEl.appendChild(this._iconEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); this._element.classList.add(PASSWORD_INITIALIZED_CLASS); } @@ -44,17 +49,31 @@ export class Password { start() { this.updateVisibleIcon(this.isVisible()); - this._toggleContainerEl.addEventListener('mouseover', () => { + const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { this.updateVisibleIcon(!this.isVisible()); - }); - this._toggleContainerEl.addEventListener('mouseout', () => { + }).bind(this), this._toggleContainerEl); + + const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (() => { this.updateVisibleIcon(this.isVisible()); - }); - this._toggleContainerEl.addEventListener('click', (event) => { + }).bind(this), this._toggleContainerEl); + + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => { event.preventDefault(); event.stopPropagation(); this.setVisible(!this.isVisible()); - }); + }).bind(this), this._toggleContainerEl ); + + this._eventManager.registerListeners([mouseOverEv, mouseOutEv, clickEv]); + } + + destroy() { + this._eventManager.cleanUp(); + this._iconEl.remove(); + this._toggleContainerEl.remove(); + this._wrapperEl.remove(); + this._iconEl.remove(); + + this._element.classList.remove(PASSWORD_INITIALIZED_CLASS); } isVisible() { diff --git a/frontend/src/utils/inputs/radio.js b/frontend/src/utils/inputs/radio.js index 38a3f0f2f..311f3fb53 100644 --- a/frontend/src/utils/inputs/radio.js +++ b/frontend/src/utils/inputs/radio.js @@ -9,39 +9,51 @@ const RADIO_INITIALIZED_CLASS = 'radio--initialized'; }) export class Radio { + _element; + _wrapperEl; + _labelEl; + constructor(element) { if (!element) { throw new Error('Radio utility cannot be setup without an element!'); } - if (element.closest('.radio-group')) { + this._element = element; + + if (this._element.closest('.radio-group')) { return false; } - if (element.classList.contains(RADIO_INITIALIZED_CLASS)) { + if (this._element.classList.contains(RADIO_INITIALIZED_CLASS)) { // throw new Error('Radio utility already initialized!'); return false; } - if (element.parentElement.classList.contains(RADIO_CLASS)) { + if (this._element.parentElement.classList.contains(RADIO_CLASS)) { // throw new Error('Radio element\'s wrapper already has class '' + RADIO_CLASS + ''!'); return false; } - const siblingEl = element.nextSibling; - const parentEl = element.parentElement; + const siblingEl = this._element.nextSibling; + const parentEl = this._element.parentElement; - const wrapperEl = document.createElement('div'); - wrapperEl.classList.add(RADIO_CLASS); + this._wrapperEl = document.createElement('div'); + this._wrapperEl.classList.add(RADIO_CLASS); - const labelEl = document.createElement('label'); - labelEl.setAttribute('for', element.id); + this._labelEl = document.createElement('label'); + this._labelEl.setAttribute('for', this._element.id); - wrapperEl.appendChild(element); - wrapperEl.appendChild(labelEl); + this._wrapperEl.appendChild(this._element); + this._wrapperEl.appendChild(this._labelEl); - parentEl.insertBefore(wrapperEl, siblingEl); + parentEl.insertBefore(this._wrapperEl, siblingEl); - element.classList.add(RADIO_INITIALIZED_CLASS); + this._element.classList.add(RADIO_INITIALIZED_CLASS); + } + + destroy() { + this._labelEl.remove(); + this._wrapperEl.remove(); + this._element.classList.remove(RADIO_INITIALIZED_CLASS); } } diff --git a/frontend/src/utils/mass-input/mass-input.js b/frontend/src/utils/mass-input/mass-input.js index aaa5f7d0c..4969e8c14 100644 --- a/frontend/src/utils/mass-input/mass-input.js +++ b/frontend/src/utils/mass-input/mass-input.js @@ -2,6 +2,7 @@ import { Utility } from '../../core/utility'; import { Datepicker } from '../form/datepicker'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './mass-input.sass'; const MASS_INPUT_CELL_SELECTOR = '.massinput__cell'; @@ -29,6 +30,8 @@ export class MassInput { _changedAdd = new Array(); + _eventManager; + constructor(element, app) { if (!element) { throw new Error('Mass Input utility cannot be setup without an element!'); @@ -37,6 +40,8 @@ export class MassInput { this._element = element; this._app = app; + this._eventManager = new EventManager(); + if (global !== undefined) this._global = global; else if (window !== undefined) @@ -64,9 +69,10 @@ export class MassInput { buttons.forEach((button) => { this._setupSubmitButton(button); }); - - this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler.bind(this)); - this._massInputForm.addEventListener('keypress', this._keypressHandler.bind(this)); + + const submitEv = new EventWrapper(EVENT_TYPE.SUBMIT, this._massInputFormSubmitHandler.bind(this), this._massInputForm); + const keyPressEv = new EventWrapper(EVENT_TYPE.KEYDOWN, this._keypressHandler.bind(this), this._massInputForm); + this._eventManager.registerListeners([submitEv, keyPressEv]); Array.from(this._element.querySelectorAll(MASS_INPUT_ADD_CELL_SELECTOR)).forEach(this._setupChangedHandlers.bind(this)); @@ -76,14 +82,16 @@ export class MassInput { destroy() { this._reset(); + this._eventManager.cleanUp(); + this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS); } _setupChangedHandlers(addCell) { Array.from(addCell.querySelectorAll(MASS_INPUT_ADD_CHANGE_FIELD_SELECTOR)).forEach(inputElem => { if (inputElem.closest('[uw-mass-input]') !== this._element) return; - - inputElem.addEventListener('change', () => { this._changedAdd.push(addCell); }); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, (() => { this._changedAdd.push(addCell); }).bind(this), inputElem); + this._eventManager.registerNewListener(changeEv); }); } @@ -207,13 +215,13 @@ export class MassInput { _setupSubmitButton(button) { button.setAttribute('type', 'button'); button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS); - button.addEventListener('click', this._massInputFormSubmitHandler); + const buttonClickEv = new EventWrapper(EVENT_TYPE.CLICK, this._massInputFormSubmitHandler.bind(this), button); + this._eventManager.registerNewListener(buttonClickEv); } _resetSubmitButton(button) { button.setAttribute('type', 'submit'); button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS); - button.removeEventListener('click', this._massInputFormSubmitHandler); } _processResponse(responseElement) { @@ -268,9 +276,6 @@ export class MassInput { } _reset() { - this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS); - this._massInputForm.removeEventListener('submit', this._massInputFormSubmitHandler); - this._massInputForm.removeEventListener('keypress', this._keypressHandler); const buttons = this._getMassInputSubmitButtons(); buttons.forEach((button) => { diff --git a/frontend/src/utils/modal/modal.js b/frontend/src/utils/modal/modal.js index c67b13ac7..8f013572f 100644 --- a/frontend/src/utils/modal/modal.js +++ b/frontend/src/utils/modal/modal.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './modal.sass'; const MODAL_HEADERS = { @@ -28,12 +29,16 @@ const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open'; }) export class Modal { + _eventManager + _element; _app; _modalsWrapper; _modalOverlay; _modalUrl; + _triggerElement; + _closerElement; constructor(element, app) { if (!element) { @@ -42,6 +47,7 @@ export class Modal { this._element = element; this._app = app; + this._eventManager = new EventManager(); if (this._element.classList.contains(MODAL_INITIALIZED_CLASS)) { return false; @@ -66,7 +72,16 @@ export class Modal { } destroy() { - // TODO + this._eventManager.cleanUp(); + if (this._closerElement !== undefined) + this._closerElement.remove(); + if(this._triggerElement !== undefined) + this._triggerElement.classList.remove(MODAL_TRIGGER_CLASS); + if(this._modalsWrapper !== undefined) + this._modalsWrapper.remove(); + if(this._modalOverlay !== undefined) + this._modalOverlay.remove(); + this._element.classList.remove(MODAL_INITIALIZED_CLASS, MODAL_CLASS); } _ensureModalWrapper() { @@ -92,23 +107,26 @@ export class Modal { if (!triggerSelector.startsWith('#')) { triggerSelector = '#' + triggerSelector; } - const triggerElement = document.querySelector(triggerSelector); + this._triggerElement = document.querySelector(triggerSelector); - if (!triggerElement) { + if (!this._triggerElement) { throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"'); } - triggerElement.classList.add(MODAL_TRIGGER_CLASS); - triggerElement.addEventListener('click', this._onTriggerClicked, false); - this._modalUrl = triggerElement.getAttribute('href'); + this._triggerElement.classList.add(MODAL_TRIGGER_CLASS); + const triggerEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onTriggerClicked.bind(this), this._triggerElement, false); + this._eventManager.registerNewListener(triggerEvent); + this._modalUrl = this._triggerElement.getAttribute('href'); } _setupCloser() { - const closerElement = document.createElement('div'); - this._element.insertBefore(closerElement, null); - closerElement.classList.add(MODAL_CLOSER_CLASS); - closerElement.addEventListener('click', this._onCloseClicked, false); - this._modalOverlay.addEventListener('click', this._onCloseClicked, false); + this._closerElement = document.createElement('div'); + this._element.insertBefore(this._closerElement, null); + this._closerElement.classList.add(MODAL_CLOSER_CLASS); + const closerElEvent = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._closerElement, false); + this._eventManager.registerNewListener(closerElEvent); + const overlayClose = new EventWrapper(EVENT_TYPE.CLICK, this._onCloseClicked.bind(this), this._modalOverlay, false); + this._eventManager.registerNewListener(overlayClose); } _onTriggerClicked = (event) => { @@ -146,6 +164,7 @@ export class Modal { this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS); document.removeEventListener('keyup', this._onKeyUp); + this._app.utilRegistry.destroyAll(this._element); }; _fillModal(url) { diff --git a/frontend/src/utils/navbar/navbar.js b/frontend/src/utils/navbar/navbar.js index f31ba77bd..08c11428c 100644 --- a/frontend/src/utils/navbar/navbar.js +++ b/frontend/src/utils/navbar/navbar.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './navbar.sass'; import * as throttle from 'lodash.throttle'; @@ -18,6 +19,8 @@ export class NavHeaderContainerUtil { _throttleUpdateWasOpen; + _eventManager; + constructor(element) { if (!element) { throw new Error('Navbar Header Container utility needs to be passed an element!'); @@ -29,6 +32,9 @@ export class NavHeaderContainerUtil { this._element = element; this.radioButton = document.getElementById(`${this._element.id}-radio`); + + this._eventManager = new EventManager(); + if (!this.radioButton) { throw new Error('Navbar Header Container utility could not find associated radio button!'); } @@ -58,8 +64,9 @@ export class NavHeaderContainerUtil { if (!this.container) return; - window.addEventListener('click', this.clickHandler.bind(this)); - this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this.clickHandler.bind(this), window); + const changeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton); + this._eventManager.registerListeners([clickEv, changeEv]); } clickHandler() { @@ -81,7 +88,10 @@ export class NavHeaderContainerUtil { this.wasOpen = this.isOpen(); } - destroy() { /* TODO */ } + destroy() { + this._eventManager.cleanUp(); + this._element.classList.remove(HEADER_CONTAINER_INITIALIZED_CLASS); + } } diff --git a/frontend/src/utils/pageactions/pageactions.js b/frontend/src/utils/pageactions/pageactions.js index 7c2334b6e..88084636b 100644 --- a/frontend/src/utils/pageactions/pageactions.js +++ b/frontend/src/utils/pageactions/pageactions.js @@ -1,4 +1,5 @@ import { Utility } from '../../core/utility'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './pageactions.sass'; import * as throttle from 'lodash.throttle'; @@ -17,9 +18,12 @@ export class PageActionSecondaryUtil { closeButton; container; wasOpen; + _closer; _throttleUpdateWasOpen; + _eventManager; + constructor(element) { if (!element) { throw new Error('Pageaction Secondary utility needs to be passed an element!'); @@ -31,6 +35,8 @@ export class PageActionSecondaryUtil { this._element = element; + this._eventManager = new EventManager(); + const childContainer = this._element.querySelector('.pagenav-item__children'); if (!childContainer) { @@ -43,7 +49,7 @@ export class PageActionSecondaryUtil { const links = Array.from(this._element.querySelectorAll('.pagenav-item__link')).filter(l => !childContainer.contains(l)); if (!links || Array.from(links).length !== 1) { - throw new Error('Pageaction Secondary utility could not find associated link!'); + throw new Error('Pageaction Secondary utility could not find associated link!'); } this.navIdent = links[0].id; } @@ -71,9 +77,9 @@ export class PageActionSecondaryUtil { throw new Error('Pageaction Secondary utility could not find associated container!'); } - const closer = this._element.querySelector('.pagenav-item__close-label'); - if (closer) { - closer.classList.add('pagenav-item__close-label--hidden'); + this._closer = this._element.querySelector('.pagenav-item__close-label'); + if (this._closer) { + this._closer.classList.add('pagenav-item__close-label--hidden'); } this.updateWasOpen(); @@ -85,12 +91,12 @@ export class PageActionSecondaryUtil { start() { if (!this.container) return; - - window.addEventListener('click', this.clickHandler.bind(this)); - this.radioButton.addEventListener('change', this.throttleUpdateWasOpen.bind(this)); + const windowClickEv = new EventWrapper(EVENT_TYPE.CLICK, ((event) => this.clickHandler(event)).bind(this), window); + const radioButtonChangeEv = new EventWrapper(EVENT_TYPE.CHANGE, this.throttleUpdateWasOpen.bind(this), this.radioButton); + this._eventManager.registerListeners([windowClickEv, radioButtonChangeEv]); } - clickHandler() { + clickHandler(event) { if (!this.container.contains(event.target) && window.document.contains(event.target) && this.wasOpen) { this.close(); } @@ -109,7 +115,12 @@ export class PageActionSecondaryUtil { this.wasOpen = this.isOpen(); } - destroy() { /* TODO */ } + destroy() { + this._eventManager.cleanUp(); + if(this._closer && this._closer.classList.contains('pagenav-item__close-label--hidden')) + this._closer.classList.remove('pagenav-item__close-label--hidden'); + this._element.classList.remove(PAGEACTION_SECONDARY_INITIALIZED_CLASS); + } } diff --git a/frontend/src/utils/show-hide/show-hide.js b/frontend/src/utils/show-hide/show-hide.js index 3419ee1f4..5739baac9 100644 --- a/frontend/src/utils/show-hide/show-hide.js +++ b/frontend/src/utils/show-hide/show-hide.js @@ -1,5 +1,6 @@ import { Utility } from '../../core/utility'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; import './show-hide.sass'; const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE'; @@ -16,6 +17,7 @@ export class ShowHide { _showHideId; _element; + _eventManager; _storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL }); constructor(element) { @@ -24,13 +26,15 @@ export class ShowHide { } this._element = element; + this._eventManager = new EventManager(); if (this._element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) { return false; } // register click listener - this._addClickListener(); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._clickHandler.bind(this), this._element); + this._eventManager.registerNewListener(clickEv); // param showHideId if (this._element.dataset.showHideId) { @@ -58,17 +62,18 @@ export class ShowHide { } this._checkHash(); - - window.addEventListener('hashchange', this._checkHash.bind(this)); + const hashChangeEv = new EventWrapper(EVENT_TYPE.HASH_CHANGE, this._checkHash.bind(this), window); + this._eventManager.registerNewListener(hashChangeEv); // mark as initialized this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS); } - destroy() {} - - _addClickListener() { - this._element.addEventListener('click', this._clickHandler.bind(this)); + destroy() { + this._eventManager.cleanUp(); + if (this._element.parentElement.classList.contains(SHOW_HIDE_COLLAPSED_CLASS)) + this._element.parentElement.classList.remove(SHOW_HIDE_COLLAPSED_CLASS); + this._element.classList.remove(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS); } _show() { diff --git a/frontend/src/utils/sort-table/sort-table.js b/frontend/src/utils/sort-table/sort-table.js index 3c43a9ee2..639664685 100644 --- a/frontend/src/utils/sort-table/sort-table.js +++ b/frontend/src/utils/sort-table/sort-table.js @@ -21,7 +21,7 @@ export class SortTable { } destroy() { - console.log('TBD destroy SortTable'); + this._storageManager.clear(); } } diff --git a/frontend/src/utils/tooltips/tooltips.js b/frontend/src/utils/tooltips/tooltips.js index 12d628f35..99b144f7e 100644 --- a/frontend/src/utils/tooltips/tooltips.js +++ b/frontend/src/utils/tooltips/tooltips.js @@ -1,6 +1,7 @@ import { Utility } from '../../core/utility'; import './tooltips.sass'; import { MovementObserver } from '../../lib/movement-observer/movement-observer'; +import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager'; const TOOLTIP_CLASS = 'tooltip'; const TOOLTIP_INITIALIZED_CLASS = 'tooltip--initialized'; @@ -17,6 +18,7 @@ export class Tooltip { _content; _movementObserver; + _eventManager; _openedPersistent = false; @@ -45,16 +47,19 @@ export class Tooltip { this._element = element; this._handle = element.querySelector('.tooltip__handle') || element; + this._eventManager = new EventManager(); + this._movementObserver = new MovementObserver(this._handle, { leadingCallback: this.close.bind(this) }); element.classList.add(TOOLTIP_INITIALIZED_CLASS); } start() { - this._element.addEventListener('mouseover', () => { this.open(false); }); - this._element.addEventListener('mouseout', this._leave.bind(this)); - this._content.addEventListener('mouseout', this._leave.bind(this)); - this._element.addEventListener('click', this._click.bind(this)); + const mouseOverEv = new EventWrapper(EVENT_TYPE.MOUSE_OVER, (() => { this.open(false); }).bind(this), this._element); + const mouseOutEv = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._element); + const contentMouseOut = new EventWrapper(EVENT_TYPE.MOUSE_OUT, (this._leave.bind(this)).bind(this), this._content); + const clickEv = new EventWrapper(EVENT_TYPE.CLICK, this._click.bind(this), this._element); + this._eventManager.registerListeners([mouseOverEv, mouseOutEv, contentMouseOut, clickEv]); } open(persistent) { @@ -183,5 +188,10 @@ export class Tooltip { } - destroy() {} + destroy() { + this._eventManager.cleanUp(); + this._movementObserver.unobserve(); + const toolTipsRegex = RegExp(/\btooltip--.+\b/, 'g'); + this._element.className = this._element.className.replace(toolTipsRegex, ''); + } }; diff --git a/messages/uniworx/categories/courses/courses/de-de-formal.msg b/messages/uniworx/categories/courses/courses/de-de-formal.msg index 92823ea08..2e1880882 100644 --- a/messages/uniworx/categories/courses/courses/de-de-formal.msg +++ b/messages/uniworx/categories/courses/courses/de-de-formal.msg @@ -187,6 +187,7 @@ LecturerFor: Dozent:in LecturersFor: Dozierende AssistantFor: Assistent:in AssistantsFor: Assistent:innen +CourseAdminFor: Kursadministration TutorsFor n@Int: #{pluralDE n "Tutor:in" "Tutor:innen"} CorrectorsFor n@Int: #{pluralDE n "Korrektor:in" "Korrektor:innen"} CourseParticipantsHeading: Kursteilnehmer:innen diff --git a/messages/uniworx/categories/courses/courses/en-eu.msg b/messages/uniworx/categories/courses/courses/en-eu.msg index da740b3ae..c4eda4efc 100644 --- a/messages/uniworx/categories/courses/courses/en-eu.msg +++ b/messages/uniworx/categories/courses/courses/en-eu.msg @@ -187,6 +187,7 @@ LecturerFor: Lecturer LecturersFor: Lecturers AssistantFor: Assistant AssistantsFor: Assistants +CourseAdminFor: Course administration TutorsFor n: #{pluralEN n "Tutor" "Tutors"} CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"} CourseParticipantsHeading: Course participants diff --git a/package-lock.json b/package-lock.json index 38b149cfa..3113011ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.21.0", + "version": "25.22.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 727983d52..ae47ea5a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "25.21.0", + "version": "25.22.4", "description": "", "keywords": [], "author": "", diff --git a/package.yaml b/package.yaml index 2793c89b4..43bc3cbc1 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 25.21.0 +version: 25.22.4 dependencies: - base - yesod diff --git a/routes b/routes index c7299e84c..8051d646f 100644 --- a/routes +++ b/routes @@ -78,7 +78,7 @@ /global-workflows/instances/#WorkflowInstanceName GlobalWorkflowInstanceR: /edit GWIEditR GET POST /delete GWIDeleteR GET POST - /workflows GWIWorkflowsR GET !¬empty + /workflows GWIWorkflowsR GET !free /initiate GWIInitiateR GET POST !workflow /update GWIUpdateR POST /global-workflows GlobalWorkflowWorkflowListR GET !free @@ -145,7 +145,7 @@ /workflows/instances/#WorkflowInstanceName SchoolWorkflowInstanceR: /edit SWIEditR GET POST /delete SWIDeleteR GET POST - /workflows SWIWorkflowsR GET !¬empty + /workflows SWIWorkflowsR GET !free /initiate SWIInitiateR GET POST !workflow /update SWIUpdateR POST /workflows SchoolWorkflowWorkflowListR GET !free diff --git a/src/Foundation/Navigation.hs b/src/Foundation/Navigation.hs index 28303797b..a1133b8e3 100644 --- a/src/Foundation/Navigation.hs +++ b/src/Foundation/Navigation.hs @@ -573,8 +573,8 @@ navLinkAccess NavLink{..} = case navAccess' of defaultLinks :: ( MonadHandler m , HandlerSite m ~ UniWorX - , MonadThrow m - , WithRunDB SqlReadBackend (HandlerFor UniWorX) m + -- , MonadThrow m + -- , WithRunDB SqlReadBackend (HandlerFor UniWorX) m , BearerAuthSite UniWorX ) => m [Nav] defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header. @@ -761,12 +761,14 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , do guardVolatile clusterVolatileWorkflowsEnabled - authCtx <- getAuthContext - (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) - <$> haveTopWorkflowInstances - <*> haveTopWorkflowWorkflows + -- authCtx <- getAuthContext + -- (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,) + -- <$> haveTopWorkflowInstances + -- <*> haveTopWorkflowWorkflows - if | haveInstances -> return NavHeader + mUserId <- maybeAuthId + -- if | haveInstances -> return NavHeader + if | isJust mUserId -> return NavHeader { navHeaderRole = NavHeaderPrimary , navIcon = IconMenuWorkflows , navLink = NavLink @@ -778,18 +780,18 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the , navForceActive = False } } - | haveWorkflows -> return NavHeader - { navHeaderRole = NavHeaderPrimary - , navIcon = IconMenuWorkflows - , navLink = NavLink - { navLabel = MsgMenuTopWorkflowWorkflowListHeader - , navRoute = TopWorkflowWorkflowListR - , navAccess' = NavAccessTrue - , navType = NavTypeLink { navModal = False } - , navQuick' = mempty - , navForceActive = False - } - } + -- | haveWorkflows -> return NavHeader + -- { navHeaderRole = NavHeaderPrimary + -- , navIcon = IconMenuWorkflows + -- , navLink = NavLink + -- { navLabel = MsgMenuTopWorkflowWorkflowListHeader + -- , navRoute = TopWorkflowWorkflowListR + -- , navAccess' = NavAccessTrue + -- , navType = NavTypeLink { navModal = False } + -- , navQuick' = mempty + -- , navForceActive = False + -- } + -- } | otherwise -> mzero , return NavHeaderContainer { navHeaderRole = NavHeaderPrimary @@ -2730,34 +2732,35 @@ haveWorkflowWorkflows rScope = hoist liftHandler . withReaderT (projectBackend @ lift $ anyM roles evalRole -haveTopWorkflowInstances, haveTopWorkflowWorkflows +-- haveTopWorkflowInstances, +haveTopWorkflowWorkflows :: ( MonadHandler m, HandlerSite m ~ UniWorX , BackendCompatible SqlReadBackend backend , BearerAuthSite UniWorX ) => ReaderT backend m Bool -haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do - roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do - let - getInstances = E.selectSource . E.from $ \workflowInstance -> do - E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope - return workflowInstance - instanceRoles (Entity _ WorkflowInstance{..}) = do - rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope - wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph - return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do - WGN{..} <- wiGraph ^.. _wgNodes . folded - WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded - return wgeActors - runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles - - let - evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool - evalRole ((rScope, win), role) = do - let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) - is _Authorized <$> hasWorkflowRole Nothing role route False - - lift $ anyM roles evalRole +-- haveTopWorkflowInstances = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do +-- roles <- memcachedBy @(Set ((RouteWorkflowScope, WorkflowInstanceName), WorkflowRole UserId)) (Just $ Right diffDay) NavCacheHaveTopWorkflowInstancesRoles $ do +-- let +-- getInstances = E.selectSource . E.from $ \workflowInstance -> do +-- E.where_ . isTopWorkflowScopeSql $ workflowInstance E.^. WorkflowInstanceScope +-- return workflowInstance +-- instanceRoles (Entity _ WorkflowInstance{..}) = do +-- rScope <- toRouteWorkflowScope $ _DBWorkflowScope # workflowInstanceScope +-- wiGraph <- lift $ getSharedIdWorkflowGraph workflowInstanceGraph +-- return . Set.mapMonotonic ((rScope, workflowInstanceName), ) . fold $ do +-- WGN{..} <- wiGraph ^.. _wgNodes . folded +-- WorkflowGraphEdgeInitial{..} <- wgnEdges ^.. folded +-- return wgeActors +-- runConduit $ transPipe lift getInstances .| C.foldMapM instanceRoles +-- +-- let +-- evalRole :: _ -> ReaderT SqlReadBackend (HandlerFor UniWorX) Bool +-- evalRole ((rScope, win), role) = do +-- let route = _WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR) +-- is _Authorized <$> hasWorkflowRole Nothing role route False +-- +-- lift $ anyM roles evalRole haveTopWorkflowWorkflows = hoist liftHandler . withReaderT (projectBackend @SqlReadBackend) . $cachedHere . maybeT (return False) $ do roles <- memcachedBy (Just $ Right diffDay) NavCacheHaveTopWorkflowWorkflowsRoles $ do let diff --git a/src/Handler/Course/Show.hs b/src/Handler/Course/Show.hs index fab484c0b..1f25a0b29 100644 --- a/src/Handler/Course/Show.hs +++ b/src/Handler/Course/Show.hs @@ -30,7 +30,7 @@ getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCShowR tid ssh csh = do mbAid <- maybeAuthId now <- liftIO getCurrentTime - (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) <- runDB . maybeT notFound $ do + (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister,(mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) <- runDB . maybeT notFound $ do [(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration, E.Value hasAllocationRegistrationOpen)] <- lift . E.select . E.from $ \((school `E.InnerJoin` course) `E.LeftOuterJoin` allocation `E.LeftOuterJoin` participant) -> do @@ -62,10 +62,10 @@ getCShowR tid ssh csh = do E.orderBy [ E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName ] return ( lecturer E.^. LecturerType , user E.^. UserDisplayEmail, user E.^. UserDisplayName, user E.^. UserSurname) - let partStaff :: (LecturerType, UserEmail, Text, Text) -> Either (UserEmail, Text, Text) (UserEmail, Text, Text) - partStaff (CourseLecturer ,name,surn,mail) = Right (name,surn,mail) - partStaff (_courseAssistant,name,surn,mail) = Left (name,surn,mail) - (assistants,lecturers) = partitionWith partStaff $ map $(unValueN 4) staff + let + (administrators', regularStaff) = partition ((==) CourseAdministrator . view _1) $ map (\(E.Value lecType, E.Value lecName, E.Value lecSurn, E.Value lecMail) -> (lecType,(lecName,lecSurn,lecMail))) staff + (lecturers', assistants') = partition ((==) CourseLecturer . view _1) regularStaff + (administrators, lecturers, assistants) = (view _2 <$> administrators', view _2 <$> lecturers', view _2 <$> assistants') correctors <- fmap (map $(unValueN 3)) . lift . E.select $ E.from $ \(sheet `E.InnerJoin` sheetCorrector `E.InnerJoin` user) -> E.distinctOnOrderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName, E.asc $ user E.^. UserEmail ] $ do E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId @@ -142,7 +142,7 @@ getCShowR tid ssh csh = do return $ material E.^. MaterialName mayViewAnyMaterial <- lift . anyM materials $ \(E.Value mnm) -> hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR - return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) + return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,administrators,correctors,tutors,mAllocation,mApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister, (mayViewSheets, mayViewAnySheet), (mayViewMaterials, mayViewAnyMaterial)) let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course mDereg <- traverse (formatTime SelFormatDateTime) mDereg' diff --git a/src/Handler/Tutorial/Users.hs b/src/Handler/Tutorial/Users.hs index f8215a0d9..eb15e4e84 100644 --- a/src/Handler/Tutorial/Users.hs +++ b/src/Handler/Tutorial/Users.hs @@ -67,6 +67,12 @@ postTUsersR tid ssh csh tutn = do ] addMessageI Success $ MsgTutorialUsersDeregistered nrDel redirect $ CTutorialR tid ssh csh tutn TUsersR + + tutors <- runDB $ + E.select $ E.from $ \(tutor `E.InnerJoin` user) -> do + E.on $ tutor E.^. TutorUser E.==. user E.^. UserId + E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid + return user let heading = prependCourseTitle tid ssh csh $ CI.original tutorialName siteLayoutMsg heading $ do diff --git a/templates/course.hamlet b/templates/course.hamlet index 2205d1f73..de6452829 100644 --- a/templates/course.hamlet +++ b/templates/course.hamlet @@ -93,6 +93,13 @@ $# #{summary}
- Bitte bedenken Sie beim Stellen Ihrer Anfrage, dass das # - Uni2work-Kernteam aktuell aus Sarah Vaupel und Gregor Kleen besteht # - und zwei Personen nicht hinreichend sind um in allen Fällen eine # - zeitnahe Bearbeitung Ihres Anliegens zu garantieren. + Bitte bedenken Sie beim Stellen Ihrer Anfrage, dass das Uni2work-Kernteam aus # + Sarah Vaupel # + besteht und # + eine Person nicht hinreichend ist, # + um in allen Fällen eine zeitnahe Bearbeitung Ihres Anliegens zu garantieren.
Falls sich Ihr Anliegen auf eine konkrete Veranstaltung bezieht, # ziehen Sie bitte auch in Betracht (insbesondere bei zeitkritischen # - Anliegen wie z.B. Abgaben) sich direkt an die Kursverwalter zu # - wenden. + Anliegen wie z.B. Abgaben) sich direkt an die Kursverwalter zu wenden. diff --git a/templates/i18n/help-instructions/en-eu.hamlet b/templates/i18n/help-instructions/en-eu.hamlet index 65e205bae..fe2b19102 100644 --- a/templates/i18n/help-instructions/en-eu.hamlet +++ b/templates/i18n/help-instructions/en-eu.hamlet @@ -2,14 +2,16 @@ $newline never
- When formulating your request please consider that the Uni2work core # - team currently consists of Sarah Vaupel and Gregor Kleen and that # - two people are not enough to guarantee a timely answer in all cases. + When formulating your request, please consider that the Uni2work core team consists of # + Sarah Vaupel # + and that # + one person is # + not enough to guarantee a timely answer in all cases.
If your request is related to a specific course, please also # consider contacting the relevant course administrators as well. # - Especially if your request is time sensitive (e.g. submitting for an # - exercise sheet). + Especially if your request is time sensitive (e.g. submitting for # + an exercise sheet). diff --git a/templates/i18n/imprint/de-de-formal.hamlet b/templates/i18n/imprint/de-de-formal.hamlet index d415a5780..af4c29eca 100644 --- a/templates/i18n/imprint/de-de-formal.hamlet +++ b/templates/i18n/imprint/de-de-formal.hamlet @@ -3,12 +3,12 @@ $newline never