diff --git a/.babelrc b/.babelrc index 352d48a80..857e7ba32 100644 --- a/.babelrc +++ b/.babelrc @@ -1,11 +1,9 @@ { "presets": [ - [ - "@babel/preset-env", - { - "useBuiltIns": "usage" - } - ] + ["@babel/preset-env", { "useBuiltIns": "usage" }] ], - "plugins": ["@babel/plugin-proposal-class-properties"] + "plugins": [ + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] + ] } diff --git a/.eslintrc.json b/.eslintrc.json index 8c0382cfa..6fcef7c27 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,12 +13,16 @@ }, "parser": "babel-eslint", "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2018, + "ecmaFeatures": { + "legacyDecorators": true + } }, "rules": { "no-console": "off", "no-extra-semi": "off", "semi": ["error", "always"], - "comma-dangle": ["error", "always-multiline"] + "comma-dangle": ["error", "always-multiline"], + "quotes": ["error", "single"] } } 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..247be9f00 100644 --- a/frontend/src/app.spec.js +++ b/frontend/src/app.spec.js @@ -1,8 +1,15 @@ -import { App } from "./app"; +import { App } from './app'; +import { Utility } from './core/utility'; + +@Utility({ selector: 'util1' }) +class TestUtil1 { } + +@Utility({ selector: 'util2' }) +class TestUtil2 { } const TEST_UTILS = [ - { name: 'util1' }, - { name: 'util2' }, + TestUtil1, + TestUtil2, ]; describe('App', () => { @@ -16,6 +23,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..dba52923a --- /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/main.js b/frontend/src/main.js index eb76d102e..aa509bdc3 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -15,9 +15,9 @@ window.App = app; // return; // } -// const contentType = response.headers.get("content-type"); +// const contentType = response.headers.get('content-type'); // if (!contentType.match(options.accept)) { -// throw new Error('Server returned with "' + contentType + '" when "' + options.accept + '" was expected'); +// throw new Error('Server returned with '' + contentType + '' when '' + options.accept + '' was expected'); // } // } diff --git a/frontend/src/services/html-helpers/html-helpers.js b/frontend/src/services/html-helpers/html-helpers.js index 92d3168b5..b8bf7771b 100644 --- a/frontend/src/services/html-helpers/html-helpers.js +++ b/frontend/src/services/html-helpers/html-helpers.js @@ -3,20 +3,21 @@ export class HtmlHelpers { // `parseResponse` takes a raw HttpClient response and an options object. // Returns an object with `element` being an contextual fragment of the // HTML in the response and `ifPrefix` being the prefix that was used to - // "unique-ify" the ids of the received HTML. + // 'unique-ify' the ids of the received HTML. // Original Response IDs can optionally be kept by adding `keepIds: true` // to the `options` object. parseResponse(response, options = {}) { return response.text() .then( (responseText) => { - const docFrag = document.createRange().createContextualFragment(responseText); + const element = document.createElement('div'); + element.innerHTML = responseText; let idPrefix = ''; if (!options.keepIds) { idPrefix = this._getIdPrefix(); - this._prefixIds(docFrag, idPrefix); + this._prefixIds(element, idPrefix); } - return Promise.resolve({ idPrefix, element: docFrag }); + return Promise.resolve({ idPrefix, element }); }, Promise.reject, ).catch(console.error); diff --git a/frontend/src/services/html-helpers/html-helpers.spec.js b/frontend/src/services/html-helpers/html-helpers.spec.js index c77015b26..f092e17fa 100644 --- a/frontend/src/services/html-helpers/html-helpers.spec.js +++ b/frontend/src/services/html-helpers/html-helpers.spec.js @@ -1,4 +1,4 @@ -import { HtmlHelpers } from "./html-helpers"; +import { HtmlHelpers } from './html-helpers'; describe('HtmlHelpers', () => { let htmlHelpers; diff --git a/frontend/src/services/http-client/http-client.spec.js b/frontend/src/services/http-client/http-client.spec.js index e61e248f0..a0f76584d 100644 --- a/frontend/src/services/http-client/http-client.spec.js +++ b/frontend/src/services/http-client/http-client.spec.js @@ -1,4 +1,4 @@ -import { HttpClient } from "./http-client"; +import { HttpClient } from './http-client'; const TEST_URL = 'http://example.com'; const FAKE_RESPONSE = { diff --git a/frontend/src/services/i18n/i18n.spec.js b/frontend/src/services/i18n/i18n.spec.js index 76e5348c0..1b4edf3c4 100644 --- a/frontend/src/services/i18n/i18n.spec.js +++ b/frontend/src/services/i18n/i18n.spec.js @@ -1,4 +1,4 @@ -import { I18n } from "./i18n"; +import { I18n } from './i18n'; describe('I18n', () => { let i18n; diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 18b656b8c..d96d7a4b3 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,16 @@ export class UtilRegistry { console.log('setting up util', { util }); } - if (util && typeof util.setup === 'function') { + let instances = []; + + 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 }); @@ -82,10 +80,13 @@ export class UtilRegistry { console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); } - this._activeUtilInstances.push(utilInstance); + instances.push(utilInstance); } }); } + + this._activeUtilInstances.push(...instances); + return instances; } find(name) { diff --git a/frontend/src/services/util-registry/util-registry.spec.js b/frontend/src/services/util-registry/util-registry.spec.js index e3f69f63c..5f29f6a3c 100644 --- a/frontend/src/services/util-registry/util-registry.spec.js +++ b/frontend/src/services/util-registry/util-registry.spec.js @@ -1,14 +1,5 @@ -import { UtilRegistry } from "./util-registry"; - -const TEST_UTILS = [{ - name: 'util1', - selector: '#some-id', - setup: () => {}, -}, { - name: 'util2', - selector: '[uw-util]', - setup: () => {}, -}]; +import { UtilRegistry } from './util-registry'; +import { Utility } from '../../core/utility'; describe('UtilRegistry', () => { let utilRegistry; @@ -21,31 +12,28 @@ 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]); + let foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toBeFalsy(); - const foundUtil = utilRegistry.find(TEST_UTILS[0].name); - expect(foundUtil).toEqual(TEST_UTILS[0]); + utilRegistry.register(TestUtil1); + + foundUtil = utilRegistry.find(TestUtil1.name); + expect(foundUtil).toEqual(TestUtil1); }); }); describe('deregister()', () => { it('should remove util', () => { // register util - utilRegistry.register(TEST_UTILS[0]); - let foundUtil = utilRegistry.find('util1'); + utilRegistry.register(TestUtil1); + let foundUtil = utilRegistry.find(TestUtil1.name); expect(foundUtil).toBeTruthy(); // deregister util - utilRegistry.deregister(TEST_UTILS[0].name); - foundUtil = utilRegistry.find('util1'); + utilRegistry.deregister(TestUtil1.name); + foundUtil = utilRegistry.find(TestUtil1.name); expect(foundUtil).toBeFalsy(); }); @@ -55,55 +43,62 @@ describe('UtilRegistry', () => { }); describe('setup()', () => { + it('should catch errors thrown by the utility', () => { - spyOn(TEST_UTILS[0], 'setup').and.throwError('some error'); expect(() => { - utilRegistry.setup(TEST_UTILS[0]); + utilRegistry.setup(ThrowingUtil); }).not.toThrow(); }); - it('should pass the app instance', () => { - const scope = document.createElement('div'); - const utilElement = document.createElement('div'); - utilElement.id = 'some-id'; - scope.appendChild(utilElement); - const fakeApp = { fn: () => {} }; - utilRegistry.setApp(fakeApp); - spyOn(TEST_UTILS[0], 'setup'); - utilRegistry.setup(TEST_UTILS[0], scope); - expect(TEST_UTILS[0].setup).toHaveBeenCalledWith(utilElement, fakeApp); - }); + describe('scope has no matching elements', () => { + it('should not construct an instance', () => { + const scope = document.createElement('div'); + const instances = utilRegistry.setup(TestUtil1, scope); + expect(instances.length).toBe(0); + }); - describe('given no scope', () => { it('should use fallback scope', () => { - spyOn(TEST_UTILS[0], 'setup'); - utilRegistry.setup(TEST_UTILS[0]); - expect(TEST_UTILS[0].setup).not.toHaveBeenCalled(); + const instances = utilRegistry.setup(TestUtil1); + expect(instances.length).toBe(0); }); }); - describe('given a scope', () => { - let scope; - let utilElement1; - let utilElement2; + describe('scope has matching elements', () => { + let testScope; + let testElement1; + let testElement2; beforeEach(() => { - scope = document.createElement('div'); - utilElement1 = document.createElement('div'); - utilElement2 = document.createElement('div'); - utilElement1.setAttribute('uw-util', ''); - utilElement2.setAttribute('uw-util', ''); - scope.appendChild(utilElement1); - scope.appendChild(utilElement2); + 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 call the utilities\' setup function for each matching element', () => { - spyOn(TEST_UTILS[1], 'setup'); - utilRegistry.setup(TEST_UTILS[1], scope); - // 2 matching elements in scope - expect(TEST_UTILS[1].setup.calls.count()).toBe(2); - expect(TEST_UTILS[1].setup.calls.argsFor(0)).toEqual([utilElement1, undefined]); - expect(TEST_UTILS[1].setup.calls.argsFor(1)).toEqual([utilElement2, undefined]); + it('should construct a utility instance', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0]).toBeTruthy(); + }); + + it('should construct an instance for each matching element', () => { + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].element).toBe(testElement1); + expect(setupUtilities[1].element).toBe(testElement2); + }); + + it('should pass the app instance', () => { + const fakeApp = { }; + utilRegistry.setApp(fakeApp); + + const setupUtilities = utilRegistry.setup(TestUtil1, testScope); + expect(setupUtilities).toBeTruthy(); + expect(setupUtilities[0].app).toBe(fakeApp); + expect(setupUtilities[1].app).toBe(fakeApp); }); }); }); @@ -111,22 +106,41 @@ describe('UtilRegistry', () => { describe('setupAll()', () => { it('should setup all the utilities', () => { spyOn(utilRegistry, 'setup'); - utilRegistry.register(TEST_UTILS[0]); - utilRegistry.register(TEST_UTILS[1]); + utilRegistry.register(TestUtil1); + utilRegistry.register(TestUtil2); utilRegistry.setupAll(); expect(utilRegistry.setup.calls.count()).toBe(2); - expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TEST_UTILS[0], undefined]); - expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TEST_UTILS[1], undefined]); + expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TestUtil1, undefined]); + expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TestUtil2, undefined]); }); it('should pass the given scope', () => { spyOn(utilRegistry, 'setup'); - utilRegistry.register(TEST_UTILS[0]); + utilRegistry.register(TestUtil1); const scope = document.createElement('div'); utilRegistry.setupAll(scope); - expect(utilRegistry.setup).toHaveBeenCalledWith(TEST_UTILS[0], scope); + expect(utilRegistry.setup).toHaveBeenCalledWith(TestUtil1, scope); }); }); }); + +// test utilities +@Utility({ selector: '.util1' }) +class TestUtil1 { + constructor(element, app) { + this.element = element; + this.app = app; + } +} + +@Utility({ selector: '#util2' }) +class TestUtil2 { } + +@Utility({ selector: '#throws' }) +class ThrowingUtil { + constructor() { + throw new Error(); + } + } 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.scss b/frontend/src/utils/alerts/alerts.scss index cd0492f11..d2faf1b22 100644 --- a/frontend/src/utils/alerts/alerts.scss +++ b/frontend/src/utils/alerts/alerts.scss @@ -20,7 +20,7 @@ &::before { content: '\f077'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; left: 50%; top: 0; height: 30px; @@ -126,7 +126,7 @@ &::before { content: '\f05a'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; font-size: 24px; top: 50%; left: 50%; @@ -164,7 +164,7 @@ &::before { content: '\f00d'; position: absolute; - font-family: "Font Awesome 5 Free"; + font-family: 'Font Awesome 5 Free'; top: 50%; left: 50%; display: flex; diff --git a/frontend/src/utils/alerts/alerts.spec.js b/frontend/src/utils/alerts/alerts.spec.js index 65a4c4a72..0b4749e97 100644 --- a/frontend/src/utils/alerts/alerts.spec.js +++ b/frontend/src/utils/alerts/alerts.spec.js @@ -1,8 +1,27 @@ -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(); + }); + + it('should throw if called without an element', () => { + expect(() => { + new Alerts(); + }).toThrow(); }); }); diff --git a/frontend/src/utils/asidenav/asidenav.js b/frontend/src/utils/asidenav/asidenav.js index c5ba552c9..2964e1617 100644 --- a/frontend/src/utils/asidenav/asidenav.js +++ b/frontend/src/utils/asidenav/asidenav.js @@ -1,89 +1,75 @@ +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: - *
- *
- *
- *