diff --git a/frontend/src/app.js b/frontend/src/app.js index 6c7fa215f..b2db86c31 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -2,6 +2,7 @@ import { HttpClient } from './services/http-client/http-client'; import { HtmlHelpers } from './services/html-helpers/html-helpers'; import { I18n } from './services/i18n/i18n'; import { UtilRegistry } from './services/util-registry/util-registry'; +import { isValidUtility } from './core/utility'; export class App { httpClient = new HttpClient(); @@ -11,6 +12,8 @@ export class App { constructor() { this.utilRegistry.setApp(this); + + document.addEventListener('DOMContentLoaded', () => this.utilRegistry.setupAll()); } registerUtilities(utils) { @@ -18,7 +21,7 @@ export class App { throw new Error('Utils are expected to be passed as array!'); } - utils.forEach((util) => { + utils.filter(isValidUtility).forEach((util) => { this.utilRegistry.register(util); }); } diff --git a/frontend/src/app.spec.js b/frontend/src/app.spec.js index 8ce962b2d..21a381a68 100644 --- a/frontend/src/app.spec.js +++ b/frontend/src/app.spec.js @@ -16,6 +16,12 @@ describe('App', () => { expect(app).toBeTruthy(); }); + it('should setup all utlites when page is done loading', () => { + spyOn(app.utilRegistry, 'setupAll'); + document.dispatchEvent(new Event('DOMContentLoaded')); + expect(app.utilRegistry.setupAll).toHaveBeenCalled(); + }); + describe('provides services', () => { it('HttpClient as httpClient', () => { expect(app.httpClient).toBeTruthy(); diff --git a/frontend/src/core/utility.js b/frontend/src/core/utility.js new file mode 100644 index 000000000..6b74920dd --- /dev/null +++ b/frontend/src/core/utility.js @@ -0,0 +1,22 @@ +export function isValidUtility(utility) { + if (!utility) { + return false; + } + + if (Utility.selector) { + return false; + } + + return true; +}; + +export function Utility(metadata) { + if (!metadata.selector) { + throw new Error('Utility needs to have a selector!'); + } + + return function (target) { + target.selector = metadata.selector; + target.isUtility = true; + }; +}; diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 18b656b8c..5e71554bc 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -6,10 +6,6 @@ export class UtilRegistry { _activeUtilInstances = []; _appInstance; - constructor() { - document.addEventListener('DOMContentLoaded', () => this.setupAll()); - } - /** * function registerUtil * @@ -63,14 +59,14 @@ export class UtilRegistry { console.log('setting up util', { util }); } - if (util && typeof util.setup === 'function') { + if (util) { const elements = this._findUtilElements(util, scope); elements.forEach((element) => { let utilInstance = null; try { - utilInstance = util.setup(element, this._appInstance); + utilInstance = new util(element, this._appInstance); } catch(err) { if (DEBUG_MODE > 0) { console.warn('Error while trying to initialize a utility!', { util , element, err }); diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js index e3f69f63c..2e4afedf2 100644 --- a/frontend/src/services/util-registry/util-registry.spec.js +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -21,12 +21,6 @@ describe('UtilRegistry', () => { expect(utilRegistry).toBeTruthy(); }); - it('should setup all utlites when page is done loading', () => { - spyOn(utilRegistry, 'setupAll'); - document.dispatchEvent(new Event('DOMContentLoaded')); - expect(utilRegistry.setupAll).toHaveBeenCalled(); - }); - describe('register()', () => { it('should allow to add utilities', () => { utilRegistry.register(TEST_UTILS[0]); diff --git a/frontend/src/utils/alerts/alerts.js b/frontend/src/utils/alerts/alerts.js index 3e49080bc..e7e04ddbb 100644 --- a/frontend/src/utils/alerts/alerts.js +++ b/frontend/src/utils/alerts/alerts.js @@ -1,191 +1,158 @@ +import { Utility } from '../../core/utility'; import './alerts.scss'; -/** - * - * Alerts Utility - * makes alerts interactive - * - * Attribute: uw-alerts - * - * Types of alerts: - * [default] - * Regular Info Alert - * Disappears automatically after 30 seconds - * Disappears after x seconds if explicitly specified via data-decay='x' - * Can be told not to disappear with data-decay='0' - * - * [success] - * Currently no special visual appearance - * Disappears automatically after 30 seconds - * - * [warning] - * Will be coloured warning-orange regardless of user's selected theme - * Does not disappear - * - * [error] - * Will be coloured error-red regardless of user's selected theme - * Does not disappear - * - * Example usage: - *
- *
- *
- *
- *
- *
- * This is some information - * - */ +const ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; +const ALERTS_ELEVATED_CLASS = 'alerts--elevated'; +const ALERTS_TOGGLER_CLASS = 'alerts__toggler'; +const ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; +const ALERTS_TOGGLER_APPEAR_DELAY = 120; -var ALERTS_UTIL_NAME = 'alerts'; -var ALERTS_UTIL_SELECTOR = '[uw-alerts]'; +const ALERT_CLASS = 'alert'; +const ALERT_INITIALIZED_CLASS = 'alert--initialized'; +const ALERT_CLOSER_CLASS = 'alert__closer'; +const ALERT_ICON_CLASS = 'alert__icon'; +const ALERT_CONTENT_CLASS = 'alert__content'; +const ALERT_INVISIBLE_CLASS = 'alert--invisible'; +const ALERT_AUTO_HIDE_DELAY = 10; +const ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success'; -var ALERTS_INITIALIZED_CLASS = 'alerts--initialized'; -var ALERTS_ELEVATED_CLASS = 'alerts--elevated'; -var ALERTS_TOGGLER_CLASS = 'alerts__toggler'; -var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible'; -var ALERTS_TOGGLER_APPEAR_DELAY = 120; +@Utility({ + selector: '[uw-alerts]', +}) +export class Alerts { + _togglerCheckRequested = false; + _togglerElement; + _alertElements; -var ALERT_CLASS = 'alert'; -var ALERT_INITIALIZED_CLASS = 'alert--initialized'; -var ALERT_CLOSER_CLASS = 'alert__closer'; -var ALERT_ICON_CLASS = 'alert__icon'; -var ALERT_CONTENT_CLASS = 'alert__content'; -var ALERT_INVISIBLE_CLASS = 'alert--invisible'; -var ALERT_AUTO_HIDE_DELAY = 10; -var ALERT_AUTOCLOSING_MATCHER = '.alert-info, .alert-success'; + _element; + _app; -var alertsUtil = function(element, app) { - var togglerCheckRequested = false; - var togglerElement; - var alertElements; - - function init() { + constructor(element, app) { if (!element) { throw new Error('Alerts util has to be called with an element!'); } - if (element.classList.contains(ALERTS_INITIALIZED_CLASS)) { + this._element = element; + this._app = app; + + if (this._element.classList.contains(ALERTS_INITIALIZED_CLASS)) { return false; } - togglerElement = element.querySelector('.' + ALERTS_TOGGLER_CLASS); - alertElements = gatherAlertElements(); + this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS); + this._alertElements = this._gatherAlertElements(); - initToggler(); - initAlerts(); + if (this._togglerElement) { + this._initToggler(); + } + + this._initAlerts(); // register http client interceptor to filter out Alerts Header - setupHttpInterceptor(); + this._setupHttpInterceptor(); // mark initialized - element.classList.add(ALERTS_INITIALIZED_CLASS); - - return { - name: ALERTS_UTIL_NAME, - element: element, - destroy: function() {}, - }; + this._element.classList.add(ALERTS_INITIALIZED_CLASS); } - function gatherAlertElements() { - return Array.from(element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) { + destroy() { + console.log('TBD: Destroy Alert'); + } + + _gatherAlertElements() { + return Array.from(this._element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) { return !alert.classList.contains(ALERT_INITIALIZED_CLASS); }); } - function initToggler() { - togglerElement.addEventListener('click', function() { - alertElements.forEach(function(alertEl) { - toggleAlert(alertEl, true); - }); - togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); + _initToggler() { + this._togglerElement.addEventListener('click', () => { + this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true)); + this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS); }); } - function initAlerts() { - alertElements.forEach(initAlert); + _initAlerts() { + this._alertElements.forEach((alert) => this._initAlert(alert)); } - function initAlert(alertElement) { - var autoHideDelay = ALERT_AUTO_HIDE_DELAY; + _initAlert(alertElement) { + let autoHideDelay = ALERT_AUTO_HIDE_DELAY; if (alertElement.dataset.decay) { autoHideDelay = parseInt(alertElement.dataset.decay, 10); } - var closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); - closeEl.addEventListener('click', function() { - toggleAlert(alertElement); + const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS); + closeEl.addEventListener('click', () => { + this._toggleAlert(alertElement); }); if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) { - window.setTimeout(function() { - toggleAlert(alertElement); - }, autoHideDelay * 1000); + window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000); } } - function toggleAlert(alertEl, visible) { + _toggleAlert(alertEl, visible) { alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible); - checkToggler(); + this._checkToggler(); } - function checkToggler() { - if (togglerCheckRequested) { + _checkToggler() { + if (this._togglerCheckRequested) { return; } - var alertsHidden = alertElements.reduce(function(acc, alert) { + const alertsHidden = this._alertElements.reduce(function(acc, alert) { return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS); }, true); - window.setTimeout(function() { - togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden); - togglerCheckRequested = false; + window.setTimeout(() => { + this._togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden); + this._togglerCheckRequested = false; }, ALERTS_TOGGLER_APPEAR_DELAY); } - function setupHttpInterceptor() { - app.httpClient.addResponseInterceptor(responseInterceptor.bind(this)); + _setupHttpInterceptor() { + this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this)); } - function elevateAlerts() { - element.classList.add(ALERTS_ELEVATED_CLASS); + _elevateAlerts() { + this._element.classList.add(ALERTS_ELEVATED_CLASS); } - function responseInterceptor(response) { - var alerts; - for (var header of response.headers) { + _responseInterceptor = (response) => { + let alerts; + for (const header of response.headers) { if (header[0] === 'alerts') { - var decodedHeader = decodeURIComponent(header[1]); + const decodedHeader = decodeURIComponent(header[1]); alerts = JSON.parse(decodedHeader); break; } } if (alerts) { - alerts.forEach(function(alert) { - var alertElement = createAlertElement(alert.status, alert.content); - element.appendChild(alertElement); - alertElements.push(alertElement); - initAlert(alertElement); + alerts.forEach((alert) => { + const alertElement = this._createAlertElement(alert.status, alert.content); + this._element.appendChild(alertElement); + this._alertElements.push(alertElement); + this._initAlert(alertElement); }); - elevateAlerts(); + this._elevateAlerts(); } } - function createAlertElement(type, content) { - var alertElement = document.createElement('div'); + _createAlertElement(type, content) { + const alertElement = document.createElement('div'); alertElement.classList.add(ALERT_CLASS, 'alert-' + type); - var alertCloser = document.createElement('div'); + const alertCloser = document.createElement('div'); alertCloser.classList.add(ALERT_CLOSER_CLASS); - var alertIcon = document.createElement('div'); + const alertIcon = document.createElement('div'); alertIcon.classList.add(ALERT_ICON_CLASS); - var alertContent = document.createElement('div'); + const alertContent = document.createElement('div'); alertContent.classList.add(ALERT_CONTENT_CLASS); alertContent.innerHTML = content; @@ -195,12 +162,4 @@ var alertsUtil = function(element, app) { return alertElement; } - - return init(); -}; - -export default { - name: ALERTS_UTIL_NAME, - selector: ALERTS_UTIL_SELECTOR, - setup: alertsUtil, -}; +} diff --git a/frontend/src/utils/alerts/alerts.md b/frontend/src/utils/alerts/alerts.md new file mode 100644 index 000000000..e3b36feed --- /dev/null +++ b/frontend/src/utils/alerts/alerts.md @@ -0,0 +1,35 @@ +# Alerts + +Makes alerts interactive. + +## Attribute: `uw-alerts` + +## Types of alerts: +- `default`\ + Regular Info Alert + Disappears automatically after 30 seconds + Disappears after x seconds if explicitly specified via data-decay='x' + Can be told not to disappear with data-decay='0' + +- `success`\ + Currently no special visual appearance + Disappears automatically after 30 seconds + +- `warning`\ + Will be coloured warning-orange regardless of user's selected theme + Does not disappear + +- `error`\ + Will be coloured error-red regardless of user's selected theme + Does not disappear + +## Example usage: +```html +
+
+
+
+
+
+ This is some information +``` diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js index 65a4c4a72..6d0440391 100644 --- a/frontend/src/utils/alerts/alerts.spec.js +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -1,8 +1,21 @@ -import alerts from "./alerts"; +import { Alerts } from "./alerts"; + +const MOCK_APP = { + httpClient: { + addResponseInterceptor: () => {}, + }, +}; describe('Alerts', () => { - it('should be called alerts', () => { - expect(alerts.name).toMatch('alerts'); + let alerts; + + beforeEach(() => { + const element = document.createElement('div'); + alerts = new Alerts(element, MOCK_APP); + }); + + it('should create', () => { + expect(alerts).toBeTruthy(); }); }); diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js index c5ba552c9..09d295063 100644 --- a/frontend/src/utils/asidenav/asidenav.js +++ b/frontend/src/utils/asidenav/asidenav.js @@ -1,89 +1,69 @@ +import { Utility } from '../../core/utility'; import './asidenav.scss'; -/** - * - * Asidenav Utility - * Correctly positions hovered asidenav submenus and handles the favorites button on mobile - * - * Attribute: uw-asidenav - * - * Example usage: - *
- *
- *
- *