Merge branch 'master' into 728-frontend-util-fur-checken-einer-reihe-von-checkboxes-via-shift-klick

This commit is contained in:
Sarah Vaupel 2021-11-17 17:20:32 +01:00
commit 6efa53b251
60 changed files with 1129 additions and 377 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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');
}

View File

@ -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) {

View File

@ -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);
});
});
});
});

View File

@ -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);
}
}

View File

@ -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' })

View File

@ -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() {

View File

@ -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();

View File

@ -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);
});
}

View File

@ -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();

View File

@ -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);
});
}

View File

@ -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();

View File

@ -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 });
// }
}

View File

@ -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);
});
});

View File

@ -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() {

View File

@ -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]));
});
});

View File

@ -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) {

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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 = () => {

View File

@ -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();
});
});

View File

@ -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);
}
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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:
<input type="text" value="" uw-enter-as-tab="" class=" enter-as-tab--initialized">

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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() {

View File

@ -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.

View File

@ -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);
});
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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) => {

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -21,7 +21,7 @@ export class SortTable {
}
destroy() {
console.log('TBD destroy SortTable');
this._storageManager.clear();
}
}

View File

@ -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, '');
}
};

View File

@ -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

View File

@ -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

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "25.21.0",
"version": "25.22.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "25.21.0",
"version": "25.22.4",
"description": "",
"keywords": [],
"author": "",

View File

@ -1,5 +1,5 @@
name: uniworx
version: 25.21.0
version: 25.22.4
dependencies:
- base
- yesod

4
routes
View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -93,6 +93,13 @@ $# #{summary}
<ul .list--inline .list--comma-separated>
$forall assi <- assistants
<li>^{nameEmailWidget' assi}
$with numadmins <- length administrators
$if numadmins /= 0
<dt .deflist__dt>_{MsgCourseAdminFor}
<dd .deflist__dd>
<ul .list--inline .list--comma-separated>
$forall admin <- administrators
<li>^{nameEmailWidget' admin}
$with numtutor <- length tutors
$if numtutor /= 0

View File

@ -2,14 +2,14 @@ $newline never
<p>
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.
<p>
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.

View File

@ -2,14 +2,16 @@ $newline never
<p>
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.
<p>
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).

View File

@ -3,12 +3,12 @@ $newline never
<h4>Inhalt
<ul style="list-style-type: none">
<li>Gregor Kleen & Sarah Vaupel
<li>Sarah Vaupel
<li>Oettingenstraße 67
<li>D-80538 München
<li>Raum L101
<li>E-Mail: ^{mailtoHtml "uni2work@ifi.lmu.de"}
<li>Telefon (Gregor Kleen): +49 (0) 89 / 2180 - 9139
<li>Telefon (Sarah Vaupel): —
<li>Telefon: +49 (0) 89 / 2180 - 9139
<h4>Jugendschutz
<ul style="list-style-type: none">

View File

@ -3,12 +3,12 @@ $newline never
<h4>Contents
<ul style="list-style-type: none">
<li>Gregor Kleen & Sarah Vaupel
<li>Sarah Vaupel
<li>Oettingenstraße 67
<li>D-80538 München (Germany)
<li>Room L101
<li>E-Mail: ^{mailtoHtml "uni2work@ifi.lmu.de"}
<li>Telefon (Gregor Kleen): +49 (0) 89 / 2180 - 9139
<li>Telefon (Sarah Vaupel): —
<li>Phone: +49 (0) 89 / 2180 - 9139
<h4>Youth Protection
<ul style="list-style-type: none">

View File

@ -1,2 +1,13 @@
$newline never
<section>
<dl .deflist>
<dt .deflist__dt>_{MsgTableTutorialTime}
<dd .deflist__dd>
^{occurrencesWidget tutorialTime}
<dt .deflist__dt>_{MsgTableTutorialTutors}
<dd .deflist__dd>
<ul>
$forall (Entity _ User{userDisplayName, userDisplayEmail, userSurname}) <- tutors
<li>
^{nameEmailWidget userDisplayEmail userDisplayName userSurname}
^{participantTable}