refactor js utilities to use class syntax and Utility decorator

This commit is contained in:
Felix Hamann 2019-05-28 00:19:39 +02:00
parent b31514d9c6
commit befa9f3941
44 changed files with 1564 additions and 1644 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
* <div .alerts uw-alerts>
* <div .alerts__toggler>
* <div .alert.alert-info>
* <div .alert__closer>
* <div .alert__icon>
* <div .alert__content>
* 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,
};
}

View File

@ -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
<div .alerts uw-alerts>
<div .alerts__toggler>
<div .alert.alert-info>
<div .alert__closer>
<div .alert__icon>
<div .alert__content>
This is some information
```

View File

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

View File

@ -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:
* <div uw-asidenav>
* <div .asidenav>
* <div .asidenav__box>
* <ul .asidenav__list.list--iconless>
* <li .asidenav__list-item>
* <a .asidenav__link-wrapper href="#">
* <div .asidenav__link-shorthand>EIP
* <div .asidenav__link-label>Einführung in die Programmierung
* <div .asidenav__nested-list-wrapper>
* <ul .asidenav__nested-list.list--iconless>
* Übungsblätter
* ...
*
*/
const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
const FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
const ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized';
const ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
const ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
const ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
var ASIDENAV_UTIL_NAME = 'asidenav';
var ASIDENAV_UTIL_SELECTOR = '[uw-asidenav]';
@Utility({
selector: '[uw-asidenav]',
})
export class Asidenav {
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
var FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
var ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized';
var ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
var ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
var ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
_element;
var asidenavUtil = function(element) {
function init() {
constructor(element) {
if (!element) {
throw new Error('Asidenav utility cannot be setup without an element!');
}
if (element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) {
this._element = element;
if (this._element.classList.contains(ASIDENAV_INITIALIZED_CLASS)) {
return false;
}
initFavoritesButton();
initAsidenavSubmenus();
this._initFavoritesButton();
this._initAsidenavSubmenus();
// mark initialized
element.classList.add(ASIDENAV_INITIALIZED_CLASS);
return {
name: ASIDENAV_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(ASIDENAV_INITIALIZED_CLASS);
}
function initFavoritesButton() {
var favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
destroy() {
console.log('TBD: Destroy Asidenav');
}
_initFavoritesButton() {
const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
favoritesBtn.addEventListener('click', function(event) {
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
event.preventDefault();
}, true);
}
function initAsidenavSubmenus() {
var asidenavLinksWithSubmenus = Array.from(element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
_initAsidenavSubmenus() {
const asidenavLinksWithSubmenus = Array.from(this._element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
.map(function(listItem) {
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
const submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
return { listItem, submenu };
}).filter(function(union) {
return union.submenu !== null;
});
asidenavLinksWithSubmenus.forEach(function(union) {
union.listItem.addEventListener('mouseover', createMouseoverHandler(union));
union.listItem.addEventListener('mouseover', this.createMouseoverHandler(union));
});
}
function createMouseoverHandler(union) {
return function mouseoverHanlder() {
var rectListItem = union.listItem.getBoundingClientRect();
var rectSubMenu = union.submenu.getBoundingClientRect();
_createMouseoverHandler(union) {
return function mouseoverHandler() {
const rectListItem = union.listItem.getBoundingClientRect();
const rectSubMenu = union.submenu.getBoundingClientRect();
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
if (window.innerHeight - rectListItem.top < rectSubMenu.height) {
@ -94,12 +74,4 @@ var asidenavUtil = function(element) {
};
}
return init();
};
export default {
name: ASIDENAV_UTIL_NAME,
selector: ASIDENAV_UTIL_SELECTOR,
setup: asidenavUtil,
};
}

View File

@ -0,0 +1,21 @@
# Asidenav
Correctly positions hovered asidenav submenus and handles the favorites button on mobile
## Attribute: `uw-asidenav`
## Example usage:
```html
<div uw-asidenav>
<div .asidenav>
<div .asidenav__box>
<ul .asidenav__list.list--iconless>
<li .asidenav__list-item>
<a .asidenav__link-wrapper href="#">
<div .asidenav__link-shorthand>EIP
<div .asidenav__link-label>Einführung in die Programmierung
<div .asidenav__nested-list-wrapper>
<ul .asidenav__nested-list.list--iconless>
Übungsblätter
...
```

View File

@ -1,74 +1,60 @@
import { Utility } from '../../core/utility';
import './async-form.scss';
/**
*
* Async Form Utility
* prevents form submissions from reloading the page but instead firing an AJAX request
*
* Attribute: uw-async-form
* (works only on <form> elements)
*
* Example usage:
* <form uw-async-form method='POST' action='...'>
* ...
*
* Internationalization:
* This utility expects the following translations to be available:
* asyncFormFailure: text that gets shown if an async form request fails
* example: "Oops. Something went wrong."
*/
const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
const ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
const ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
const ASYNC_FORM_MIN_DELAY = 600;
var ASYNC_FORM_UTIL_NAME = 'asyncForm';
var ASYNC_FORM_UTIL_SELECTOR = 'form[uw-async-form]';
const MODAL_SELECTOR = '.modal';
const MODAL_HEADER_KEY = 'Is-Modal';
const MODAL_HEADER_VALUE = 'True';
var ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
var ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
var ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
var ASYNC_FORM_MIN_DELAY = 600;
@Utility({
selector: 'form[uw-async-form]',
})
export class AsyncForm {
var MODAL_SELECTOR = '.modal';
var MODAL_HEADER_KEY = 'Is-Modal';
var MODAL_HEADER_VALUE = 'True';
_lastRequestTimestamp = 0;
_element;
_app;
var asyncFormUtil = function(element, app) {
var lastRequestTimestamp = 0;
function init() {
constructor(element, app) {
if (!element) {
throw new Error('Async Form Utility cannot be setup without an element!');
}
if (element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) {
this._element = element;
this._app = app;
if (this._element.classList.contains(ASYNC_FORM_INITIALIZED_CLASS)) {
return false;
}
element.addEventListener('submit', submitHandler);
this._element.addEventListener('submit', this._submitHandler);
element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
return {
name: ASYNC_FORM_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
}
function processResponse(response) {
var responseElement = makeResponseElement(response.content, response.status);
var parentElement = element.parentElement;
destroy() {
// TODO
}
_processResponse(response) {
const responseElement = this._makeResponseElement(response.content, response.status);
const parentElement = this._element.parentElement;
// make sure there is a delay between click and response
var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now());
const delay = Math.max(0, ASYNC_FORM_MIN_DELAY + this._lastRequestTimestamp - Date.now());
setTimeout(function() {
parentElement.insertBefore(responseElement, element);
element.remove();
setTimeout(() => {
parentElement.insertBefore(responseElement, this._element);
this._element.remove();
}, delay);
}
function makeResponseElement(content, status) {
var responseElement = document.createElement('div');
_makeResponseElement(content, status) {
const responseElement = document.createElement('div');
status = status || 'info';
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS + '--' + status);
@ -76,42 +62,34 @@ var asyncFormUtil = function(element, app) {
return responseElement;
}
function submitHandler(event) {
_submitHandler = (event) => {
event.preventDefault();
element.classList.add(ASYNC_FORM_LOADING_CLASS);
lastRequestTimestamp = Date.now();
this._element.classList.add(ASYNC_FORM_LOADING_CLASS);
this._lastRequestTimestamp = Date.now();
var url = element.getAttribute('action');
var headers = { };
var body = new FormData(element);
const url = this._element.getAttribute('action');
const headers = { };
const body = new FormData(this._element);
var isModal = element.closest(MODAL_SELECTOR);
const isModal = this._element.closest(MODAL_SELECTOR);
if (isModal) {
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
}
app.httpClient.post({
this._app.httpClient.post({
url: url,
headers: headers,
body: body,
}).then(function(response) {
return response.json();
}).then(function(response) {
processResponse(response[0]);
}).catch(function() {
var failureMessage = app.i18n.get('asyncFormFailure');
processResponse({ content: failureMessage });
}).then(
(response) => response.json()
).then(
(response) => this._processResponse(response[0])
).catch(() => {
const failureMessage = this._app.i18n.get('asyncFormFailure');
this._processResponse({ content: failureMessage });
element.classList.remove(ASYNC_FORM_LOADING_CLASS);
this._element.classList.remove(ASYNC_FORM_LOADING_CLASS);
});
}
return init();
};
export default {
name: ASYNC_FORM_UTIL_NAME,
selector: ASYNC_FORM_UTIL_SELECTOR,
setup: asyncFormUtil,
};
}

View File

@ -0,0 +1,17 @@
# Async Form Utility
Prevents form submissions from reloading the page but instead firing an AJAX request.
## Attribute: `uw-async-form`
(works only on `<form>` elements)
## Example usage:
```html
<form uw-async-form method='POST' action='...'>
...
```
## Internationalization:
This utility expects the following translations to be available:
- `asyncFormFailure`\
text that gets shown if an async form request fails (e.g. "Oops. Something went wrong.").

View File

@ -1,235 +1,227 @@
import { Utility } from '../../core/utility';
import { HttpClient } from '../../services/http-client/http-client';
import './async-table.scss';
import './async-table-filter.scss';
import './async-table.scss';
/**
*
* Async Table Utility
* makes table filters, sorting and pagination behave asynchronously via AJAX calls
*
* Attribute: uw-async-table
*
* Example usage:
* (regular table)
*/
const INPUT_DEBOUNCE = 600;
const HEADER_HEIGHT = 80;
var INPUT_DEBOUNCE = 600;
var HEADER_HEIGHT = 80;
const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
const ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
const ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
const ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
var ASYNC_TABLE_UTIL_NAME = 'asyncTable';
var ASYNC_TABLE_UTIL_SELECTOR = '[uw-async-table]';
const ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
const ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
var ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
var ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
var ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
var ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
@Utility({
selector: '[uw-async-table]',
})
export class AsyncTable {
var ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
var ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
_element;
_app;
_asyncTableHeader;
_asyncTableId;
var asyncTableUtil = function(element, app) {
var asyncTableHeader;
var asyncTableId;
_ths = [];
_pageLinks = [];
_pagesizeForm;
_scrollTable;
_cssIdPrefix = '';
var ths = [];
var pageLinks = [];
var pagesizeForm;
var scrollTable;
var cssIdPrefix = '';
var tableFilterInputs = {
_tableFilterInputs = {
search: [],
input: [],
change: [],
select: [],
};
function init() {
constructor(element, app) {
if (!element) {
throw new Error('Async Table utility cannot be setup without an element!');
}
if (element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
this._element = element;
this._app = app;
if (this._element.classList.contains(ASYNC_TABLE_INITIALIZED_CLASS)) {
return false;
}
// param asyncTableDbHeader
if (element.dataset.asyncTableDbHeader !== undefined) {
asyncTableHeader = element.dataset.asyncTableDbHeader;
if (this._element.dataset.asyncTableDbHeader !== undefined) {
this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
}
var rawTableId = element.querySelector('table').id;
cssIdPrefix = findCssIdPrefix(rawTableId);
asyncTableId = rawTableId.replace(cssIdPrefix, '');
const rawTableId = this._element.querySelector('table').id;
this._cssIdPrefix = findCssIdPrefix(rawTableId);
this._asyncTableId = rawTableId.replace(this._cssIdPrefix, '');
// find scrolltable wrapper
scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
if (!scrollTable) {
this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
if (!this._scrollTable) {
throw new Error('Async Table cannot be set up without a scrolltable element!');
}
setupSortableHeaders();
setupPagination();
setupPageSizeSelect();
setupTableFilter();
this._setupSortableHeaders();
this._setupPagination();
this._setupPageSizeSelect();
this._setupTableFilter();
processLocalStorage();
this._processLocalStorage();
// clear currentTableUrl from previous requests
setLocalStorageParameter('currentTableUrl', null);
// mark initialized
element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
return {
name: ASYNC_TABLE_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
}
function setupSortableHeaders() {
ths = Array.from(scrollTable.querySelectorAll('th.sortable')).map(function(th) {
return { element: th };
});
destroy() {
console.log('TBD: Destroy AsyncTable');
}
ths.forEach(function(th) {
th.clickHandler = function(event) {
setLocalStorageParameter('horizPos', (scrollTable || {}).scrollLeft);
linkClickHandler(event);
_setupSortableHeaders() {
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable'))
.map((th) => ({ element: th }));
this._ths.forEach((th) => {
th.clickHandler = (event) => {
setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft);
this._linkClickHandler(event);
};
th.element.addEventListener('click', th.clickHandler);
});
}
function setupPagination() {
var pagination = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagination');
_setupPagination() {
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
if (pagination) {
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
return { element: link };
});
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
.map((link) => ({ element: link }));
pageLinks.forEach(function(link) {
link.clickHandler = function(event) {
var tableBoundingRect = scrollTable.getBoundingClientRect();
this._pageLinks.forEach((link) => {
link.clickHandler = (event) => {
const tableBoundingRect = this._scrollTable.getBoundingClientRect();
if (tableBoundingRect.top < HEADER_HEIGHT) {
var scrollTo = {
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
left: scrollTable.offsetLeft || 0,
const scrollTo = {
top: (this._scrollTable.offsetTop || 0) - HEADER_HEIGHT,
left: this._scrollTable.offsetLeft || 0,
behavior: 'smooth',
};
setLocalStorageParameter('scrollTo', scrollTo);
}
linkClickHandler(event);
this._linkClickHandler(event);
};
link.element.addEventListener('click', link.clickHandler);
});
}
}
function setupPageSizeSelect() {
_setupPageSizeSelect() {
// pagesize form
pagesizeForm = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagesize-form');
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
pagesizeSelect.addEventListener('change', changePagesizeHandler);
if (this._pagesizeForm) {
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
pagesizeSelect.addEventListener('change', this._changePagesizeHandler);
}
}
function setupTableFilter() {
var tableFilterForm = element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
_setupTableFilter() {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) {
gatherTableFilterInputs(tableFilterForm);
addTableFilterEventListeners(tableFilterForm);
this._gatherTableFilterInputs(tableFilterForm);
this._addTableFilterEventListeners(tableFilterForm);
}
}
function gatherTableFilterInputs(tableFilterForm) {
_gatherTableFilterInputs(tableFilterForm) {
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach(function(input) {
tableFilterInputs.search.push(input);
this._tableFilterInputs.search.push(input);
});
Array.from(tableFilterForm.querySelectorAll('input[type="text"]')).forEach(function(input) {
tableFilterInputs.input.push(input);
this._tableFilterInputs.input.push(input);
});
Array.from(tableFilterForm.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) {
tableFilterInputs.change.push(input);
this._tableFilterInputs.change.push(input);
});
Array.from(tableFilterForm.querySelectorAll('select')).forEach(function(input) {
tableFilterInputs.select.push(input);
this._tableFilterInputs.select.push(input);
});
}
function addTableFilterEventListeners(tableFilterForm) {
tableFilterInputs.search.forEach(function(input) {
var debouncedInput = debounce(function() {
_addTableFilterEventListeners(tableFilterForm) {
this._tableFilterInputs.search.forEach(function(input) {
const debouncedInput = debounce(function() {
if (input.value.length === 0 || input.value.length > 2) {
updateFromTableFilter(tableFilterForm);
this.updateFromTableFilter(tableFilterForm);
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
});
tableFilterInputs.input.forEach(function(input) {
var debouncedInput = debounce(function() {
this._tableFilterInputs.input.forEach(function(input) {
const debouncedInput = debounce(function() {
if (input.value.length === 0 || input.value.length > 2) {
updateFromTableFilter(tableFilterForm);
this.updateFromTableFilter(tableFilterForm);
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
});
tableFilterInputs.change.forEach(function(input) {
this._tableFilterInputs.change.forEach(function(input) {
input.addEventListener('change', function() {
updateFromTableFilter(tableFilterForm);
this.updateFromTableFilter(tableFilterForm);
});
});
tableFilterInputs.select.forEach(function(input) {
this._tableFilterInputs.select.forEach(function(input) {
input.addEventListener('change', function() {
updateFromTableFilter(tableFilterForm);
this.updateFromTableFilter(tableFilterForm);
});
});
tableFilterForm.addEventListener('submit', function(event) {
event.preventDefault();
updateFromTableFilter(tableFilterForm);
this.updateFromTableFilter(tableFilterForm);
});
}
function updateFromTableFilter(tableFilterForm) {
var url = serializeTableFilterToURL();
var callback = null;
_updateFromTableFilter(tableFilterForm) {
const url = this.serializeTableFilterToURL();
let callback = null;
var focusedInput = tableFilterForm.querySelector(':focus, :active');
const focusedInput = tableFilterForm.querySelector(':focus, :active');
// focus previously focused input
if (focusedInput && focusedInput.selectionStart !== null) {
var selectionStart = focusedInput.selectionStart;
const selectionStart = focusedInput.selectionStart;
// remove the following part of the id to get rid of the random
// (yet somewhat structured) prefix we got from nudging.
var prefix = findCssIdPrefix(focusedInput.id);
var focusId = focusedInput.id.replace(prefix, '');
const prefix = findCssIdPrefix(focusedInput.id);
const focusId = focusedInput.id.replace(prefix, '');
callback = function(wrapper) {
var idPrefix = getLocalStorageParameter('cssIdPrefix');
var toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
const idPrefix = getLocalStorageParameter('cssIdPrefix');
const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
if (toBeFocused) {
toBeFocused.focus();
toBeFocused.selectionStart = selectionStart;
}
};
}
updateTableFrom(url, callback);
this._updateTableFrom(url, callback);
}
function serializeTableFilterToURL() {
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
serializeTableFilterToURL() {
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
var formIdElement = element.querySelector(ASYNC_TABLE_FILTER_FORM_ID_SELECTOR);
const formIdElement = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_ID_SELECTOR);
if (!formIdElement) {
// cannot serialize the filter form without an identifier
return;
@ -237,25 +229,25 @@ var asyncTableUtil = function(element, app) {
url.searchParams.set('form-identifier', formIdElement.value);
url.searchParams.set('_hasdata', 'true');
url.searchParams.set(asyncTableId + '-page', '0');
url.searchParams.set(this._asyncTableId + '-page', '0');
tableFilterInputs.search.forEach(function(input) {
this._tableFilterInputs.search.forEach(function(input) {
url.searchParams.set(input.name, input.value);
});
tableFilterInputs.input.forEach(function(input) {
this._tableFilterInputs.input.forEach(function(input) {
url.searchParams.set(input.name, input.value);
});
tableFilterInputs.change.forEach(function(input) {
this._tableFilterInputs.change.forEach(function(input) {
if (input.checked) {
url.searchParams.set(input.name, input.value);
}
});
tableFilterInputs.select.forEach(function(select) {
var options = Array.from(select.querySelectorAll('option'));
var selected = options.find(function(option) { return option.selected; });
this._tableFilterInputs.select.forEach(function(select) {
const options = Array.from(select.querySelectorAll('option'));
const selected = options.find((option) => option.selected);
if (selected) {
url.searchParams.set(select.name, selected.value);
}
@ -264,119 +256,114 @@ var asyncTableUtil = function(element, app) {
return url;
}
function processLocalStorage() {
var scrollTo = getLocalStorageParameter('scrollTo');
if (scrollTo && scrollTable) {
_processLocalStorage() {
const scrollTo = getLocalStorageParameter('scrollTo');
if (scrollTo && this._scrollTable) {
window.scrollTo(scrollTo);
}
setLocalStorageParameter('scrollTo', null);
var horizPos = getLocalStorageParameter('horizPos');
if (horizPos && scrollTable) {
scrollTable.scrollLeft = horizPos;
const horizPos = getLocalStorageParameter('horizPos');
if (horizPos && this._scrollTable) {
this._scrollTable.scrollLeft = horizPos;
}
setLocalStorageParameter('horizPos', null);
}
function removeListeners() {
ths.forEach(function(th) {
_removeListeners() {
this._ths.forEach(function(th) {
th.element.removeEventListener('click', th.clickHandler);
});
pageLinks.forEach(function(link) {
this._pageLinks.forEach(function(link) {
link.element.removeEventListener('click', link.clickHandler);
});
if (pagesizeForm) {
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
if (this._pagesizeForm) {
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
pagesizeSelect.removeEventListener('change', this._changePagesizeHandler);
}
}
function linkClickHandler(event) {
_linkClickHandler = (event) => {
event.preventDefault();
var url = getClickDestination(event.target);
let url = this._getClickDestination(event.target);
if (!url.match(/^http/)) {
url = window.location.origin + window.location.pathname + url;
}
updateTableFrom(url);
this._updateTableFrom(url);
}
function getClickDestination(el) {
_getClickDestination(el) {
if (!el.matches('a') && !el.querySelector('a')) {
return '';
}
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
}
function changePagesizeHandler(event) {
var paginationParamKey = asyncTableId + '-pagination';
var pagesizeParamKey = asyncTableId + '-pagesize';
var pageParamKey = asyncTableId + '-page';
_changePagesizeHandler = (event) => {
const paginationParamKey = this._asyncTableId + '-pagination';
const pagesizeParamKey = this._asyncTableId + '-pagesize';
const pageParamKey = this._asyncTableId + '-page';
var paginationParamEl = pagesizeForm.querySelector('[name="' + paginationParamKey + '"]');
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
const paginationParamEl = this._pagesizeForm.querySelector('[name="' + paginationParamKey + '"]');
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
url.searchParams.set(pagesizeParamKey, event.target.value);
url.searchParams.set(pageParamKey, 0);
if (paginationParamEl) {
var encodedValue = encodeURIComponent(paginationParamEl.value);
const encodedValue = encodeURIComponent(paginationParamEl.value);
url.searchParams.set(paginationParamKey, encodedValue);
}
updateTableFrom(url.href);
this._updateTableFrom(url.href);
}
// fetches new sorted element from url with params and replaces contents of current element
function updateTableFrom(url, callback) {
element.classList.add(ASYNC_TABLE_LOADING_CLASS);
_updateTableFrom(url, callback) {
this._element.classList.add(ASYNC_TABLE_LOADING_CLASS);
var headers = {
const headers = {
'Accept': HttpClient.ACCEPT.TEXT_HTML,
[asyncTableHeader]: asyncTableId,
[this._asyncTableHeader]: this._asyncTableId,
};
app.httpClient.get({
this._app.httpClient.get({
url: url,
headers: headers,
}).then(function(response) {
return app.htmlHelpers.parseResponse(response);
}).then(function(response) {
}).then(
(response) => this._app.htmlHelpers.parseResponse(response)
).then((response) => {
setLocalStorageParameter('currentTableUrl', url.href);
// reset table
removeListeners();
element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
this._removeListeners();
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
// update table with new
updateWrapperContents(response);
this._updateWrapperContents(response);
app.utilRegistry.setupAll(element);
this._app.utilRegistry.setupAll(this._element);
if (callback && typeof callback === 'function') {
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
callback(element);
callback(this._element);
setLocalStorageParameter('cssIdPrefix', '');
}
}).catch(function(err) {
console.error(err);
}).finally(function() {
element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
});
}).catch((err) => console.error(err)
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
}
function updateWrapperContents(response) {
var newPage = document.createElement('div');
_updateWrapperContents(response) {
const newPage = document.createElement('div');
newPage.appendChild(response.element);
var newWrapperContents = newPage.querySelector('#' + response.idPrefix + element.id);
element.innerHTML = newWrapperContents.innerHTML;
const newWrapperContents = newPage.querySelector('#' + response.idPrefix + this._element.id);
this._element.innerHTML = newWrapperContents.innerHTML;
}
return init();
};
}
// returns any random nudged prefix found in the given id
function findCssIdPrefix(id) {
var matcher = /r\d*?__/;
var maybePrefix = id.match(matcher);
const matcher = /r\d*?__/;
const maybePrefix = id.match(matcher);
if (maybePrefix && maybePrefix[0]) {
return maybePrefix[0];
}
@ -384,7 +371,7 @@ function findCssIdPrefix(id) {
}
function setLocalStorageParameter(key, value) {
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
if (value !== null) {
currentLSState[key] = value;
} else {
@ -392,30 +379,23 @@ function setLocalStorageParameter(key, value) {
}
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
}
function getLocalStorageParameter(key) {
var currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
return currentLSState[key];
}
// debounce function, taken from Underscore.js
function debounce(func, wait, immediate) {
var timeout;
let timeout;
return function() {
var context = this, args = arguments;
var later = function() {
const context = this, args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
export default {
name: ASYNC_TABLE_UTIL_NAME,
selector: ASYNC_TABLE_UTIL_SELECTOR,
setup: asyncTableUtil,
};

View File

@ -0,0 +1,7 @@
# Async Table Utility
Makes table filters, sorting and pagination behave asynchronously via AJAX calls.
## Attribute: `uw-async-table`
## Example usage:
(any regular table)

View File

@ -1,58 +1,53 @@
/**
*
* Check All Checkbox Utility
* adds a Check All Checkbox above columns with only checkboxes
*
* Attribute: [none]
* (will be set up automatically on tables)
*
* Example usage:
* (table with one column thats only checkboxes)
*/
import { Utility } from "../../core/utility";
var CHECK_ALL_UTIL_NAME = 'checkAll';
var CHECK_ALL_UTIL_SELECTOR = 'table';
const CHECKBOX_SELECTOR = '[type="checkbox"]';
var CHECKBOX_SELECTOR = '[type="checkbox"]';
const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
var CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
@Utility({
selector: 'table',
})
export class CheckAll {
var checkAllUtil = function(element, app) {
var columns = [];
var checkboxColumn = [];
var checkAllCheckbox = null;
_element;
_app;
function init() {
_columns = [];
_checkboxColumn = [];
_checkAllCheckbox = null;
constructor(element, app) {
if (!element) {
throw new Error('Check All utility cannot be setup without an element!');
}
if (element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
this._element = element;
this._app = app;
if (this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
return false;
}
gatherColumns();
setupCheckAllCheckbox();
this._gatherColumns();
this._setupCheckAllCheckbox();
// mark initialized
element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
return {
name: CHECK_ALL_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
}
function getCheckboxId() {
destroy() {
console.log('TBD: Destroy CheckAll');
}
_getCheckboxId() {
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
}
function gatherColumns() {
var rows = Array.from(element.querySelectorAll('tr'));
var cols = [];
_gatherColumns() {
const rows = Array.from(this._element.querySelectorAll('tr'));
const cols = [];
rows.forEach(function(tr) {
var cells = Array.from(tr.querySelectorAll('td'));
const cells = Array.from(tr.querySelectorAll('td'));
cells.forEach(function(cell, cellIndex) {
if (!cols[cellIndex]) {
cols[cellIndex] = [];
@ -60,21 +55,21 @@ var checkAllUtil = function(element, app) {
cols[cellIndex].push(cell);
});
});
columns = cols;
this._columns = cols;
}
function findCheckboxColumn(columns) {
var checkboxColumnId = null;
_findCheckboxColumn(columns) {
let checkboxColumnId = null;
columns.forEach(function(col, i) {
if (isCheckboxColumn(col)) {
if (this.isCheckboxColumn(col)) {
checkboxColumnId = i;
}
});
return checkboxColumnId;
}
function isCheckboxColumn(col) {
var onlyCheckboxes = true;
_isCheckboxColumn(col) {
let onlyCheckboxes = true;
col.forEach(function(cell) {
if (onlyCheckboxes && !cell.querySelector(CHECKBOX_SELECTOR)) {
onlyCheckboxes = false;
@ -83,59 +78,50 @@ var checkAllUtil = function(element, app) {
return onlyCheckboxes;
}
function setupCheckAllCheckbox() {
var checkboxColumnId = findCheckboxColumn(columns);
_setupCheckAllCheckbox() {
const checkboxColumnId = this._findCheckboxColumn(this._columns);
if (checkboxColumnId === null) {
return;
}
checkboxColumn = columns[checkboxColumnId];
var firstRow = element.querySelector('tr');
var th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
checkAllCheckbox = document.createElement('input');
checkAllCheckbox.setAttribute('type', 'checkbox');
checkAllCheckbox.setAttribute('id', getCheckboxId());
th.insertBefore(checkAllCheckbox, th.firstChild);
this._checkboxColumn = this._columns[checkboxColumnId];
const firstRow = this._element.querySelector('tr');
const th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
this._checkAllCheckbox = document.createElement('input');
this._checkAllCheckbox.setAttribute('type', 'checkbox');
this._checkAllCheckbox.setAttribute('id', this._getCheckboxId());
th.insertBefore(this._checkAllCheckbox, th.firstChild);
// manually set up new checkbox
app.utilRegistry.setup(app.utilRegistry.find('checkbox'), th);
this._app.utilRegistry.setup(this._app.utilRegistry.find('checkbox'), th);
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);
setupCheckboxListeners();
this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput);
this._setupCheckboxListeners();
}
function onCheckAllCheckboxInput() {
toggleAll(checkAllCheckbox.checked);
_onCheckAllCheckboxInput() {
this._toggleAll(this._checkAllCheckbox.checked);
}
function setupCheckboxListeners() {
checkboxColumn
.map(function(cell) {
_setupCheckboxListeners() {
this._checkboxColumn.map(function(cell) {
return cell.querySelector(CHECKBOX_SELECTOR);
})
.forEach(function(checkbox) {
checkbox.addEventListener('input', updateCheckAllCheckboxState);
checkbox.addEventListener('input', this.updateCheckAllCheckboxState);
});
}
function updateCheckAllCheckboxState() {
var allChecked = checkboxColumn.every(function(cell) {
_updateCheckAllCheckboxState() {
const allChecked = this._checkboxColumn.every(function(cell) {
return cell.querySelector(CHECKBOX_SELECTOR).checked;
});
checkAllCheckbox.checked = allChecked;
this._checkAllCheckbox.checked = allChecked;
}
function toggleAll(checked) {
checkboxColumn.forEach(function(cell) {
_toggleAll(checked) {
this._checkboxColumn.forEach(function(cell) {
cell.querySelector(CHECKBOX_SELECTOR).checked = checked;
});
}
return init();
};
export default {
name: CHECK_ALL_UTIL_NAME,
selector: CHECK_ALL_UTIL_SELECTOR,
setup: checkAllUtil,
};
}

View File

@ -0,0 +1,9 @@
# Check All Checkbox Utility
Adds a Check All Checkbox above columns with only checkboxes
## Attribute: (none)
(will be set up automatically on tables)
## Example usage:
(table with exactly one column thats only checkboxes)

View File

@ -0,0 +1,29 @@
import { Utility } from "../../core/utility";
export const AUTO_SUBMIT_BUTTON_UTIL_SELECTOR = '[uw-auto-submit-button]';
const AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS = 'auto-submit-button--initialized';
const AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
@Utility({
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
})
export class AutoSubmitButton {
constructor(element) {
if (!element) {
throw new Error('Auto Submit Button utility needs to be passed an element!');
}
if (element.classList.contains(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
return false;
}
// hide and mark initialized
element.classList.add(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
}
destroy() {
// TODO
}
}

View File

@ -0,0 +1,11 @@
# Auto Submit Button Utility
Hides submit buttons in forms that are submitted programmatically.
We hide the button using JavaScript so no-js users will still be able to submit the form.
## Attribute: `uw-auto-submit-button`
## Example usage:
```html
<button type="submit" uw-auto-submit-button>Submit
```

View File

@ -0,0 +1,47 @@
import * as debounce from 'lodash.debounce';
import { Utility } from '../../core/utility';
export const AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]';
const AUTO_SUBMIT_INPUT_INITIALIZED_CLASS = 'auto-submit-input--initialized';
@Utility({
selector: AUTO_SUBMIT_INPUT_UTIL_SELECTOR,
})
export class AutoSubmitInput {
_element;
_form;
_debouncedHandler;
constructor(element) {
if (!element) {
throw new Error('Auto Submit Input utility needs to be passed an element!');
}
this._element = element;
if (this._element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) {
return false;
}
this._form = this._element.form;
if (!this._form) {
throw new Error('Could not determine associated form for auto submit input');
}
this._debouncedHandler = debounce(this.autoSubmit, 500);
this._element.addEventListener('input', this._debouncedHandler);
this._element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
}
destroy() {
this._element.removeEventListener('input', this._debouncedHandler);
}
autoSubmit() {
this._form.submit();
}
}

View File

@ -0,0 +1,9 @@
# Auto Submit Input Utility
Programmatically submits forms when a certain input changes value
## Attribute: `uw-auto-submit-input`
## Example usage:
```html
<input type="text" uw-auto-submit-input />
```

View File

@ -0,0 +1,62 @@
import flatpickr from "flatpickr";
import { Utility } from "../../core/utility";
const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
const DATEPICKER_CONFIG = {
"datetime-local": {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
dateFormat: "Y-m-dTH:i",
time_24hr: true,
},
"date": {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true,
},
"time": {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true,
},
};
@Utility({
selector: DATEPICKER_UTIL_SELECTOR,
})
export class Datepicker {
flatpickrInstance;
constructor(element) {
if (!element) {
throw new Error('Datepicker utility needs to be passed an element!');
}
if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) {
return false;
}
const flatpickrConfig = DATEPICKER_CONFIG[element.getAttribute("type")];
if (!flatpickrConfig) {
throw new Error('Datepicker utility called on unsupported element!');
}
this.flatpickrInstance = flatpickr(element, flatpickrConfig);
// mark initialized
element.classList.add(DATEPICKER_INITIALIZED_CLASS);
}
destroy() {
this.flatpickrInstance.destroy();
}
}

View File

@ -0,0 +1,8 @@
# Datepicker Utility
Provides UI for entering dates and times
## Attribute: (none)
(automatically setup on all relevant input tags)
## Example usage:
(any form that uses inputs of type date, time, or datetime-local)

View File

@ -0,0 +1,46 @@
import { Utility } from "../../core/utility";
const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
const FORM_GROUP_SELECTOR = '.form-group';
const FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
@Utility({
selector: 'form',
})
export class FormErrorRemover {
constructor(element) {
if (!element) {
throw new Error('Form Error Remover utility needs to be passed an element!');
}
if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS)) {
return false;
}
// find form groups
const formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR));
formGroups.forEach(function(formGroup) {
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
return;
}
const inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
if (!inputElements) {
return false;
}
inputElements.forEach(function(inputElement) {
inputElement.addEventListener('input', function() {
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
});
});
});
// mark initialized
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
}
}

View File

@ -0,0 +1,8 @@
# Form Error Remover Utility
Removes errors from inputs when they are focused
## Attribute: (none)
(automatically setup on all form tags)
## Example usage:
(any regular form that can show input errors)

View File

@ -1,600 +1,17 @@
import flatpickr from 'flatpickr';
import './form.scss';
/**
*
* Reactive Submit Button Utility
* disables a forms LAST sumit button as long as the required inputs are invalid
* (only checks if the value of the inputs are not empty)
*
* Attribute: [none]
* (automatically setup on all form tags)
*
* Params:
* data-formnorequired: string
* If present the submit button will never get disabled
*
* Example usage:
* <form uw-reactive-submit-button>
* <input type="text" required>
* <button type="submit">
* </form>
*/
var REACTIVE_SUBMIT_BUTTON_UTIL_NAME = 'reactiveSubmitButton';
// var REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR = 'form';
var REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
/* eslint-disable-next-line */
var reactiveSubmitButtonUtil = function(element) {
var requiredInputs;
var submitButton;
function init() {
if (!element) {
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
}
if (element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
return false;
}
// abort if form has param data-formnorequired
if (element.dataset.formnorequired !== undefined) {
throw new Error('Form has formnorequired data attribute. Will skip setup of reactive submit button.');
}
requiredInputs = Array.from(element.querySelectorAll('[required]'));
if (!requiredInputs) {
// abort if form has no required inputs
throw new Error('Submit button has formnorequired data attribute. Will skip setup of reactive submit button.');
}
var submitButtons = Array.from(element.querySelectorAll('[type="submit"]'));
if (!submitButtons || !submitButtons.length) {
throw new Error('Reactive Submit Button utility couldn\'t find any submit buttons!');
}
submitButton = submitButtons.reverse()[0];
// abort if form has param data-formnorequired
if (submitButton.dataset.formnorequired !== undefined) {
return false;
}
setupInputs();
updateButtonState();
element.classList.add(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
return {
name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
element: element,
destroy: function() {},
};
}
function setupInputs() {
requiredInputs.forEach(function(el) {
var checkbox = el.getAttribute('type') === 'checkbox';
var eventType = checkbox ? 'change' : 'input';
el.addEventListener(eventType, function() {
updateButtonState();
});
});
}
function updateButtonState() {
if (inputsValid()) {
submitButton.removeAttribute('disabled');
} else {
submitButton.setAttribute('disabled', 'true');
}
}
function inputsValid() {
var done = true;
requiredInputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
return init();
};
// skipping reactiveButtonUtil (for now)
// the button did not properly re-enable after filling out a form for some safari users.
// if maybe in the future there is going to be a proper way of (asynchronously) and
// meaningfully validating forms this can be re-activated by commenting in the next few lines
// eport const reactiveSubmitButton = {
// name: REACTIVE_SUBMIT_BUTTON_UTIL_NAME,
// selector: REACTIVE_SUBMIT_BUTTON_UTIL_SELECTOR,
// setup: reactiveSubmitButtonUtil,
// };
/**
*
* Interactive Fieldset Utility
* shows/hides inputs based on value of particular input
*
* Attribute: uw-interactive-fieldset
*
* Params:
* data-conditional-input: string
* Selector for the input that this fieldset watches for changes
* data-conditional-value: string
* The value the conditional input needs to be set to for this fieldset to be shown
* Can be omitted if conditionalInput is a checkbox
*
* Example usage:
* ## example with text input
* <input id="input-0" type="text">
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="yes">...</fieldset>
* <fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="no">...</fieldset>
* ## example with <select>
* <select id="select-0">
* <option value="0">Zero
* <option value="1">One
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="0">...</fieldset>
* <fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="1">...</fieldset>
* ## example with checkbox
* <input id="checkbox-0" type="checkbox">
* <input id="checkbox-1" type="checkbox">
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-0">...</fieldset>
* <fieldset uw-interactive-fieldset data-conditional-input="#checkbox-1">...</fieldset>
*/
var INTERACTIVE_FIELDSET_UTIL_NAME = 'interactiveFieldset';
var INTERACTIVE_FIELDSET_UTIL_SELECTOR = '[uw-interactive-fieldset]';
var INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target';
var INTERACTIVE_FIELDSET_INITIALIZED_CLASS = 'interactive-fieldset--initialized';
var INTERACTIVE_FIELDSET_CHILD_SELECTOR = 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])';
var interactiveFieldsetUtil = function(element) {
var conditionalInput;
var conditionalValue;
var target;
var childInputs;
function init() {
if (!element) {
throw new Error('Interactive Fieldset utility cannot be setup without an element!');
}
if (element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
return false;
}
// param conditionalInput
if (!element.dataset.conditionalInput) {
throw new Error('Interactive Fieldset needs a selector for a conditional input!');
}
conditionalInput = document.querySelector('#' + element.dataset.conditionalInput);
if (!conditionalInput) {
// abort if form has no required inputs
throw new Error('Couldn\'t find the conditional input. Aborting setup for interactive fieldset.');
}
// param conditionalValue
if (!element.dataset.conditionalValue && !isCheckbox()) {
throw new Error('Interactive Fieldset needs a conditional value!');
}
conditionalValue = element.dataset.conditionalValue;
target = element.closest(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR);
if (!target || element.matches(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR)) {
target = element;
}
childInputs = Array.from(element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR));
// add event listener
var observer = new MutationObserver(() => updateVisibility());
observer.observe(conditionalInput, { attributes: true, attributeFilter: ['disabled'] });
conditionalInput.addEventListener('input', updateVisibility);
// initial visibility update
updateVisibility();
// mark as initialized
element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
return {
name: INTERACTIVE_FIELDSET_UTIL_NAME,
element: element,
destroy: function() {},
};
}
function updateVisibility() {
var active = matchesConditionalValue() && !conditionalInput.disabled;
target.classList.toggle('hidden', !active);
childInputs.forEach(function(el) {
el.disabled = !active;
if (el._flatpickr) {
el._flatpickr.altInput.disabled = !active;
}
});
}
function matchesConditionalValue() {
if (isCheckbox()) {
return conditionalInput.checked === true;
}
return conditionalInput.value === conditionalValue;
}
function isCheckbox() {
return conditionalInput.getAttribute('type') === 'checkbox';
}
return init();
};
export const interactiveFieldset = {
name: INTERACTIVE_FIELDSET_UTIL_NAME,
selector: INTERACTIVE_FIELDSET_UTIL_SELECTOR,
setup: interactiveFieldsetUtil,
};
/**
*
* Navigate Away Prompt Utility
* This utility asks the user if (s)he really wants to navigate away
* from a page containing a form if (s)he already touched an input.
* Form-Submits will not trigger the prompt.
* Utility will ignore forms that contain auto submit elements (buttons, inputs).
*
* Attribute: [none]
* (automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil)
*
* Example usage:
* (any page with a form)
*/
var NAVIGATE_AWAY_PROMPT_UTIL_NAME = 'navigateAwayPrompt';
var NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR = 'form';
var NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized';
var navigateAwayPromptUtil = function(element) {
var touched = false;
var unloadDueToSubmit = false;
function init() {
if (!element) {
throw new Error('Navigate Away Prompt utility needs to be passed an element!');
}
if (element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) {
return false;
}
// ignore forms that get submitted automatically
if (element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) {
return false;
}
window.addEventListener('beforeunload', beforeUnloadHandler);
element.addEventListener('submit', function() {
unloadDueToSubmit = true;
});
element.addEventListener('change', function() {
touched = true;
unloadDueToSubmit = false;
});
// mark initialized
element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS);
return {
name: NAVIGATE_AWAY_PROMPT_UTIL_NAME,
element: element,
destroy: function() {
window.removeEventListener('beforeunload', beforeUnloadHandler);
},
};
}
function beforeUnloadHandler(event) {
// allow the event to happen if the form was not touched by the
// user or the unload event was initiated by a form submit
if (!touched || unloadDueToSubmit) {
return false;
}
// cancel the unload event. This is the standard to force the prompt to appear.
event.preventDefault();
// for all non standard compliant browsers we return a truthy value to activate the prompt.
return true;
}
return init();
};
export const navigateAwayPrompt = {
name: NAVIGATE_AWAY_PROMPT_UTIL_NAME,
selector: NAVIGATE_AWAY_PROMPT_UTIL_SELECTOR,
setup: navigateAwayPromptUtil,
};
/**
*
* Auto Submit Button Utility
* Hides submit buttons in forms that are submitted programmatically
* We hide the button using JavaScript so no-js users will still be able to submit the form
*
* Attribute: uw-auto-submit-button
*
* Example usage:
* <button type="submit" uw-auto-submit-button>Submit
*/
var AUTO_SUBMIT_BUTTON_UTIL_NAME = 'autoSubmitButton';
var AUTO_SUBMIT_BUTTON_UTIL_SELECTOR = '[uw-auto-submit-button]';
var AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS = 'auto-submit-button--initialized';
var AUTO_SUBMIT_BUTTON_HIDDEN_CLASS = 'hidden';
var autoSubmitButtonUtil = function(element) {
if (!element) {
throw new Error('Auto Submit Button utility needs to be passed an element!');
}
if (element.classList.contains(AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
return false;
}
// hide and mark initialized
element.classList.add(AUTO_SUBMIT_BUTTON_HIDDEN_CLASS, AUTO_SUBMIT_BUTTON_INITIALIZED_CLASS);
return {
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
element: element,
destroy: function() {},
};
};
export const autoSubmitButton = {
name: AUTO_SUBMIT_BUTTON_UTIL_NAME,
selector: AUTO_SUBMIT_BUTTON_UTIL_SELECTOR,
setup: autoSubmitButtonUtil,
};
/**
*
* Auto Submit Input Utility
* Programmatically submits forms when a certain input changes value
*
* Attribute: uw-auto-submit-input
*
* Example usage:
* <input type="text" uw-auto-submit-input />
*/
var AUTO_SUBMIT_INPUT_UTIL_NAME = 'autoSubmitInput';
var AUTO_SUBMIT_INPUT_UTIL_SELECTOR = '[uw-auto-submit-input]';
var AUTO_SUBMIT_INPUT_INITIALIZED_CLASS = 'auto-submit-input--initialized';
var autoSubmitInputUtil = function(element) {
var form;
var debouncedHandler;
function autoSubmit() {
form.submit();
}
function init() {
if (!element) {
throw new Error('Auto Submit Input utility needs to be passed an element!');
}
if (element.classList.contains(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS)) {
return false;
}
form = element.form;
if (!form) {
throw new Error('Could not determine associated form for auto submit input');
}
debouncedHandler = debounce(autoSubmit, 500);
element.addEventListener('input', debouncedHandler);
element.classList.add(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
return {
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
element: element,
destroy: function() {
element.removeEventListener('input', debouncedHandler);
},
};
}
return init();
};
export const autoSubmitInput = {
name: AUTO_SUBMIT_INPUT_UTIL_NAME,
selector: AUTO_SUBMIT_INPUT_UTIL_SELECTOR,
setup: autoSubmitInputUtil,
};
/**
*
* Form Error Remover Utility
* Removes errors from inputs when they are focused
*
* Attribute: [none]
* (automatically setup on all form tags)
*
* Example usage:
* (any regular form that can show input errors)
*/
var FORM_ERROR_REMOVER_UTIL_NAME = 'formErrorRemover';
var FORM_ERROR_REMOVER_UTIL_SELECTOR = 'form';
var FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
var FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
var FORM_GROUP_SELECTOR = '.form-group';
var FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error';
var formErrorRemoverUtil = function(element) {
var formGroups;
function init() {
if (!element) {
throw new Error('Form Error Remover utility needs to be passed an element!');
}
if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS)) {
return false;
}
// find form groups
formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR));
formGroups.forEach(function(formGroup) {
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
return;
}
var inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
if (!inputElements) {
return false;
}
inputElements.forEach(function(inputElement) {
inputElement.addEventListener('input', function() {
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS);
});
});
});
// mark initialized
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
return {
name: FORM_ERROR_REMOVER_UTIL_NAME,
element: element,
destroy: function() {},
};
}
return init();
};
export const formErrorRemover = {
name: FORM_ERROR_REMOVER_UTIL_NAME,
selector: FORM_ERROR_REMOVER_UTIL_SELECTOR,
setup: formErrorRemoverUtil,
};
/**
*
* Datepicker Utility
* Provides UI for entering dates and times
*
* Attribute: [none]
* (automatically setup on all relevant input tags)
*
* Example usage:
* (any form that uses inputs of type date, time, or datetime-local)
*/
var DATEPICKER_UTIL_NAME = 'datepicker';
var DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
var DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
var DATEPICKER_CONFIG = {
"datetime-local": {
enableTime: true,
altInput: true,
altFormat: "j. F Y, H:i", // maybe interpolate these formats for locale
dateFormat: "Y-m-dTH:i",
time_24hr: true,
},
"date": {
altFormat: "j. F Y",
dateFormat: "Y-m-d",
altInput: true,
},
"time": {
enableTime: true,
noCalendar: true,
altFormat: "H:i",
dateFormat: "H:i",
altInput: true,
time_24hr: true,
},
};
var datepickerUtil = function(element) {
var flatpickrInstance;
function init() {
if (!element) {
throw new Error('Datepicker utility needs to be passed an element!');
}
if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) {
return false;
}
var flatpickrConfig = DATEPICKER_CONFIG[element.getAttribute("type")];
if (!flatpickrConfig) {
throw new Error('Datepicker utility called on unsupported element!');
}
flatpickrInstance = flatpickr(element, flatpickrConfig);
// mark initialized
element.classList.add(DATEPICKER_INITIALIZED_CLASS);
return {
name: DATEPICKER_UTIL_NAME,
element: element,
destroy: function() { flatpickrInstance.destroy(); },
};
}
return init();
};
export const datepicker = {
name: DATEPICKER_UTIL_NAME,
selector: DATEPICKER_UTIL_SELECTOR,
setup: datepickerUtil,
};
// debounce function, taken from Underscore.js
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
import { AutoSubmitButton } from './auto-submit-button';
import { AutoSubmitInput } from './auto-submit-input';
import { Datepicker } from './datepicker';
import { FormErrorRemover } from './form-error-remover';
import { InteractiveFieldset } from './interactive-fieldset';
import { NavigateAwayPrompt } from './navigate-away-prompt';
export const FormUtils = [
AutoSubmitButton,
AutoSubmitInput,
Datepicker,
FormErrorRemover,
InteractiveFieldset,
NavigateAwayPrompt,
// ReactiveSubmitButton // not used currently
];

View File

@ -0,0 +1,97 @@
import { Utility } from "../../core/utility";
const INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR = '.interactive-fieldset__target';
const INTERACTIVE_FIELDSET_INITIALIZED_CLASS = 'interactive-fieldset--initialized';
const INTERACTIVE_FIELDSET_CHILD_SELECTOR = 'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])';
@Utility({
selector: '[uw-interactive-fieldset]',
})
export class InteractiveFieldset {
_element;
conditionalInput;
conditionalValue;
target;
childInputs;
constructor(element) {
if (!element) {
throw new Error('Interactive Fieldset utility cannot be setup without an element!');
}
this._element = element;
if (this._element.classList.contains(INTERACTIVE_FIELDSET_INITIALIZED_CLASS)) {
return false;
}
// param conditionalInput
if (!this._element.dataset.conditionalInput) {
throw new Error('Interactive Fieldset needs a selector for a conditional input!');
}
this.conditionalInput = document.querySelector('#' + this._element.dataset.conditionalInput);
if (!this.conditionalInput) {
// abort if form has no required inputs
throw new Error('Couldn\'t find the conditional input. Aborting setup for interactive fieldset.');
}
// param conditionalValue
if (!this._element.dataset.conditionalValue && !this._isCheckbox()) {
throw new Error('Interactive Fieldset needs a conditional value!');
}
this.conditionalValue = this._element.dataset.conditionalValue;
this.target = this._element.closest(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR);
if (!this.target || this._element.matches(INTERACTIVE_FIELDSET_UTIL_TARGET_SELECTOR)) {
this.target = this._element;
}
this.childInputs = Array.from(this._element.querySelectorAll(INTERACTIVE_FIELDSET_CHILD_SELECTOR));
// add event listener
const observer = new MutationObserver(() => this._updateVisibility());
observer.observe(this.conditionalInput, { attributes: true, attributeFilter: ['disabled'] });
this.conditionalInput.addEventListener('input', () => this._updateVisibility());
// initial visibility update
this._updateVisibility();
// mark as initialized
this._element.classList.add(INTERACTIVE_FIELDSET_INITIALIZED_CLASS);
}
destroy() {
// TODO
}
_updateVisibility() {
const active = this._matchesConditionalValue() && !this.conditionalInput.disabled;
this.target.classList.toggle('hidden', !active);
this.childInputs.forEach(function(el) {
el.disabled = !active;
// disable input for flatpickrs added input as well if exists
if (el._flatpickr) {
el._flatpickr.altInput.disabled = !active;
}
});
}
_matchesConditionalValue() {
if (this._isCheckbox()) {
return this.conditionalInput.checked === true;
}
return this.conditionalInput.value === this.conditionalValue;
}
_isCheckbox() {
return this.conditionalInput.getAttribute('type') === 'checkbox';
}
}

View File

@ -0,0 +1,35 @@
# Interactive Fieldset Utility
Shows/hides inputs based on value of particular input
## Attribute: `uw-interactive-fieldset`
## Params:
- `data-conditional-input: string`\
Selector for the input that this fieldset watches for changes
- `data-conditional-value: string`\
The value the conditional input needs to be set to for this fieldset to be shown. Can be omitted if conditionalInput is a checkbox
## Example usage:
### example with text input
```html
<input id="input-0" type="text">
<fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="yes">...</fieldset>
<fieldset uw-interactive-fieldset data-conditional-input="#input-0" data-conditional-value="no">...</fieldset>
```
### example with `<select>`
```html
<select id="select-0">
<option value="0">Zero
<option value="1">One
<fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="0">...</fieldset>
<fieldset uw-interactive-fieldset data-conditional-input="#select-0" data-conditional-value="1">...</fieldset>
```
### example with checkbox
```html
<input id="checkbox-0" type="checkbox">
<input id="checkbox-1" type="checkbox">
<fieldset uw-interactive-fieldset data-conditional-input="#checkbox-0">...</fieldset>
<fieldset uw-interactive-fieldset data-conditional-input="#checkbox-1">...</fieldset>
```

View File

@ -0,0 +1,64 @@
import { Utility } from "../../core/utility";
import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from "./auto-submit-button";
import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from "./auto-submit-input";
const NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS = 'navigate-away-prompt--initialized';
@Utility({
selector: 'form',
})
export class NavigateAwayPrompt {
_element;
_touched = false;
_unloadDueToSubmit = false;
constructor(element) {
if (!element) {
throw new Error('Navigate Away Prompt utility needs to be passed an element!');
}
this._element = element;
if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) {
return false;
}
// ignore forms that get submitted automatically
if (this._element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || this._element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) {
return false;
}
window.addEventListener('beforeunload', this._beforeUnloadHandler);
this._element.addEventListener('submit', function() {
this.unloadDueToSubmit = true;
});
this._element.addEventListener('change', function() {
this.touched = true;
this.unloadDueToSubmit = false;
});
// mark initialized
this._element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS);
}
destroy() {
window.removeEventListener('beforeunload', this._beforeUnloadHandler);
}
_beforeUnloadHandler(event) {
// allow the event to happen if the form was not touched by the
// user or the unload event was initiated by a form submit
if (!this._touched || this._unloadDueToSubmit) {
return false;
}
// cancel the unload event. This is the standard to force the prompt to appear.
event.preventDefault();
// for all non standard compliant browsers we return a truthy value to activate the prompt.
return true;
}
}

View File

@ -0,0 +1,11 @@
# Navigate Away Prompt Utility
This utility asks the user if (s)he really wants to navigate away from a page containing a form if (s)he already touched an input.
- Form-Submits will not trigger the prompt.
- Utility will ignore forms that contain auto submit elements (buttons, inputs).
## Attribute: (none)
(automatically setup on all form tags that dont automatically submit, see AutoSubmitButtonUtil)
## Example usage:
(any page with a form)

View File

@ -0,0 +1,85 @@
import { Utility } from "../../core/utility";
var REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS = 'reactive-submit-button--initialized';
@Utility({
selector: 'form',
})
export class ReactiveSubmitButton {
_element;
_requiredInputs;
_submitButton;
constructor(element) {
if (!element) {
throw new Error('Reactive Submit Button utility cannot be setup without an element!');
}
this._element = element;
if (this._element.classList.contains(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS)) {
return false;
}
// abort if form has param data-formnorequired
if (this._element.dataset.formnorequired !== undefined) {
throw new Error('Form has formnorequired data attribute. Will skip setup of reactive submit button.');
}
this._requiredInputs = Array.from(this._element.querySelectorAll('[required]'));
if (!this._requiredInputs) {
// abort if form has no required inputs
throw new Error('Submit button has formnorequired data attribute. Will skip setup of reactive submit button.');
}
const submitButtons = Array.from(this._element.querySelectorAll('[type="submit"]'));
if (!submitButtons || !submitButtons.length) {
throw new Error('Reactive Submit Button utility couldn\'t find any submit buttons!');
}
this._submitButton = submitButtons.reverse()[0];
// abort if form has param data-formnorequired
if (this._submitButton.dataset.formnorequired !== undefined) {
return false;
}
this.setupInputs();
this.updateButtonState();
this._element.classList.add(REACTIVE_SUBMIT_BUTTON_INITIALIZED_CLASS);
}
destroy() {
// TODO
}
setupInputs() {
this._requiredInputs.forEach(function(el) {
var checkbox = el.getAttribute('type') === 'checkbox';
var eventType = checkbox ? 'change' : 'input';
el.addEventListener(eventType, function() {
this.updateButtonState();
});
});
}
updateButtonState() {
if (this.inputsValid()) {
this._submitButton.removeAttribute('disabled');
} else {
this._submitButton.setAttribute('disabled', 'true');
}
}
inputsValid() {
var done = true;
this._requiredInputs.forEach(function(inp) {
var len = inp.value.trim().length;
if (done && len === 0) {
done = false;
}
});
return done;
}
}

View File

@ -0,0 +1,17 @@
# Reactive Submit Button Utility
Disables a forms LAST sumit button as long as the required inputs are invalid
(only checks if the value of the inputs are not empty)
## Attribute: (none)
(automatically setup on all form tags)
## Params:
- `data-formnorequired: string`\
If present the submit button will never get disabled
## Example usage:
```html
<form uw-reactive-submit-button>
<input type="text" required>
<button type="submit">
```

View File

@ -0,0 +1,43 @@
import { Utility } from "../../core/utility";
import './checkbox.scss';
var CHECKBOX_CLASS = 'checkbox';
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
@Utility({
selector: 'input[type="checkbox"]',
})
export class Checkbox {
constructor(element) {
if (!element) {
throw new Error('Checkbox utility cannot be setup without an element!');
}
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
// throw new Error('Checkbox utility already initialized!');
return false;
}
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
// throw new Error('Checkbox element\'s wrapper already has class "' + CHECKBOX_CLASS + '"!');
return false;
}
var siblingEl = element.nextSibling;
var parentEl = element.parentElement;
var wrapperEl = document.createElement('div');
wrapperEl.classList.add(CHECKBOX_CLASS);
var labelEl = document.createElement('label');
labelEl.setAttribute('for', element.id);
wrapperEl.appendChild(element);
wrapperEl.appendChild(labelEl);
parentEl.insertBefore(wrapperEl, siblingEl);
element.classList.add(CHECKBOX_INITIALIZED_CLASS);
}
}

View File

@ -0,0 +1,10 @@
# Checkbox Utility
Wraps native checkbox
## Attribute: (none)
(element must be an input of type="checkbox")
## Example usage:
```html
<input type="checkbox">
```

View File

@ -0,0 +1,99 @@
import { Utility } from "../../core/utility";
const FILE_INPUT_CLASS = 'file-input';
const FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';
const FILE_INPUT_LIST_CLASS = 'file-input__list';
const FILE_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
const FILE_INPUT_LABEL_CLASS = 'file-input__label';
@Utility({
selector: 'input[type="file"][uw-file-input]',
})
export class FileInput {
_element;
_app;
_isMultiFileInput = false;
_fileList;
_label;
constructor(element, app) {
if (!element) {
throw new Error('FileInput utility cannot be setup without an element!');
}
this._element = element;
this._app = app;
if (this._element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) {
throw new Error('FileInput utility already initialized!');
}
// check if is multi-file input
this._isMultiFileInput = this._element.hasAttribute('multiple');
if (this._isMultiFileInput) {
this._fileList = this._createFileList();
}
this._label = this._createFileLabel();
this._updateLabel();
// add change listener
this._element.addEventListener('change', function() {
this.updateLabel();
this.renderFileList();
});
// add util class for styling and mark as initialized
this._element.classList.add(FILE_INPUT_CLASS);
this._element.classList.add(FILE_INPUT_INITIALIZED_CLASS);
}
destroy() {
// TODO
}
_renderFileList() {
if (!this._fileList) {
return;
}
const files = this._element.files;
this._fileList.innerHTML = '';
Array.from(files).forEach(function(file) {
const fileDisplayEl = document.createElement('li');
fileDisplayEl.innerHTML = file.name;
this.fileList.appendChild(fileDisplayEl);
});
}
_createFileList() {
const list = document.createElement('ol');
list.classList.add(FILE_INPUT_LIST_CLASS);
const unpackEl = this._element.parentElement.querySelector('.' + FILE_INPUT_UNPACK_CHECKBOX_CLASS);
if (unpackEl) {
this._element.parentElement.insertBefore(list, unpackEl);
} else {
this._element.parentElement.appendChild(list);
}
return list;
}
_createFileLabel() {
const label = document.createElement('label');
label.classList.add(FILE_INPUT_LABEL_CLASS);
label.setAttribute('for', this._element.id);
this._element.parentElement.insertBefore(label, this._element);
return label;
}
_updateLabel() {
const files = this._element.files;
if (files && files.length) {
this._label.innerText = this._isMultiFileInput ? files.length + ' ' + this._app.i18n.get('filesSelected') : files[0].name;
} else {
this._label.innerText = this._isMultiFileInput ? this._app.i18n.get('selectFiles') : this._app.i18n.get('selectFile');
}
}
}

View File

@ -0,0 +1,23 @@
# FileInput Utility
Wraps native file input
## Attribute: `uw-file-input`
(element must be an input of type='file')
## Example usage:
```html
<input type='file' uw-file-input>
```
## Internationalization:
This utility expects the following translations to be available:
- `filesSelected`:\
label of multi-input button after selection\
*example*: "Dateien ausgewählt" (will be prepended by number of selected files)
- `selectFile`:\
label of single-input button before selection\
*example*: "Datei auswählen"
- `selectFiles`:\
label of multi-input button before selection\
*example*: "Datei(en) auswählen"

View File

@ -1,194 +1,10 @@
import './checkbox.scss';
import './radio.scss';
import { Checkbox } from './checkbox';
import { FileInput } from './file-input';
import './inputs.scss';
import './radio.scss';
/**
*
* FileInput Utility
* wraps native file input
*
* Attribute: uw-file-input
* (element must be an input of type='file')
*
* Example usage:
* <input type='file' uw-file-input>
*
* Internationalization:
* This utility expects the following translations to be available:
* »filesSelected«: label of multi-input button after selection
* example: "Dateien ausgewählt" (will be prepended by number of selected files)
* »selectFile«: label of single-input button before selection
* example: "Datei auswählen"
* »selectFiles«: label of multi-input button before selection
* example: "Datei(en) auswählen"
*
*/
var FILE_INPUT_UTIL_NAME = 'fileInput';
var FILE_INPUT_UTIL_SELECTOR = 'input[type="file"][uw-file-input]';
var FILE_INPUT_CLASS = 'file-input';
var FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';
var FILE_INPUT_LIST_CLASS = 'file-input__list';
var FILE_INPUT_UNPACK_CHECKBOX_CLASS = 'file-input__unpack';
var FILE_INPUT_LABEL_CLASS = 'file-input__label';
var fileInputUtil = function(element, app) {
var isMultiFileInput = false;
var fileList;
var label;
function init() {
if (!element) {
throw new Error('FileInput utility cannot be setup without an element!');
}
if (element.classList.contains(FILE_INPUT_INITIALIZED_CLASS)) {
throw new Error('FileInput utility already initialized!');
}
// check if is multi-file input
isMultiFileInput = element.hasAttribute('multiple');
if (isMultiFileInput) {
fileList = createFileList();
}
label = createFileLabel();
updateLabel();
// add change listener
element.addEventListener('change', function() {
updateLabel();
renderFileList();
});
// add util class for styling and mark as initialized
element.classList.add(FILE_INPUT_CLASS);
element.classList.add(FILE_INPUT_INITIALIZED_CLASS);
return {
name: FILE_INPUT_UTIL_NAME,
element: element,
destroy: function() {},
};
}
function renderFileList() {
if (!fileList) {
return;
}
var files = element.files;
fileList.innerHTML = '';
Array.from(files).forEach(function(file) {
var fileDisplayEl = document.createElement('li');
fileDisplayEl.innerHTML = file.name;
fileList.appendChild(fileDisplayEl);
});
}
function createFileList() {
var list = document.createElement('ol');
list.classList.add(FILE_INPUT_LIST_CLASS);
var unpackEl = element.parentElement.querySelector('.' + FILE_INPUT_UNPACK_CHECKBOX_CLASS);
if (unpackEl) {
element.parentElement.insertBefore(list, unpackEl);
} else {
element.parentElement.appendChild(list);
}
return list;
}
function createFileLabel() {
var label = document.createElement('label');
label.classList.add(FILE_INPUT_LABEL_CLASS);
label.setAttribute('for', element.id);
element.parentElement.insertBefore(label, element);
return label;
}
function updateLabel() {
var files = element.files;
if (files && files.length) {
label.innerText = isMultiFileInput ? files.length + ' ' + app.i18n.get('filesSelected') : files[0].name;
} else {
label.innerText = isMultiFileInput ? app.i18n.get('selectFiles') : app.i18n.get('selectFile');
}
}
return init();
};
export const fileInput = {
name: FILE_INPUT_UTIL_NAME,
selector: FILE_INPUT_UTIL_SELECTOR,
setup: fileInputUtil,
};
/**
*
* Checkbox Utility
* wraps native checkbox
*
* Attribute: (none)
* (element must be an input of type="checkbox")
*
* Example usage:
* <input type="checkbox">
*
*/
var CHECKBOX_UTIL_NAME = 'checkbox';
var CHECKBOX_UTIL_SELECTOR = 'input[type="checkbox"]';
var CHECKBOX_CLASS = 'checkbox';
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
var checkboxUtil = function(element) {
function init() {
if (!element) {
throw new Error('Checkbox utility cannot be setup without an element!');
}
if (element.classList.contains(CHECKBOX_INITIALIZED_CLASS)) {
// throw new Error('Checkbox utility already initialized!');
return false;
}
if (element.parentElement.classList.contains(CHECKBOX_CLASS)) {
// throw new Error('Checkbox element\'s wrapper already has class "' + CHECKBOX_CLASS + '"!');
return false;
}
var siblingEl = element.nextSibling;
var parentEl = element.parentElement;
var wrapperEl = document.createElement('div');
wrapperEl.classList.add(CHECKBOX_CLASS);
var labelEl = document.createElement('label');
labelEl.setAttribute('for', element.id);
wrapperEl.appendChild(element);
wrapperEl.appendChild(labelEl);
parentEl.insertBefore(wrapperEl, siblingEl);
element.classList.add(CHECKBOX_INITIALIZED_CLASS);
return {
name: CHECKBOX_UTIL_NAME,
element: element,
destroy: function() {},
};
}
return init();
};
export const checkbox = {
name: CHECKBOX_UTIL_NAME,
selector: CHECKBOX_UTIL_SELECTOR,
setup: checkboxUtil,
};
export const InputUtils = [
Checkbox,
FileInput,
];

View File

@ -1,176 +1,163 @@
/**
*
* Mass Input Utility
* allows form shapes to be manipulated asynchronously:
* will asynchronously submit the containing form and replace the contents
* of the mass input element with the one from the BE response
* The utility will only trigger an AJAX request if the mass input element has
* an active/focused element whilst the form is being submitted.
*
* Attribute: uw-mass-input
*
* Example usage:
* <form method="POST" action="...">
* <input type="text">
* <div uw-mass-input>
* <input type="text">
* <button type="submit">
*/
import { Utility } from "../../core/utility";
var MASS_INPUT_UTIL_NAME = 'massInput';
var MASS_INPUT_UTIL_SELECTOR = '[uw-mass-input]';
const MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
const MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add';
const MASS_INPUT_SUBMIT_BUTTON_CLASS = 'massinput__submit-button';
const MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized';
var MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
var MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add';
var MASS_INPUT_SUBMIT_BUTTON_CLASS = 'massinput__submit-button';
var MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized';
@Utility({
selector: '[uw-mass-input]',
})
export class MassInput {
var massInputUtil = function(element, app) {
var massInputId;
var massInputFormSubmitHandler;
var massInputForm;
_element;
_app;
function init() {
_massInputId;
_massInputFormSubmitHandler;
_massInputForm;
constructor(element, app) {
if (!element) {
throw new Error('Mass Input utility cannot be setup without an element!');
}
if (element.classList.contains(MASS_INPUT_INITIALIZED_CLASS)) {
this._element = element;
this._app = app;
if (this._element.classList.contains(MASS_INPUT_INITIALIZED_CLASS)) {
return false;
}
massInputId = element.dataset.massInputIdent || '_';
massInputForm = element.closest('form');
this._massInputId = this._element.dataset.massInputIdent || '_';
this._massInputForm = this._element.closest('form');
if (!massInputForm) {
if (!this._massInputForm) {
throw new Error('Mass Input utility cannot be setup without being wrapped in a <form>!');
}
massInputFormSubmitHandler = makeSubmitHandler();
this._massInputFormSubmitHandler = this._makeSubmitHandler();
// setup submit buttons inside this massinput so browser
// uses correct submit button for form submission.
var buttons = getMassInputSubmitButtons();
const buttons = this._getMassInputSubmitButtons();
buttons.forEach(function(button) {
setupSubmitButton(button);
this.setupSubmitButton(button);
});
massInputForm.addEventListener('submit', massInputFormSubmitHandler);
massInputForm.addEventListener('keypress', keypressHandler);
this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler);
this._massInputForm.addEventListener('keypress', this._keypressHandler);
// mark initialized
element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
return {
name: MASS_INPUT_UTIL_NAME,
element: element,
destroy: function() {
reset();
},
};
this._element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
}
function makeSubmitHandler() {
var method = massInputForm.getAttribute('method') || 'POST';
var url = massInputForm.getAttribute('action') || window.location.href;
var enctype = massInputForm.getAttribute('enctype') || 'application/json';
destroy() {
this._reset();
}
var requestFn;
if (app.httpClient[method.toLowerCase()]) {
requestFn = app.httpClient[method.toLowerCase()].bind(app.httpClient);
_makeSubmitHandler() {
const method = this._massInputForm.getAttribute('method') || 'POST';
const url = this._massInputForm.getAttribute('action') || window.location.href;
const enctype = this._massInputForm.getAttribute('enctype') || 'application/json';
let requestFn;
if (this._app.httpClient[method.toLowerCase()]) {
requestFn = this._app.httpClient[method.toLowerCase()].bind(this._app.httpClient);
}
return function(event) {
var activeElement;
let activeElement;
// check if event occured from either a mass input add/delete button or
// from inside one of massinput's inputs (i.e. a child is focused/active)
activeElement = element.querySelector(':focus, :active');
activeElement = this._element.querySelector(':focus, :active');
if (!activeElement) {
return false;
}
// find the according massinput cell thats hosts the element that triggered the submit
var massInputCell = activeElement.closest(MASS_INPUT_CELL_SELECTOR);
const massInputCell = activeElement.closest(MASS_INPUT_CELL_SELECTOR);
if (!massInputCell) {
return false;
}
var submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS);
const submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS);
if (!submitButton) {
return false;
}
var isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR);
var submitButtonIsActive = submitButton.matches(':focus, :active');
const isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR);
const submitButtonIsActive = submitButton.matches(':focus, :active');
// if the cell is not an add cell the active element must at least be the cells submit button
if (!isAddCell && !submitButtonIsActive) {
return false;
}
event.preventDefault();
var requestBody = serializeForm(submitButton, enctype);
const requestBody = this.serializeForm(submitButton, enctype);
if (requestFn && requestBody) {
var headers = {'Mass-Input-Shortcircuit': massInputId};
const headers = {'Mass-Input-Shortcircuit': this.massInputId};
if (enctype !== 'multipart/form-data')
if (enctype !== 'multipart/form-data') {
headers['Content-Type'] = enctype;
}
requestFn({
url: url,
headers: headers,
body: requestBody,
}).then(function(response) {
return app.htmlHelpers.parseResponse(response);
return this._app.htmlHelpers.parseResponse(response);
}).then(function(response) {
processResponse(response.element);
this.processResponse(response.element);
if (isAddCell) {
reFocusAddCell();
this.reFocusAddCell();
}
});
}
};
}
function keypressHandler(event) {
_keypressHandler(event) {
if (event.keyCode !== 13) {
return false;
}
if (massInputFormSubmitHandler) {
return massInputFormSubmitHandler(event);
if (this._massInputFormSubmitHandler) {
return this._massInputFormSubmitHandler(event);
}
}
function getMassInputSubmitButtons() {
return Array.from(element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));
_getMassInputSubmitButtons() {
return Array.from(this._element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));
}
function setupSubmitButton(button) {
_setupSubmitButton(button) {
button.setAttribute('type', 'button');
button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS);
button.addEventListener('click', massInputFormSubmitHandler);
button.addEventListener('click', this._massInputFormSubmitHandler);
}
function resetSubmitButton(button) {
_resetSubmitButton(button) {
button.setAttribute('type', 'submit');
button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS);
button.removeEventListener('click', massInputFormSubmitHandler);
button.removeEventListener('click', this._massInputFormSubmitHandler);
}
function processResponse(responseElement) {
element.innerHTML = "";
element.appendChild(responseElement);
_processResponse(responseElement) {
this._element.innerHTML = "";
this._element.appendChild(responseElement);
reset();
this._reset();
app.utilRegistry.setupAll(element);
this._app.utilRegistry.setupAll(this._element);
}
function serializeForm(submitButton, enctype) {
var formData = new FormData(massInputForm);
_serializeForm(submitButton, enctype) {
const formData = new FormData(this._massInputForm);
// manually add name and value of submit button to formData
formData.append(submitButton.name, submitButton.value);
@ -184,35 +171,27 @@ var massInputUtil = function(element, app) {
}
}
function reFocusAddCell() {
var addCell = element.querySelector(MASS_INPUT_ADD_CELL_SELECTOR);
_reFocusAddCell() {
const addCell = this._element.querySelector(MASS_INPUT_ADD_CELL_SELECTOR);
if (!addCell) {
return false;
}
var addCellInput = addCell.querySelector('input:not([type="hidden"])');
const addCellInput = addCell.querySelector('input:not([type="hidden"])');
if (addCellInput) {
// Clearing of add-inputs is done in the backend
addCellInput.focus();
}
}
function reset() {
element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
massInputForm.removeEventListener('submit', massInputFormSubmitHandler);
massInputForm.removeEventListener('keypress', keypressHandler);
_reset() {
this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
this._massInputForm.removeEventListener('submit', this._massInputFormSubmitHandler);
this._massInputForm.removeEventListener('keypress', this._keypressHandler);
var buttons = getMassInputSubmitButtons();
const buttons = this._getMassInputSubmitButtons();
buttons.forEach(function(button) {
resetSubmitButton(button);
this.resetSubmitButton(button);
});
}
return init();
};
export default {
name: MASS_INPUT_UTIL_NAME,
selector: MASS_INPUT_UTIL_SELECTOR,
setup: massInputUtil,
};
}

View File

@ -0,0 +1,17 @@
# Mass Input Utility
Allows form shapes to be manipulated asynchronously.
Will asynchronously submit the containing form and replace the contents of the mass input element with the one from the BE response.
The utility will only trigger an AJAX request if the mass input element has an active/focused element whilst the form is being submitted.
## Attribute: `uw-mass-input`
## Example usage:
```html
<form method="POST" action="...">
<input type="text">
<div uw-mass-input>
<input type="text">
<button type="submit">
```

View File

@ -1,203 +1,182 @@
import { Utility } from '../../core/utility';
import './modal.scss';
/**
*
* Modal Utility
*
* Attribute: uw-modal
*
* Params:
* data-modal-trigger: string
* Selector for the element that toggles the modal.
* If trigger element has "href" attribute the modal will be dynamically loaded from the referenced page
* data-modal-closeable: boolean property
* If the param is present the modal will have a close-icon and can also be closed by clicking anywhere on the overlay
*
* Example usage:
* <div uw-modal data-modal-trigger='#trigger' data-modal-closeable>This is the modal content
* <div id='trigger'>Click me to open the modal
*/
var MODAL_UTIL_NAME = 'modal';
var MODAL_UTIL_SELECTOR = '[uw-modal]';
var MODAL_HEADERS = {
const MODAL_HEADERS = {
'Is-Modal': 'True',
};
var MODAL_INITIALIZED_CLASS = 'modal--initialized';
var MODAL_CLASS = 'modal';
var MODAL_OPEN_CLASS = 'modal--open';
var MODAL_TRIGGER_CLASS = 'modal__trigger';
var MODAL_CONTENT_CLASS = 'modal__content';
var MODAL_OVERLAY_CLASS = 'modal__overlay';
var MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
var MODAL_CLOSER_CLASS = 'modal__closer';
const MODAL_INITIALIZED_CLASS = 'modal--initialized';
const MODAL_CLASS = 'modal';
const MODAL_OPEN_CLASS = 'modal--open';
const MODAL_TRIGGER_CLASS = 'modal__trigger';
const MODAL_CONTENT_CLASS = 'modal__content';
const MODAL_OVERLAY_CLASS = 'modal__overlay';
const MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
const MODAL_CLOSER_CLASS = 'modal__closer';
var MAIN_CONTENT_CLASS = 'main__content-body';
const MAIN_CONTENT_CLASS = 'main__content-body';
// one singleton wrapper to keep all the modals to avoid CSS bug
// with blurry text due to `transform: translate(-50%, -50%)`
// will be created (and reused) for the first modal that gets initialized
var MODALS_WRAPPER_CLASS = 'modals-wrapper';
var MODALS_WRAPPER_SELECTOR = '.' + MODALS_WRAPPER_CLASS;
var MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
const MODALS_WRAPPER_CLASS = 'modals-wrapper';
const MODALS_WRAPPER_SELECTOR = '.' + MODALS_WRAPPER_CLASS;
const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
var modalUtil = function(element, app) {
@Utility({
selector: '[uw-modal]',
})
export class Modal {
var modalsWrapper;
var modalOverlay;
var modalUrl;
_element;
_app;
function _init() {
_modalsWrapper;
_modalOverlay;
_modalUrl;
constructor(element, app) {
if (!element) {
throw new Error('Modal utility cannot be setup without an element!');
}
if (element.classList.contains(MODAL_INITIALIZED_CLASS)) {
this._element = element;
this._app = app;
if (this._element.classList.contains(MODAL_INITIALIZED_CLASS)) {
return false;
}
ensureModalWrapper();
this._ensureModalWrapper();
// param modalTrigger
if (!element.dataset.modalTrigger) {
if (!this._element.dataset.modalTrigger) {
throw new Error('Modal utility cannot be setup without a trigger element!');
} else {
setupTrigger();
this._setupTrigger();
}
// param modalCloseable
if (element.dataset.modalCloseable !== undefined) {
setupCloser();
if (this._element.dataset.modalCloseable !== undefined) {
this._setupCloser();
}
// mark as initialized and add modal class for styling
element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
return {
name: MODAL_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
}
function ensureModalWrapper() {
modalsWrapper = document.querySelector(MODALS_WRAPPER_SELECTOR);
if (!modalsWrapper) {
destroy() {
// TODO
}
_ensureModalWrapper() {
this._modalsWrapper = document.querySelector(MODALS_WRAPPER_SELECTOR);
if (!this._modalsWrapper) {
// create modal wrapper
modalsWrapper = document.createElement('div');
modalsWrapper.classList.add(MODALS_WRAPPER_CLASS);
document.body.appendChild(modalsWrapper);
this._modalsWrapper = document.createElement('div');
this._modalsWrapper.classList.add(MODALS_WRAPPER_CLASS);
document.body.appendChild(this._modalsWrapper);
}
modalOverlay = modalsWrapper.querySelector('.' + MODAL_OVERLAY_CLASS);
if (!modalOverlay) {
this._modalOverlay = this._modalsWrapper.querySelector('.' + MODAL_OVERLAY_CLASS);
if (!this._modalOverlay) {
// create modal overlay
modalOverlay = document.createElement('div');
modalOverlay.classList.add(MODAL_OVERLAY_CLASS);
modalsWrapper.appendChild(modalOverlay);
this._modalOverlay = document.createElement('div');
this._modalOverlay.classList.add(MODAL_OVERLAY_CLASS);
this._modalsWrapper.appendChild(this._modalOverlay);
}
}
function setupTrigger() {
var triggerSelector = element.dataset.modalTrigger;
_setupTrigger() {
let triggerSelector = this._element.dataset.modalTrigger;
if (!triggerSelector.startsWith('#')) {
triggerSelector = '#' + triggerSelector;
}
var triggerElement = document.querySelector(triggerSelector);
const triggerElement = document.querySelector(triggerSelector);
if (!triggerElement) {
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
}
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
triggerElement.addEventListener('click', onTriggerClicked, false);
modalUrl = triggerElement.getAttribute('href');
triggerElement.addEventListener('click', this._onTriggerClicked, false);
this._modalUrl = triggerElement.getAttribute('href');
}
function setupCloser() {
var closerElement = document.createElement('div');
element.insertBefore(closerElement, null);
_setupCloser() {
const closerElement = document.createElement('div');
this._element.insertBefore(closerElement, null);
closerElement.classList.add(MODAL_CLOSER_CLASS);
closerElement.addEventListener('click', onCloseClicked, false);
modalOverlay.addEventListener('click', onCloseClicked, false);
closerElement.addEventListener('click', this._onCloseClicked, false);
this._modalOverlay.addEventListener('click', this._onCloseClicked, false);
}
function onTriggerClicked(event) {
_onTriggerClicked = (event) => {
event.preventDefault();
open();
this._open();
}
function onCloseClicked(event) {
_onCloseClicked = (event) => {
event.preventDefault();
close();
this._close();
}
function onKeyUp(event) {
_onKeyUp = (event) => {
if (event.key === 'Escape') {
close();
this._close();
}
}
function open() {
element.classList.add(MODAL_OPEN_CLASS);
modalOverlay.classList.add(MODAL_OVERLAY_OPEN_CLASS);
modalsWrapper.classList.add(MODALS_WRAPPER_OPEN_CLASS);
modalsWrapper.appendChild(element);
_open() {
this._element.classList.add(MODAL_OPEN_CLASS);
this._modalOverlay.classList.add(MODAL_OVERLAY_OPEN_CLASS);
this._modalsWrapper.classList.add(MODALS_WRAPPER_OPEN_CLASS);
this._modalsWrapper.appendChild(this._element);
if (modalUrl) {
fillModal(modalUrl);
if (this._modalUrl) {
this._fillModal(this._modalUrl);
}
document.addEventListener('keyup', onKeyUp);
document.addEventListener('keyup', this._onKeyUp);
}
function close() {
modalOverlay.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
element.classList.remove(MODAL_OPEN_CLASS);
modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS);
_close() {
this._modalOverlay.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
this._element.classList.remove(MODAL_OPEN_CLASS);
this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS);
document.removeEventListener('keyup', onKeyUp);
document.removeEventListener('keyup', this._onKeyUp);
};
function fillModal(url) {
app.httpClient.get({
_fillModal(url) {
this._app.httpClient.get({
url: url,
headers: MODAL_HEADERS,
}).then(function(response) {
return app.htmlHelpers.parseResponse(response);
}).then(function(response) {
processResponse(response.element);
});
}).then(
(response) => this._app.htmlHelpers.parseResponse(response)
).then(
(response) => this._processResponse(response.element)
);
}
function processResponse(responseElement) {
var modalContent = document.createElement('div');
_processResponse(responseElement) {
const modalContent = document.createElement('div');
modalContent.classList.add(MODAL_CONTENT_CLASS);
var contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
const contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
if (contentBody) {
modalContent.innerHTML = contentBody.innerHTML;
}
var previousModalContent = element.querySelector('.' + MODAL_CONTENT_CLASS);
const previousModalContent = this._element.querySelector('.' + MODAL_CONTENT_CLASS);
if (previousModalContent) {
previousModalContent.remove();
}
element.insertBefore(modalContent, null);
this._element.insertBefore(modalContent, null);
// setup any newly arrived utils
app.utilRegistry.setupAll(element);
this._app.utilRegistry.setupAll(this._element);
}
return _init();
};
export default {
name: MODAL_UTIL_NAME,
selector: MODAL_UTIL_SELECTOR,
setup: modalUtil,
};
}

View File

@ -0,0 +1,16 @@
# Modal Utility
## Attribute: `uw-modal`
## Params:
- `data-modal-trigger: string`\
Selector for the element that toggles the modal.
If trigger element has "href" attribute the modal will be dynamically loaded from the referenced page
- `data-modal-closeable: boolean`\
If the param is present the modal will have a close-icon and can also be closed by clicking anywhere on the overlay
## Example usage:
```html
<div uw-modal data-modal-trigger='#trigger' data-modal-closeable>This is the modal content
<div id='trigger'>Click me to open the modal
```

View File

@ -1,109 +1,91 @@
import { Utility } from '../../core/utility';
import './show-hide.scss';
var SHOW_HIDE_UTIL_NAME = 'showHide';
var SHOW_HIDE_UTIL_SELECTOR = '[uw-show-hide]';
const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
const SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
const SHOW_HIDE_COLLAPSED_CLASS = 'show-hide--collapsed';
const SHOW_HIDE_TOGGLE_CLASS = 'show-hide__toggle';
const SHOW_HIDE_TOGGLE_RIGHT_CLASS = 'show-hide__toggle--right';
var SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
var SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
var SHOW_HIDE_COLLAPSED_CLASS = 'show-hide--collapsed';
var SHOW_HIDE_TOGGLE_CLASS = 'show-hide__toggle';
var SHOW_HIDE_TOGGLE_RIGHT_CLASS = 'show-hide__toggle--right';
@Utility({
selector: '[uw-show-hide]',
})
export class ShowHide {
/**
*
* ShowHide Utility
*
* Attribute: uw-show-hide
*
* Params: (all optional)
* data-show-hide-id: string
* If this param is given the state of the utility will be persisted in the clients local storage.
* data-show-hide-collapsed: boolean property
* If this param is present the ShowHide utility will be collapsed. This value will be overruled by any value stored in the LocalStorage.
* data-show-hide-align: 'right'
* Where to put the arrow that marks the element as a ShowHide toggle. Left of toggle by default.
*
* Example usage:
* <div>
* <div uw-show-hide>Click me
* <div>This will be toggled
* <div>This will be toggled as well
*/
var showHideUtil = function(element) {
_showHideId;
_element;
var showHideId;
function init() {
constructor(element) {
if (!element) {
throw new Error('ShowHide utility cannot be setup without an element!');
}
if (element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) {
this._element = element;
if (this._element.classList.contains(SHOW_HIDE_INITIALIZED_CLASS)) {
return false;
}
// register click listener
addClickListener();
this._addClickListener();
// param showHideId
if (element.dataset.showHideId) {
showHideId = element.dataset.showHideId;
if (this._element.dataset.showHideId) {
this._showHideId = this._element.dataset.showHideId;
}
// param showHideCollapsed
var collapsed = false;
if (element.dataset.showHideCollapsed !== undefined) {
let collapsed = false;
if (this._element.dataset.showHideCollapsed !== undefined) {
collapsed = true;
}
if (showHideId) {
var localStorageCollapsed = getLocalStorage()[showHideId];
if (this._showHideId) {
let localStorageCollapsed = this._getLocalStorage()[this._showHideId];
if (typeof localStorageCollapsed !== 'undefined') {
collapsed = localStorageCollapsed;
}
}
element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
// param showHideAlign
var alignment = element.dataset.showHideAlign;
const alignment = this._element.dataset.showHideAlign;
if (alignment === 'right') {
element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
this._element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
}
// mark as initialized
element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
return {
name: SHOW_HIDE_UTIL_NAME,
element: element,
destroy: function() {},
};
this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
}
function addClickListener() {
element.addEventListener('click', function clickListener() {
var newState = element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
if (showHideId) {
setLocalStorage(showHideId, newState);
}
});
destroy() {
this._removeClickListener();
}
function setLocalStorage(id, state) {
var lsData = getLocalStorage();
_removeClickListener() {
this._element.removeEventListener('click', this._clickHandler);
}
_addClickListener() {
this._element.addEventListener('click', this._clickHandler);
}
_clickHandler = () => {
const newState = this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
if (this.__showHideId) {
this._setLocalStorage(this.__showHideId, newState);
}
}
// maybe move these to a LocalStorageHelper?
_setLocalStorage(id, state) {
const lsData = this._getLocalStorage();
lsData[id] = state;
window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData));
}
function getLocalStorage() {
_getLocalStorage() {
return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {};
}
return init();
};
export default {
name: SHOW_HIDE_UTIL_NAME,
selector: SHOW_HIDE_UTIL_SELECTOR,
setup: showHideUtil,
};
}

View File

@ -0,0 +1,21 @@
# ShowHide
Allows to toggle the visibilty of an element by clicking another element.
## Attribute: `uw-show-hide`
## Params:
- `data-show-hide-id: string` (optional)\
If this param is given the state of the utility will be persisted in the clients local storage.
- `data-show-hide-collapsed: boolean` (optional)\
If this param is present the ShowHide utility will be collapsed. This value will be overruled by any value stored in the LocalStorage.
- `data-show-hide-align: 'right'` (optional)\
Where to put the arrow that marks the element as a ShowHide toggle. Left of toggle by default.
## Example usage:
```html
<div>
<div uw-show-hide>Click me
<div>This will be toggled
<div>This will be toggled as well
```

View File

@ -1,7 +1,10 @@
import { Utility } from '../../core/utility';
import './tooltips.scss';
export default {
name: 'tooltips',
// empty shell to be able to load styles
@Utility({
selector: '[not-something-that-would-be-found]',
setup: () => {},
})
export class Tooltip {
destroy() {}
};

View File

@ -1,38 +1,26 @@
import alerts from './alerts/alerts';
import asidenav from './asidenav/asidenav';
import asyncForm from './async-form/async-form';
import asyncTable from './async-table/async-table';
import checkAll from './check-all/check-all';
import massInput from './mass-input/mass-input';
import { fileInput, checkbox } from './inputs/inputs';
import modal from './modal/modal';
import showHide from './show-hide/show-hide';
import {
interactiveFieldset,
navigateAwayPrompt,
autoSubmitButton,
autoSubmitInput,
formErrorRemover,
datepicker,
} from './form/form';
import tooltips from './tooltips/tooltips';
import { Alerts } from "./alerts/alerts";
import { Asidenav } from "./asidenav/asidenav";
import { AsyncForm } from "./async-form/async-form";
import { ShowHide } from "./show-hide/show-hide";
import { AsyncTable } from "./async-table/async-table";
import { CheckAll } from "./check-all/check-all";
import { FormUtils } from "./form/form";
import { InputUtils } from "./inputs/inputs";
import { MassInput } from "./mass-input/mass-input";
import { Modal } from "./modal/modal";
import { Tooltip } from "./tooltips/tooltips";
export const Utils = [
alerts,
asidenav,
asyncForm,
asyncTable,
checkAll,
massInput,
fileInput,
checkbox,
modal,
showHide,
interactiveFieldset,
navigateAwayPrompt,
autoSubmitButton,
autoSubmitInput,
formErrorRemover,
datepicker,
tooltips,
Alerts,
Asidenav,
AsyncForm,
AsyncTable,
CheckAll,
ShowHide,
...FormUtils,
...InputUtils,
MassInput,
Modal,
ShowHide,
Tooltip,
];