refactor js utilities to use class syntax and Utility decorator
This commit is contained in:
parent
b31514d9c6
commit
befa9f3941
@ -2,6 +2,7 @@ import { HttpClient } from './services/http-client/http-client';
|
|||||||
import { HtmlHelpers } from './services/html-helpers/html-helpers';
|
import { HtmlHelpers } from './services/html-helpers/html-helpers';
|
||||||
import { I18n } from './services/i18n/i18n';
|
import { I18n } from './services/i18n/i18n';
|
||||||
import { UtilRegistry } from './services/util-registry/util-registry';
|
import { UtilRegistry } from './services/util-registry/util-registry';
|
||||||
|
import { isValidUtility } from './core/utility';
|
||||||
|
|
||||||
export class App {
|
export class App {
|
||||||
httpClient = new HttpClient();
|
httpClient = new HttpClient();
|
||||||
@ -11,6 +12,8 @@ export class App {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.utilRegistry.setApp(this);
|
this.utilRegistry.setApp(this);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => this.utilRegistry.setupAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
registerUtilities(utils) {
|
registerUtilities(utils) {
|
||||||
@ -18,7 +21,7 @@ export class App {
|
|||||||
throw new Error('Utils are expected to be passed as array!');
|
throw new Error('Utils are expected to be passed as array!');
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.forEach((util) => {
|
utils.filter(isValidUtility).forEach((util) => {
|
||||||
this.utilRegistry.register(util);
|
this.utilRegistry.register(util);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,12 @@ describe('App', () => {
|
|||||||
expect(app).toBeTruthy();
|
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', () => {
|
describe('provides services', () => {
|
||||||
it('HttpClient as httpClient', () => {
|
it('HttpClient as httpClient', () => {
|
||||||
expect(app.httpClient).toBeTruthy();
|
expect(app.httpClient).toBeTruthy();
|
||||||
|
|||||||
22
frontend/src/core/utility.js
Normal file
22
frontend/src/core/utility.js
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -6,10 +6,6 @@ export class UtilRegistry {
|
|||||||
_activeUtilInstances = [];
|
_activeUtilInstances = [];
|
||||||
_appInstance;
|
_appInstance;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => this.setupAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* function registerUtil
|
* function registerUtil
|
||||||
*
|
*
|
||||||
@ -63,14 +59,14 @@ export class UtilRegistry {
|
|||||||
console.log('setting up util', { util });
|
console.log('setting up util', { util });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (util && typeof util.setup === 'function') {
|
if (util) {
|
||||||
const elements = this._findUtilElements(util, scope);
|
const elements = this._findUtilElements(util, scope);
|
||||||
|
|
||||||
elements.forEach((element) => {
|
elements.forEach((element) => {
|
||||||
let utilInstance = null;
|
let utilInstance = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
utilInstance = util.setup(element, this._appInstance);
|
utilInstance = new util(element, this._appInstance);
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
if (DEBUG_MODE > 0) {
|
if (DEBUG_MODE > 0) {
|
||||||
console.warn('Error while trying to initialize a utility!', { util , element, err });
|
console.warn('Error while trying to initialize a utility!', { util , element, err });
|
||||||
|
|||||||
@ -21,12 +21,6 @@ describe('UtilRegistry', () => {
|
|||||||
expect(utilRegistry).toBeTruthy();
|
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()', () => {
|
describe('register()', () => {
|
||||||
it('should allow to add utilities', () => {
|
it('should allow to add utilities', () => {
|
||||||
utilRegistry.register(TEST_UTILS[0]);
|
utilRegistry.register(TEST_UTILS[0]);
|
||||||
|
|||||||
@ -1,191 +1,158 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './alerts.scss';
|
import './alerts.scss';
|
||||||
|
|
||||||
/**
|
const ALERTS_INITIALIZED_CLASS = 'alerts--initialized';
|
||||||
*
|
const ALERTS_ELEVATED_CLASS = 'alerts--elevated';
|
||||||
* Alerts Utility
|
const ALERTS_TOGGLER_CLASS = 'alerts__toggler';
|
||||||
* makes alerts interactive
|
const ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
|
||||||
*
|
const ALERTS_TOGGLER_APPEAR_DELAY = 120;
|
||||||
* 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
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ALERTS_UTIL_NAME = 'alerts';
|
const ALERT_CLASS = 'alert';
|
||||||
var ALERTS_UTIL_SELECTOR = '[uw-alerts]';
|
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';
|
@Utility({
|
||||||
var ALERTS_ELEVATED_CLASS = 'alerts--elevated';
|
selector: '[uw-alerts]',
|
||||||
var ALERTS_TOGGLER_CLASS = 'alerts__toggler';
|
})
|
||||||
var ALERTS_TOGGLER_VISIBLE_CLASS = 'alerts__toggler--visible';
|
export class Alerts {
|
||||||
var ALERTS_TOGGLER_APPEAR_DELAY = 120;
|
_togglerCheckRequested = false;
|
||||||
|
_togglerElement;
|
||||||
|
_alertElements;
|
||||||
|
|
||||||
var ALERT_CLASS = 'alert';
|
_element;
|
||||||
var ALERT_INITIALIZED_CLASS = 'alert--initialized';
|
_app;
|
||||||
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';
|
|
||||||
|
|
||||||
var alertsUtil = function(element, app) {
|
constructor(element, app) {
|
||||||
var togglerCheckRequested = false;
|
|
||||||
var togglerElement;
|
|
||||||
var alertElements;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Alerts util has to be called with an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
togglerElement = element.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
this._togglerElement = this._element.querySelector('.' + ALERTS_TOGGLER_CLASS);
|
||||||
alertElements = gatherAlertElements();
|
this._alertElements = this._gatherAlertElements();
|
||||||
|
|
||||||
initToggler();
|
if (this._togglerElement) {
|
||||||
initAlerts();
|
this._initToggler();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._initAlerts();
|
||||||
|
|
||||||
// register http client interceptor to filter out Alerts Header
|
// register http client interceptor to filter out Alerts Header
|
||||||
setupHttpInterceptor();
|
this._setupHttpInterceptor();
|
||||||
|
|
||||||
// mark initialized
|
// mark initialized
|
||||||
element.classList.add(ALERTS_INITIALIZED_CLASS);
|
this._element.classList.add(ALERTS_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: ALERTS_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatherAlertElements() {
|
destroy() {
|
||||||
return Array.from(element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) {
|
console.log('TBD: Destroy Alert');
|
||||||
|
}
|
||||||
|
|
||||||
|
_gatherAlertElements() {
|
||||||
|
return Array.from(this._element.querySelectorAll('.' + ALERT_CLASS)).filter(function(alert) {
|
||||||
return !alert.classList.contains(ALERT_INITIALIZED_CLASS);
|
return !alert.classList.contains(ALERT_INITIALIZED_CLASS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initToggler() {
|
_initToggler() {
|
||||||
togglerElement.addEventListener('click', function() {
|
this._togglerElement.addEventListener('click', () => {
|
||||||
alertElements.forEach(function(alertEl) {
|
this._alertElements.forEach((alertEl) => this._toggleAlert(alertEl, true));
|
||||||
toggleAlert(alertEl, true);
|
this._togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
||||||
});
|
|
||||||
togglerElement.classList.remove(ALERTS_TOGGLER_VISIBLE_CLASS);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAlerts() {
|
_initAlerts() {
|
||||||
alertElements.forEach(initAlert);
|
this._alertElements.forEach((alert) => this._initAlert(alert));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAlert(alertElement) {
|
_initAlert(alertElement) {
|
||||||
var autoHideDelay = ALERT_AUTO_HIDE_DELAY;
|
let autoHideDelay = ALERT_AUTO_HIDE_DELAY;
|
||||||
if (alertElement.dataset.decay) {
|
if (alertElement.dataset.decay) {
|
||||||
autoHideDelay = parseInt(alertElement.dataset.decay, 10);
|
autoHideDelay = parseInt(alertElement.dataset.decay, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
var closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS);
|
const closeEl = alertElement.querySelector('.' + ALERT_CLOSER_CLASS);
|
||||||
closeEl.addEventListener('click', function() {
|
closeEl.addEventListener('click', () => {
|
||||||
toggleAlert(alertElement);
|
this._toggleAlert(alertElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
if (autoHideDelay > 0 && alertElement.matches(ALERT_AUTOCLOSING_MATCHER)) {
|
||||||
window.setTimeout(function() {
|
window.setTimeout(() => this._toggleAlert(alertElement), autoHideDelay * 1000);
|
||||||
toggleAlert(alertElement);
|
|
||||||
}, autoHideDelay * 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAlert(alertEl, visible) {
|
_toggleAlert(alertEl, visible) {
|
||||||
alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible);
|
alertEl.classList.toggle(ALERT_INVISIBLE_CLASS, !visible);
|
||||||
checkToggler();
|
this._checkToggler();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkToggler() {
|
_checkToggler() {
|
||||||
if (togglerCheckRequested) {
|
if (this._togglerCheckRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var alertsHidden = alertElements.reduce(function(acc, alert) {
|
const alertsHidden = this._alertElements.reduce(function(acc, alert) {
|
||||||
return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS);
|
return acc && alert.classList.contains(ALERT_INVISIBLE_CLASS);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
window.setTimeout(function() {
|
window.setTimeout(() => {
|
||||||
togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
|
this._togglerElement.classList.toggle(ALERTS_TOGGLER_VISIBLE_CLASS, alertsHidden);
|
||||||
togglerCheckRequested = false;
|
this._togglerCheckRequested = false;
|
||||||
}, ALERTS_TOGGLER_APPEAR_DELAY);
|
}, ALERTS_TOGGLER_APPEAR_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupHttpInterceptor() {
|
_setupHttpInterceptor() {
|
||||||
app.httpClient.addResponseInterceptor(responseInterceptor.bind(this));
|
this._app.httpClient.addResponseInterceptor(this._responseInterceptor.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
function elevateAlerts() {
|
_elevateAlerts() {
|
||||||
element.classList.add(ALERTS_ELEVATED_CLASS);
|
this._element.classList.add(ALERTS_ELEVATED_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function responseInterceptor(response) {
|
_responseInterceptor = (response) => {
|
||||||
var alerts;
|
let alerts;
|
||||||
for (var header of response.headers) {
|
for (const header of response.headers) {
|
||||||
if (header[0] === 'alerts') {
|
if (header[0] === 'alerts') {
|
||||||
var decodedHeader = decodeURIComponent(header[1]);
|
const decodedHeader = decodeURIComponent(header[1]);
|
||||||
alerts = JSON.parse(decodedHeader);
|
alerts = JSON.parse(decodedHeader);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alerts) {
|
if (alerts) {
|
||||||
alerts.forEach(function(alert) {
|
alerts.forEach((alert) => {
|
||||||
var alertElement = createAlertElement(alert.status, alert.content);
|
const alertElement = this._createAlertElement(alert.status, alert.content);
|
||||||
element.appendChild(alertElement);
|
this._element.appendChild(alertElement);
|
||||||
alertElements.push(alertElement);
|
this._alertElements.push(alertElement);
|
||||||
initAlert(alertElement);
|
this._initAlert(alertElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
elevateAlerts();
|
this._elevateAlerts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAlertElement(type, content) {
|
_createAlertElement(type, content) {
|
||||||
var alertElement = document.createElement('div');
|
const alertElement = document.createElement('div');
|
||||||
alertElement.classList.add(ALERT_CLASS, 'alert-' + type);
|
alertElement.classList.add(ALERT_CLASS, 'alert-' + type);
|
||||||
|
|
||||||
var alertCloser = document.createElement('div');
|
const alertCloser = document.createElement('div');
|
||||||
alertCloser.classList.add(ALERT_CLOSER_CLASS);
|
alertCloser.classList.add(ALERT_CLOSER_CLASS);
|
||||||
|
|
||||||
var alertIcon = document.createElement('div');
|
const alertIcon = document.createElement('div');
|
||||||
alertIcon.classList.add(ALERT_ICON_CLASS);
|
alertIcon.classList.add(ALERT_ICON_CLASS);
|
||||||
|
|
||||||
var alertContent = document.createElement('div');
|
const alertContent = document.createElement('div');
|
||||||
alertContent.classList.add(ALERT_CONTENT_CLASS);
|
alertContent.classList.add(ALERT_CONTENT_CLASS);
|
||||||
alertContent.innerHTML = content;
|
alertContent.innerHTML = content;
|
||||||
|
|
||||||
@ -195,12 +162,4 @@ var alertsUtil = function(element, app) {
|
|||||||
|
|
||||||
return alertElement;
|
return alertElement;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return init();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: ALERTS_UTIL_NAME,
|
|
||||||
selector: ALERTS_UTIL_SELECTOR,
|
|
||||||
setup: alertsUtil,
|
|
||||||
};
|
|
||||||
|
|||||||
35
frontend/src/utils/alerts/alerts.md
Normal file
35
frontend/src/utils/alerts/alerts.md
Normal 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
|
||||||
|
```
|
||||||
@ -1,8 +1,21 @@
|
|||||||
import alerts from "./alerts";
|
import { Alerts } from "./alerts";
|
||||||
|
|
||||||
|
const MOCK_APP = {
|
||||||
|
httpClient: {
|
||||||
|
addResponseInterceptor: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('Alerts', () => {
|
describe('Alerts', () => {
|
||||||
|
|
||||||
it('should be called alerts', () => {
|
let alerts;
|
||||||
expect(alerts.name).toMatch('alerts');
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
alerts = new Alerts(element, MOCK_APP);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(alerts).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,89 +1,69 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './asidenav.scss';
|
import './asidenav.scss';
|
||||||
|
|
||||||
/**
|
const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
|
||||||
*
|
const FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';
|
||||||
* Asidenav Utility
|
const ASIDENAV_INITIALIZED_CLASS = 'asidenav--initialized';
|
||||||
* Correctly positions hovered asidenav submenus and handles the favorites button on mobile
|
const ASIDENAV_EXPANDED_CLASS = 'main__aside--expanded';
|
||||||
*
|
const ASIDENAV_LIST_ITEM_CLASS = 'asidenav__list-item';
|
||||||
* Attribute: uw-asidenav
|
const ASIDENAV_SUBMENU_CLASS = 'asidenav__nested-list-wrapper';
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ASIDENAV_UTIL_NAME = 'asidenav';
|
@Utility({
|
||||||
var ASIDENAV_UTIL_SELECTOR = '[uw-asidenav]';
|
selector: '[uw-asidenav]',
|
||||||
|
})
|
||||||
|
export class Asidenav {
|
||||||
|
|
||||||
var FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
|
_element;
|
||||||
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';
|
|
||||||
|
|
||||||
var asidenavUtil = function(element) {
|
constructor(element) {
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Asidenav utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initFavoritesButton();
|
this._initFavoritesButton();
|
||||||
initAsidenavSubmenus();
|
this._initAsidenavSubmenus();
|
||||||
|
|
||||||
// mark initialized
|
// mark initialized
|
||||||
element.classList.add(ASIDENAV_INITIALIZED_CLASS);
|
this._element.classList.add(ASIDENAV_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: ASIDENAV_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFavoritesButton() {
|
destroy() {
|
||||||
var favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
|
console.log('TBD: Destroy Asidenav');
|
||||||
|
}
|
||||||
|
|
||||||
|
_initFavoritesButton() {
|
||||||
|
const favoritesBtn = document.querySelector('.' + FAVORITES_BTN_CLASS);
|
||||||
favoritesBtn.addEventListener('click', function(event) {
|
favoritesBtn.addEventListener('click', function(event) {
|
||||||
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
|
favoritesBtn.classList.toggle(FAVORITES_BTN_ACTIVE_CLASS);
|
||||||
element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
this._element.classList.toggle(ASIDENAV_EXPANDED_CLASS);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAsidenavSubmenus() {
|
_initAsidenavSubmenus() {
|
||||||
var asidenavLinksWithSubmenus = Array.from(element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
|
const asidenavLinksWithSubmenus = Array.from(this._element.querySelectorAll('.' + ASIDENAV_LIST_ITEM_CLASS))
|
||||||
.map(function(listItem) {
|
.map(function(listItem) {
|
||||||
var submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
|
const submenu = listItem.querySelector('.' + ASIDENAV_SUBMENU_CLASS);
|
||||||
return { listItem, submenu };
|
return { listItem, submenu };
|
||||||
}).filter(function(union) {
|
}).filter(function(union) {
|
||||||
return union.submenu !== null;
|
return union.submenu !== null;
|
||||||
});
|
});
|
||||||
|
|
||||||
asidenavLinksWithSubmenus.forEach(function(union) {
|
asidenavLinksWithSubmenus.forEach(function(union) {
|
||||||
union.listItem.addEventListener('mouseover', createMouseoverHandler(union));
|
union.listItem.addEventListener('mouseover', this.createMouseoverHandler(union));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMouseoverHandler(union) {
|
_createMouseoverHandler(union) {
|
||||||
return function mouseoverHanlder() {
|
return function mouseoverHandler() {
|
||||||
var rectListItem = union.listItem.getBoundingClientRect();
|
const rectListItem = union.listItem.getBoundingClientRect();
|
||||||
var rectSubMenu = union.submenu.getBoundingClientRect();
|
const rectSubMenu = union.submenu.getBoundingClientRect();
|
||||||
|
|
||||||
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
|
union.submenu.style.left = (rectListItem.left + rectListItem.width) + 'px';
|
||||||
if (window.innerHeight - rectListItem.top < rectSubMenu.height) {
|
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,
|
|
||||||
};
|
|
||||||
|
|||||||
21
frontend/src/utils/asidenav/asidenav.md
Normal file
21
frontend/src/utils/asidenav/asidenav.md
Normal 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
|
||||||
|
...
|
||||||
|
```
|
||||||
@ -1,74 +1,60 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './async-form.scss';
|
import './async-form.scss';
|
||||||
|
|
||||||
/**
|
const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
|
||||||
*
|
const ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
|
||||||
* Async Form Utility
|
const ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
|
||||||
* prevents form submissions from reloading the page but instead firing an AJAX request
|
const ASYNC_FORM_MIN_DELAY = 600;
|
||||||
*
|
|
||||||
* 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."
|
|
||||||
*/
|
|
||||||
|
|
||||||
var ASYNC_FORM_UTIL_NAME = 'asyncForm';
|
const MODAL_SELECTOR = '.modal';
|
||||||
var ASYNC_FORM_UTIL_SELECTOR = 'form[uw-async-form]';
|
const MODAL_HEADER_KEY = 'Is-Modal';
|
||||||
|
const MODAL_HEADER_VALUE = 'True';
|
||||||
|
|
||||||
var ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
|
@Utility({
|
||||||
var ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';
|
selector: 'form[uw-async-form]',
|
||||||
var ASYNC_FORM_LOADING_CLASS = 'async-form--loading';
|
})
|
||||||
var ASYNC_FORM_MIN_DELAY = 600;
|
export class AsyncForm {
|
||||||
|
|
||||||
var MODAL_SELECTOR = '.modal';
|
_lastRequestTimestamp = 0;
|
||||||
var MODAL_HEADER_KEY = 'Is-Modal';
|
_element;
|
||||||
var MODAL_HEADER_VALUE = 'True';
|
_app;
|
||||||
|
|
||||||
var asyncFormUtil = function(element, app) {
|
constructor(element, app) {
|
||||||
|
|
||||||
var lastRequestTimestamp = 0;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Async Form Utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener('submit', submitHandler);
|
this._element.addEventListener('submit', this._submitHandler);
|
||||||
|
|
||||||
element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
|
this._element.classList.add(ASYNC_FORM_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: ASYNC_FORM_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processResponse(response) {
|
destroy() {
|
||||||
var responseElement = makeResponseElement(response.content, response.status);
|
// TODO
|
||||||
var parentElement = element.parentElement;
|
}
|
||||||
|
|
||||||
|
_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
|
// 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() {
|
setTimeout(() => {
|
||||||
parentElement.insertBefore(responseElement, element);
|
parentElement.insertBefore(responseElement, this._element);
|
||||||
element.remove();
|
this._element.remove();
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeResponseElement(content, status) {
|
_makeResponseElement(content, status) {
|
||||||
var responseElement = document.createElement('div');
|
const responseElement = document.createElement('div');
|
||||||
status = status || 'info';
|
status = status || 'info';
|
||||||
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
|
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
|
||||||
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS + '--' + status);
|
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS + '--' + status);
|
||||||
@ -76,42 +62,34 @@ var asyncFormUtil = function(element, app) {
|
|||||||
return responseElement;
|
return responseElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitHandler(event) {
|
_submitHandler = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
element.classList.add(ASYNC_FORM_LOADING_CLASS);
|
this._element.classList.add(ASYNC_FORM_LOADING_CLASS);
|
||||||
lastRequestTimestamp = Date.now();
|
this._lastRequestTimestamp = Date.now();
|
||||||
|
|
||||||
var url = element.getAttribute('action');
|
const url = this._element.getAttribute('action');
|
||||||
var headers = { };
|
const headers = { };
|
||||||
var body = new FormData(element);
|
const body = new FormData(this._element);
|
||||||
|
|
||||||
var isModal = element.closest(MODAL_SELECTOR);
|
const isModal = this._element.closest(MODAL_SELECTOR);
|
||||||
if (isModal) {
|
if (isModal) {
|
||||||
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
|
headers[MODAL_HEADER_KEY] = MODAL_HEADER_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.httpClient.post({
|
this._app.httpClient.post({
|
||||||
url: url,
|
url: url,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: body,
|
body: body,
|
||||||
}).then(function(response) {
|
}).then(
|
||||||
return response.json();
|
(response) => response.json()
|
||||||
}).then(function(response) {
|
).then(
|
||||||
processResponse(response[0]);
|
(response) => this._processResponse(response[0])
|
||||||
}).catch(function() {
|
).catch(() => {
|
||||||
var failureMessage = app.i18n.get('asyncFormFailure');
|
const failureMessage = this._app.i18n.get('asyncFormFailure');
|
||||||
processResponse({ content: failureMessage });
|
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,
|
|
||||||
};
|
|
||||||
|
|||||||
17
frontend/src/utils/async-form/async-form.md
Normal file
17
frontend/src/utils/async-form/async-form.md
Normal 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.").
|
||||||
@ -1,235 +1,227 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import { HttpClient } from '../../services/http-client/http-client';
|
import { HttpClient } from '../../services/http-client/http-client';
|
||||||
import './async-table.scss';
|
|
||||||
import './async-table-filter.scss';
|
import './async-table-filter.scss';
|
||||||
|
import './async-table.scss';
|
||||||
|
|
||||||
/**
|
const INPUT_DEBOUNCE = 600;
|
||||||
*
|
const HEADER_HEIGHT = 80;
|
||||||
* Async Table Utility
|
|
||||||
* makes table filters, sorting and pagination behave asynchronously via AJAX calls
|
|
||||||
*
|
|
||||||
* Attribute: uw-async-table
|
|
||||||
*
|
|
||||||
* Example usage:
|
|
||||||
* (regular table)
|
|
||||||
*/
|
|
||||||
|
|
||||||
var INPUT_DEBOUNCE = 600;
|
const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
|
||||||
var HEADER_HEIGHT = 80;
|
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';
|
const ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
|
||||||
var ASYNC_TABLE_UTIL_SELECTOR = '[uw-async-table]';
|
const ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
|
||||||
|
|
||||||
var ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
|
@Utility({
|
||||||
var ASYNC_TABLE_SCROLLTABLE_SELECTOR = '.scrolltable';
|
selector: '[uw-async-table]',
|
||||||
var ASYNC_TABLE_INITIALIZED_CLASS = 'async-table--initialized';
|
})
|
||||||
var ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
|
export class AsyncTable {
|
||||||
|
|
||||||
var ASYNC_TABLE_FILTER_FORM_SELECTOR = '.table-filter-form';
|
_element;
|
||||||
var ASYNC_TABLE_FILTER_FORM_ID_SELECTOR = '[name="form-identifier"]';
|
_app;
|
||||||
|
|
||||||
|
_asyncTableHeader;
|
||||||
|
_asyncTableId;
|
||||||
|
|
||||||
var asyncTableUtil = function(element, app) {
|
_ths = [];
|
||||||
var asyncTableHeader;
|
_pageLinks = [];
|
||||||
var asyncTableId;
|
_pagesizeForm;
|
||||||
|
_scrollTable;
|
||||||
|
_cssIdPrefix = '';
|
||||||
|
|
||||||
var ths = [];
|
_tableFilterInputs = {
|
||||||
var pageLinks = [];
|
|
||||||
var pagesizeForm;
|
|
||||||
var scrollTable;
|
|
||||||
var cssIdPrefix = '';
|
|
||||||
|
|
||||||
var tableFilterInputs = {
|
|
||||||
search: [],
|
search: [],
|
||||||
input: [],
|
input: [],
|
||||||
change: [],
|
change: [],
|
||||||
select: [],
|
select: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function init() {
|
constructor(element, app) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Async Table utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// param asyncTableDbHeader
|
// param asyncTableDbHeader
|
||||||
if (element.dataset.asyncTableDbHeader !== undefined) {
|
if (this._element.dataset.asyncTableDbHeader !== undefined) {
|
||||||
asyncTableHeader = element.dataset.asyncTableDbHeader;
|
this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawTableId = element.querySelector('table').id;
|
const rawTableId = this._element.querySelector('table').id;
|
||||||
cssIdPrefix = findCssIdPrefix(rawTableId);
|
this._cssIdPrefix = findCssIdPrefix(rawTableId);
|
||||||
asyncTableId = rawTableId.replace(cssIdPrefix, '');
|
this._asyncTableId = rawTableId.replace(this._cssIdPrefix, '');
|
||||||
|
|
||||||
// find scrolltable wrapper
|
// find scrolltable wrapper
|
||||||
scrollTable = element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
|
this._scrollTable = this._element.querySelector(ASYNC_TABLE_SCROLLTABLE_SELECTOR);
|
||||||
if (!scrollTable) {
|
if (!this._scrollTable) {
|
||||||
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
throw new Error('Async Table cannot be set up without a scrolltable element!');
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSortableHeaders();
|
this._setupSortableHeaders();
|
||||||
setupPagination();
|
this._setupPagination();
|
||||||
setupPageSizeSelect();
|
this._setupPageSizeSelect();
|
||||||
setupTableFilter();
|
this._setupTableFilter();
|
||||||
|
|
||||||
processLocalStorage();
|
this._processLocalStorage();
|
||||||
|
|
||||||
// clear currentTableUrl from previous requests
|
// clear currentTableUrl from previous requests
|
||||||
setLocalStorageParameter('currentTableUrl', null);
|
setLocalStorageParameter('currentTableUrl', null);
|
||||||
|
|
||||||
// mark initialized
|
// mark initialized
|
||||||
element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: ASYNC_TABLE_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSortableHeaders() {
|
destroy() {
|
||||||
ths = Array.from(scrollTable.querySelectorAll('th.sortable')).map(function(th) {
|
console.log('TBD: Destroy AsyncTable');
|
||||||
return { element: th };
|
}
|
||||||
});
|
|
||||||
|
|
||||||
ths.forEach(function(th) {
|
_setupSortableHeaders() {
|
||||||
th.clickHandler = function(event) {
|
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable'))
|
||||||
setLocalStorageParameter('horizPos', (scrollTable || {}).scrollLeft);
|
.map((th) => ({ element: th }));
|
||||||
linkClickHandler(event);
|
|
||||||
|
this._ths.forEach((th) => {
|
||||||
|
th.clickHandler = (event) => {
|
||||||
|
setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft);
|
||||||
|
this._linkClickHandler(event);
|
||||||
};
|
};
|
||||||
th.element.addEventListener('click', th.clickHandler);
|
th.element.addEventListener('click', th.clickHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPagination() {
|
_setupPagination() {
|
||||||
var pagination = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagination');
|
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
|
||||||
if (pagination) {
|
if (pagination) {
|
||||||
pageLinks = Array.from(pagination.querySelectorAll('.page-link')).map(function(link) {
|
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
|
||||||
return { element: link };
|
.map((link) => ({ element: link }));
|
||||||
});
|
|
||||||
|
|
||||||
pageLinks.forEach(function(link) {
|
this._pageLinks.forEach((link) => {
|
||||||
link.clickHandler = function(event) {
|
link.clickHandler = (event) => {
|
||||||
var tableBoundingRect = scrollTable.getBoundingClientRect();
|
const tableBoundingRect = this._scrollTable.getBoundingClientRect();
|
||||||
if (tableBoundingRect.top < HEADER_HEIGHT) {
|
if (tableBoundingRect.top < HEADER_HEIGHT) {
|
||||||
var scrollTo = {
|
const scrollTo = {
|
||||||
top: (scrollTable.offsetTop || 0) - HEADER_HEIGHT,
|
top: (this._scrollTable.offsetTop || 0) - HEADER_HEIGHT,
|
||||||
left: scrollTable.offsetLeft || 0,
|
left: this._scrollTable.offsetLeft || 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
};
|
};
|
||||||
setLocalStorageParameter('scrollTo', scrollTo);
|
setLocalStorageParameter('scrollTo', scrollTo);
|
||||||
}
|
}
|
||||||
linkClickHandler(event);
|
this._linkClickHandler(event);
|
||||||
};
|
};
|
||||||
link.element.addEventListener('click', link.clickHandler);
|
link.element.addEventListener('click', link.clickHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPageSizeSelect() {
|
_setupPageSizeSelect() {
|
||||||
// pagesize form
|
// pagesize form
|
||||||
pagesizeForm = element.querySelector('#' + cssIdPrefix + asyncTableId + '-pagesize-form');
|
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');
|
||||||
|
|
||||||
if (pagesizeForm) {
|
if (this._pagesizeForm) {
|
||||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
|
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
|
||||||
pagesizeSelect.addEventListener('change', changePagesizeHandler);
|
pagesizeSelect.addEventListener('change', this._changePagesizeHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTableFilter() {
|
_setupTableFilter() {
|
||||||
var tableFilterForm = element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
|
||||||
|
|
||||||
if (tableFilterForm) {
|
if (tableFilterForm) {
|
||||||
gatherTableFilterInputs(tableFilterForm);
|
this._gatherTableFilterInputs(tableFilterForm);
|
||||||
addTableFilterEventListeners(tableFilterForm);
|
this._addTableFilterEventListeners(tableFilterForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatherTableFilterInputs(tableFilterForm) {
|
_gatherTableFilterInputs(tableFilterForm) {
|
||||||
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach(function(input) {
|
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) {
|
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) {
|
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) {
|
Array.from(tableFilterForm.querySelectorAll('select')).forEach(function(input) {
|
||||||
tableFilterInputs.select.push(input);
|
this._tableFilterInputs.select.push(input);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTableFilterEventListeners(tableFilterForm) {
|
_addTableFilterEventListeners(tableFilterForm) {
|
||||||
tableFilterInputs.search.forEach(function(input) {
|
this._tableFilterInputs.search.forEach(function(input) {
|
||||||
var debouncedInput = debounce(function() {
|
const debouncedInput = debounce(function() {
|
||||||
if (input.value.length === 0 || input.value.length > 2) {
|
if (input.value.length === 0 || input.value.length > 2) {
|
||||||
updateFromTableFilter(tableFilterForm);
|
this.updateFromTableFilter(tableFilterForm);
|
||||||
}
|
}
|
||||||
}, INPUT_DEBOUNCE);
|
}, INPUT_DEBOUNCE);
|
||||||
input.addEventListener('input', debouncedInput);
|
input.addEventListener('input', debouncedInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterInputs.input.forEach(function(input) {
|
this._tableFilterInputs.input.forEach(function(input) {
|
||||||
var debouncedInput = debounce(function() {
|
const debouncedInput = debounce(function() {
|
||||||
if (input.value.length === 0 || input.value.length > 2) {
|
if (input.value.length === 0 || input.value.length > 2) {
|
||||||
updateFromTableFilter(tableFilterForm);
|
this.updateFromTableFilter(tableFilterForm);
|
||||||
}
|
}
|
||||||
}, INPUT_DEBOUNCE);
|
}, INPUT_DEBOUNCE);
|
||||||
input.addEventListener('input', debouncedInput);
|
input.addEventListener('input', debouncedInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterInputs.change.forEach(function(input) {
|
this._tableFilterInputs.change.forEach(function(input) {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', function() {
|
||||||
updateFromTableFilter(tableFilterForm);
|
this.updateFromTableFilter(tableFilterForm);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterInputs.select.forEach(function(input) {
|
this._tableFilterInputs.select.forEach(function(input) {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', function() {
|
||||||
updateFromTableFilter(tableFilterForm);
|
this.updateFromTableFilter(tableFilterForm);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterForm.addEventListener('submit', function(event) {
|
tableFilterForm.addEventListener('submit', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateFromTableFilter(tableFilterForm);
|
this.updateFromTableFilter(tableFilterForm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromTableFilter(tableFilterForm) {
|
_updateFromTableFilter(tableFilterForm) {
|
||||||
var url = serializeTableFilterToURL();
|
const url = this.serializeTableFilterToURL();
|
||||||
var callback = null;
|
let callback = null;
|
||||||
|
|
||||||
var focusedInput = tableFilterForm.querySelector(':focus, :active');
|
const focusedInput = tableFilterForm.querySelector(':focus, :active');
|
||||||
// focus previously focused input
|
// focus previously focused input
|
||||||
if (focusedInput && focusedInput.selectionStart !== null) {
|
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
|
// remove the following part of the id to get rid of the random
|
||||||
// (yet somewhat structured) prefix we got from nudging.
|
// (yet somewhat structured) prefix we got from nudging.
|
||||||
var prefix = findCssIdPrefix(focusedInput.id);
|
const prefix = findCssIdPrefix(focusedInput.id);
|
||||||
var focusId = focusedInput.id.replace(prefix, '');
|
const focusId = focusedInput.id.replace(prefix, '');
|
||||||
callback = function(wrapper) {
|
callback = function(wrapper) {
|
||||||
var idPrefix = getLocalStorageParameter('cssIdPrefix');
|
const idPrefix = getLocalStorageParameter('cssIdPrefix');
|
||||||
var toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
|
const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
|
||||||
if (toBeFocused) {
|
if (toBeFocused) {
|
||||||
toBeFocused.focus();
|
toBeFocused.focus();
|
||||||
toBeFocused.selectionStart = selectionStart;
|
toBeFocused.selectionStart = selectionStart;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
updateTableFrom(url, callback);
|
this._updateTableFrom(url, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeTableFilterToURL() {
|
serializeTableFilterToURL() {
|
||||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
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) {
|
if (!formIdElement) {
|
||||||
// cannot serialize the filter form without an identifier
|
// cannot serialize the filter form without an identifier
|
||||||
return;
|
return;
|
||||||
@ -237,25 +229,25 @@ var asyncTableUtil = function(element, app) {
|
|||||||
|
|
||||||
url.searchParams.set('form-identifier', formIdElement.value);
|
url.searchParams.set('form-identifier', formIdElement.value);
|
||||||
url.searchParams.set('_hasdata', 'true');
|
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);
|
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);
|
url.searchParams.set(input.name, input.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterInputs.change.forEach(function(input) {
|
this._tableFilterInputs.change.forEach(function(input) {
|
||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
url.searchParams.set(input.name, input.value);
|
url.searchParams.set(input.name, input.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tableFilterInputs.select.forEach(function(select) {
|
this._tableFilterInputs.select.forEach(function(select) {
|
||||||
var options = Array.from(select.querySelectorAll('option'));
|
const options = Array.from(select.querySelectorAll('option'));
|
||||||
var selected = options.find(function(option) { return option.selected; });
|
const selected = options.find((option) => option.selected);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
url.searchParams.set(select.name, selected.value);
|
url.searchParams.set(select.name, selected.value);
|
||||||
}
|
}
|
||||||
@ -264,119 +256,114 @@ var asyncTableUtil = function(element, app) {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processLocalStorage() {
|
_processLocalStorage() {
|
||||||
var scrollTo = getLocalStorageParameter('scrollTo');
|
const scrollTo = getLocalStorageParameter('scrollTo');
|
||||||
if (scrollTo && scrollTable) {
|
if (scrollTo && this._scrollTable) {
|
||||||
window.scrollTo(scrollTo);
|
window.scrollTo(scrollTo);
|
||||||
}
|
}
|
||||||
setLocalStorageParameter('scrollTo', null);
|
setLocalStorageParameter('scrollTo', null);
|
||||||
|
|
||||||
var horizPos = getLocalStorageParameter('horizPos');
|
const horizPos = getLocalStorageParameter('horizPos');
|
||||||
if (horizPos && scrollTable) {
|
if (horizPos && this._scrollTable) {
|
||||||
scrollTable.scrollLeft = horizPos;
|
this._scrollTable.scrollLeft = horizPos;
|
||||||
}
|
}
|
||||||
setLocalStorageParameter('horizPos', null);
|
setLocalStorageParameter('horizPos', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeListeners() {
|
_removeListeners() {
|
||||||
ths.forEach(function(th) {
|
this._ths.forEach(function(th) {
|
||||||
th.element.removeEventListener('click', th.clickHandler);
|
th.element.removeEventListener('click', th.clickHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
pageLinks.forEach(function(link) {
|
this._pageLinks.forEach(function(link) {
|
||||||
link.element.removeEventListener('click', link.clickHandler);
|
link.element.removeEventListener('click', link.clickHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pagesizeForm) {
|
if (this._pagesizeForm) {
|
||||||
var pagesizeSelect = pagesizeForm.querySelector('[name=' + asyncTableId + '-pagesize]');
|
const pagesizeSelect = this._pagesizeForm.querySelector('[name=' + this._asyncTableId + '-pagesize]');
|
||||||
pagesizeSelect.removeEventListener('change', changePagesizeHandler);
|
pagesizeSelect.removeEventListener('change', this._changePagesizeHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkClickHandler(event) {
|
_linkClickHandler = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var url = getClickDestination(event.target);
|
let url = this._getClickDestination(event.target);
|
||||||
if (!url.match(/^http/)) {
|
if (!url.match(/^http/)) {
|
||||||
url = window.location.origin + window.location.pathname + url;
|
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')) {
|
if (!el.matches('a') && !el.querySelector('a')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePagesizeHandler(event) {
|
_changePagesizeHandler = (event) => {
|
||||||
var paginationParamKey = asyncTableId + '-pagination';
|
const paginationParamKey = this._asyncTableId + '-pagination';
|
||||||
var pagesizeParamKey = asyncTableId + '-pagesize';
|
const pagesizeParamKey = this._asyncTableId + '-pagesize';
|
||||||
var pageParamKey = asyncTableId + '-page';
|
const pageParamKey = this._asyncTableId + '-page';
|
||||||
|
|
||||||
var paginationParamEl = pagesizeForm.querySelector('[name="' + paginationParamKey + '"]');
|
const paginationParamEl = this._pagesizeForm.querySelector('[name="' + paginationParamKey + '"]');
|
||||||
var url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
|
||||||
url.searchParams.set(pagesizeParamKey, event.target.value);
|
url.searchParams.set(pagesizeParamKey, event.target.value);
|
||||||
url.searchParams.set(pageParamKey, 0);
|
url.searchParams.set(pageParamKey, 0);
|
||||||
|
|
||||||
if (paginationParamEl) {
|
if (paginationParamEl) {
|
||||||
var encodedValue = encodeURIComponent(paginationParamEl.value);
|
const encodedValue = encodeURIComponent(paginationParamEl.value);
|
||||||
url.searchParams.set(paginationParamKey, encodedValue);
|
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
|
// fetches new sorted element from url with params and replaces contents of current element
|
||||||
function updateTableFrom(url, callback) {
|
_updateTableFrom(url, callback) {
|
||||||
element.classList.add(ASYNC_TABLE_LOADING_CLASS);
|
this._element.classList.add(ASYNC_TABLE_LOADING_CLASS);
|
||||||
|
|
||||||
var headers = {
|
const headers = {
|
||||||
'Accept': HttpClient.ACCEPT.TEXT_HTML,
|
'Accept': HttpClient.ACCEPT.TEXT_HTML,
|
||||||
[asyncTableHeader]: asyncTableId,
|
[this._asyncTableHeader]: this._asyncTableId,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.httpClient.get({
|
this._app.httpClient.get({
|
||||||
url: url,
|
url: url,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
}).then(function(response) {
|
}).then(
|
||||||
return app.htmlHelpers.parseResponse(response);
|
(response) => this._app.htmlHelpers.parseResponse(response)
|
||||||
}).then(function(response) {
|
).then((response) => {
|
||||||
setLocalStorageParameter('currentTableUrl', url.href);
|
setLocalStorageParameter('currentTableUrl', url.href);
|
||||||
// reset table
|
// reset table
|
||||||
removeListeners();
|
this._removeListeners();
|
||||||
element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
|
||||||
// update table with new
|
// update table with new
|
||||||
updateWrapperContents(response);
|
this._updateWrapperContents(response);
|
||||||
|
|
||||||
app.utilRegistry.setupAll(element);
|
this._app.utilRegistry.setupAll(this._element);
|
||||||
|
|
||||||
if (callback && typeof callback === 'function') {
|
if (callback && typeof callback === 'function') {
|
||||||
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
|
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
|
||||||
callback(element);
|
callback(this._element);
|
||||||
setLocalStorageParameter('cssIdPrefix', '');
|
setLocalStorageParameter('cssIdPrefix', '');
|
||||||
}
|
}
|
||||||
}).catch(function(err) {
|
}).catch((err) => console.error(err)
|
||||||
console.error(err);
|
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
|
||||||
}).finally(function() {
|
|
||||||
element.classList.remove(ASYNC_TABLE_LOADING_CLASS);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWrapperContents(response) {
|
_updateWrapperContents(response) {
|
||||||
var newPage = document.createElement('div');
|
const newPage = document.createElement('div');
|
||||||
newPage.appendChild(response.element);
|
newPage.appendChild(response.element);
|
||||||
var newWrapperContents = newPage.querySelector('#' + response.idPrefix + element.id);
|
const newWrapperContents = newPage.querySelector('#' + response.idPrefix + this._element.id);
|
||||||
element.innerHTML = newWrapperContents.innerHTML;
|
this._element.innerHTML = newWrapperContents.innerHTML;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return init();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// returns any random nudged prefix found in the given id
|
// returns any random nudged prefix found in the given id
|
||||||
function findCssIdPrefix(id) {
|
function findCssIdPrefix(id) {
|
||||||
var matcher = /r\d*?__/;
|
const matcher = /r\d*?__/;
|
||||||
var maybePrefix = id.match(matcher);
|
const maybePrefix = id.match(matcher);
|
||||||
if (maybePrefix && maybePrefix[0]) {
|
if (maybePrefix && maybePrefix[0]) {
|
||||||
return maybePrefix[0];
|
return maybePrefix[0];
|
||||||
}
|
}
|
||||||
@ -384,7 +371,7 @@ function findCssIdPrefix(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLocalStorageParameter(key, value) {
|
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) {
|
if (value !== null) {
|
||||||
currentLSState[key] = value;
|
currentLSState[key] = value;
|
||||||
} else {
|
} else {
|
||||||
@ -392,30 +379,23 @@ function setLocalStorageParameter(key, value) {
|
|||||||
}
|
}
|
||||||
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
|
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocalStorageParameter(key) {
|
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];
|
return currentLSState[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// debounce function, taken from Underscore.js
|
// debounce function, taken from Underscore.js
|
||||||
function debounce(func, wait, immediate) {
|
function debounce(func, wait, immediate) {
|
||||||
var timeout;
|
let timeout;
|
||||||
return function() {
|
return function() {
|
||||||
var context = this, args = arguments;
|
const context = this, args = arguments;
|
||||||
var later = function() {
|
const later = function() {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
if (!immediate) func.apply(context, args);
|
if (!immediate) func.apply(context, args);
|
||||||
};
|
};
|
||||||
var callNow = immediate && !timeout;
|
const callNow = immediate && !timeout;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = setTimeout(later, wait);
|
||||||
if (callNow) func.apply(context, args);
|
if (callNow) func.apply(context, args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
name: ASYNC_TABLE_UTIL_NAME,
|
|
||||||
selector: ASYNC_TABLE_UTIL_SELECTOR,
|
|
||||||
setup: asyncTableUtil,
|
|
||||||
};
|
|
||||||
|
|||||||
7
frontend/src/utils/async-table/async-table.md
Normal file
7
frontend/src/utils/async-table/async-table.md
Normal 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)
|
||||||
@ -1,58 +1,53 @@
|
|||||||
/**
|
import { Utility } from "../../core/utility";
|
||||||
*
|
|
||||||
* 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)
|
|
||||||
*/
|
|
||||||
|
|
||||||
var CHECK_ALL_UTIL_NAME = 'checkAll';
|
const CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||||
var CHECK_ALL_UTIL_SELECTOR = 'table';
|
|
||||||
|
|
||||||
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) {
|
_element;
|
||||||
var columns = [];
|
_app;
|
||||||
var checkboxColumn = [];
|
|
||||||
var checkAllCheckbox = null;
|
|
||||||
|
|
||||||
function init() {
|
_columns = [];
|
||||||
|
_checkboxColumn = [];
|
||||||
|
_checkAllCheckbox = null;
|
||||||
|
|
||||||
|
constructor(element, app) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Check All utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
gatherColumns();
|
this._gatherColumns();
|
||||||
setupCheckAllCheckbox();
|
this._setupCheckAllCheckbox();
|
||||||
|
|
||||||
// mark initialized
|
// mark initialized
|
||||||
element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
|
this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: CHECK_ALL_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCheckboxId() {
|
destroy() {
|
||||||
|
console.log('TBD: Destroy CheckAll');
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCheckboxId() {
|
||||||
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatherColumns() {
|
_gatherColumns() {
|
||||||
var rows = Array.from(element.querySelectorAll('tr'));
|
const rows = Array.from(this._element.querySelectorAll('tr'));
|
||||||
var cols = [];
|
const cols = [];
|
||||||
rows.forEach(function(tr) {
|
rows.forEach(function(tr) {
|
||||||
var cells = Array.from(tr.querySelectorAll('td'));
|
const cells = Array.from(tr.querySelectorAll('td'));
|
||||||
cells.forEach(function(cell, cellIndex) {
|
cells.forEach(function(cell, cellIndex) {
|
||||||
if (!cols[cellIndex]) {
|
if (!cols[cellIndex]) {
|
||||||
cols[cellIndex] = [];
|
cols[cellIndex] = [];
|
||||||
@ -60,21 +55,21 @@ var checkAllUtil = function(element, app) {
|
|||||||
cols[cellIndex].push(cell);
|
cols[cellIndex].push(cell);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
columns = cols;
|
this._columns = cols;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCheckboxColumn(columns) {
|
_findCheckboxColumn(columns) {
|
||||||
var checkboxColumnId = null;
|
let checkboxColumnId = null;
|
||||||
columns.forEach(function(col, i) {
|
columns.forEach(function(col, i) {
|
||||||
if (isCheckboxColumn(col)) {
|
if (this.isCheckboxColumn(col)) {
|
||||||
checkboxColumnId = i;
|
checkboxColumnId = i;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return checkboxColumnId;
|
return checkboxColumnId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCheckboxColumn(col) {
|
_isCheckboxColumn(col) {
|
||||||
var onlyCheckboxes = true;
|
let onlyCheckboxes = true;
|
||||||
col.forEach(function(cell) {
|
col.forEach(function(cell) {
|
||||||
if (onlyCheckboxes && !cell.querySelector(CHECKBOX_SELECTOR)) {
|
if (onlyCheckboxes && !cell.querySelector(CHECKBOX_SELECTOR)) {
|
||||||
onlyCheckboxes = false;
|
onlyCheckboxes = false;
|
||||||
@ -83,59 +78,50 @@ var checkAllUtil = function(element, app) {
|
|||||||
return onlyCheckboxes;
|
return onlyCheckboxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCheckAllCheckbox() {
|
_setupCheckAllCheckbox() {
|
||||||
var checkboxColumnId = findCheckboxColumn(columns);
|
const checkboxColumnId = this._findCheckboxColumn(this._columns);
|
||||||
if (checkboxColumnId === null) {
|
if (checkboxColumnId === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkboxColumn = columns[checkboxColumnId];
|
this._checkboxColumn = this._columns[checkboxColumnId];
|
||||||
var firstRow = element.querySelector('tr');
|
const firstRow = this._element.querySelector('tr');
|
||||||
var th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
|
const th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
|
||||||
checkAllCheckbox = document.createElement('input');
|
this._checkAllCheckbox = document.createElement('input');
|
||||||
checkAllCheckbox.setAttribute('type', 'checkbox');
|
this._checkAllCheckbox.setAttribute('type', 'checkbox');
|
||||||
checkAllCheckbox.setAttribute('id', getCheckboxId());
|
this._checkAllCheckbox.setAttribute('id', this._getCheckboxId());
|
||||||
th.insertBefore(checkAllCheckbox, th.firstChild);
|
th.insertBefore(this._checkAllCheckbox, th.firstChild);
|
||||||
|
|
||||||
// manually set up new checkbox
|
// 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);
|
this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput);
|
||||||
setupCheckboxListeners();
|
this._setupCheckboxListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCheckAllCheckboxInput() {
|
_onCheckAllCheckboxInput() {
|
||||||
toggleAll(checkAllCheckbox.checked);
|
this._toggleAll(this._checkAllCheckbox.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCheckboxListeners() {
|
_setupCheckboxListeners() {
|
||||||
checkboxColumn
|
this._checkboxColumn.map(function(cell) {
|
||||||
.map(function(cell) {
|
|
||||||
return cell.querySelector(CHECKBOX_SELECTOR);
|
return cell.querySelector(CHECKBOX_SELECTOR);
|
||||||
})
|
})
|
||||||
.forEach(function(checkbox) {
|
.forEach(function(checkbox) {
|
||||||
checkbox.addEventListener('input', updateCheckAllCheckboxState);
|
checkbox.addEventListener('input', this.updateCheckAllCheckboxState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCheckAllCheckboxState() {
|
_updateCheckAllCheckboxState() {
|
||||||
var allChecked = checkboxColumn.every(function(cell) {
|
const allChecked = this._checkboxColumn.every(function(cell) {
|
||||||
return cell.querySelector(CHECKBOX_SELECTOR).checked;
|
return cell.querySelector(CHECKBOX_SELECTOR).checked;
|
||||||
});
|
});
|
||||||
checkAllCheckbox.checked = allChecked;
|
this._checkAllCheckbox.checked = allChecked;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAll(checked) {
|
_toggleAll(checked) {
|
||||||
checkboxColumn.forEach(function(cell) {
|
this._checkboxColumn.forEach(function(cell) {
|
||||||
cell.querySelector(CHECKBOX_SELECTOR).checked = checked;
|
cell.querySelector(CHECKBOX_SELECTOR).checked = checked;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return init();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: CHECK_ALL_UTIL_NAME,
|
|
||||||
selector: CHECK_ALL_UTIL_SELECTOR,
|
|
||||||
setup: checkAllUtil,
|
|
||||||
};
|
|
||||||
|
|||||||
9
frontend/src/utils/check-all/check-all.md
Normal file
9
frontend/src/utils/check-all/check-all.md
Normal 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)
|
||||||
|
|
||||||
29
frontend/src/utils/form/auto-submit-button.js
Normal file
29
frontend/src/utils/form/auto-submit-button.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/utils/form/auto-submit-button.md
Normal file
11
frontend/src/utils/form/auto-submit-button.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
47
frontend/src/utils/form/auto-submit-input.js
Normal file
47
frontend/src/utils/form/auto-submit-input.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
frontend/src/utils/form/auto-submit-input.md
Normal file
9
frontend/src/utils/form/auto-submit-input.md
Normal 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 />
|
||||||
|
```
|
||||||
62
frontend/src/utils/form/datepicker.js
Normal file
62
frontend/src/utils/form/datepicker.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/src/utils/form/datepicker.md
Normal file
8
frontend/src/utils/form/datepicker.md
Normal 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)
|
||||||
46
frontend/src/utils/form/form-error-remover.js
Normal file
46
frontend/src/utils/form/form-error-remover.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/src/utils/form/form-error-remover.md
Normal file
8
frontend/src/utils/form/form-error-remover.md
Normal 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)
|
||||||
@ -1,600 +1,17 @@
|
|||||||
import flatpickr from 'flatpickr';
|
|
||||||
import './form.scss';
|
import './form.scss';
|
||||||
|
import { AutoSubmitButton } from './auto-submit-button';
|
||||||
/**
|
import { AutoSubmitInput } from './auto-submit-input';
|
||||||
*
|
import { Datepicker } from './datepicker';
|
||||||
* Reactive Submit Button Utility
|
import { FormErrorRemover } from './form-error-remover';
|
||||||
* disables a forms LAST sumit button as long as the required inputs are invalid
|
import { InteractiveFieldset } from './interactive-fieldset';
|
||||||
* (only checks if the value of the inputs are not empty)
|
import { NavigateAwayPrompt } from './navigate-away-prompt';
|
||||||
*
|
|
||||||
* Attribute: [none]
|
export const FormUtils = [
|
||||||
* (automatically setup on all form tags)
|
AutoSubmitButton,
|
||||||
*
|
AutoSubmitInput,
|
||||||
* Params:
|
Datepicker,
|
||||||
* data-formnorequired: string
|
FormErrorRemover,
|
||||||
* If present the submit button will never get disabled
|
InteractiveFieldset,
|
||||||
*
|
NavigateAwayPrompt,
|
||||||
* Example usage:
|
// ReactiveSubmitButton // not used currently
|
||||||
* <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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
97
frontend/src/utils/form/interactive-fieldset.js
Normal file
97
frontend/src/utils/form/interactive-fieldset.js
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
35
frontend/src/utils/form/interactive-fieldset.md
Normal file
35
frontend/src/utils/form/interactive-fieldset.md
Normal 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>
|
||||||
|
```
|
||||||
64
frontend/src/utils/form/navigate-away-prompt.js
Normal file
64
frontend/src/utils/form/navigate-away-prompt.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/utils/form/navigate-away-prompt.md
Normal file
11
frontend/src/utils/form/navigate-away-prompt.md
Normal 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)
|
||||||
85
frontend/src/utils/form/reactive-submit-button.js
Normal file
85
frontend/src/utils/form/reactive-submit-button.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/src/utils/form/reactive-submit-button.md
Normal file
17
frontend/src/utils/form/reactive-submit-button.md
Normal 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">
|
||||||
|
```
|
||||||
43
frontend/src/utils/inputs/checkbox.js
Normal file
43
frontend/src/utils/inputs/checkbox.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/utils/inputs/checkbox.md
Normal file
10
frontend/src/utils/inputs/checkbox.md
Normal 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">
|
||||||
|
```
|
||||||
99
frontend/src/utils/inputs/file-input.js
Normal file
99
frontend/src/utils/inputs/file-input.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
frontend/src/utils/inputs/file-input.md
Normal file
23
frontend/src/utils/inputs/file-input.md
Normal 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"
|
||||||
|
|
||||||
@ -1,194 +1,10 @@
|
|||||||
import './checkbox.scss';
|
import { Checkbox } from './checkbox';
|
||||||
import './radio.scss';
|
import { FileInput } from './file-input';
|
||||||
|
|
||||||
import './inputs.scss';
|
import './inputs.scss';
|
||||||
|
import './radio.scss';
|
||||||
|
|
||||||
/**
|
export const InputUtils = [
|
||||||
*
|
Checkbox,
|
||||||
* FileInput Utility
|
FileInput,
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,176 +1,163 @@
|
|||||||
/**
|
import { Utility } from "../../core/utility";
|
||||||
*
|
|
||||||
* 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">
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MASS_INPUT_UTIL_NAME = 'massInput';
|
const MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
|
||||||
var MASS_INPUT_UTIL_SELECTOR = '[uw-mass-input]';
|
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';
|
@Utility({
|
||||||
var MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add';
|
selector: '[uw-mass-input]',
|
||||||
var MASS_INPUT_SUBMIT_BUTTON_CLASS = 'massinput__submit-button';
|
})
|
||||||
var MASS_INPUT_INITIALIZED_CLASS = 'mass-input--initialized';
|
export class MassInput {
|
||||||
|
|
||||||
var massInputUtil = function(element, app) {
|
_element;
|
||||||
var massInputId;
|
_app;
|
||||||
var massInputFormSubmitHandler;
|
|
||||||
var massInputForm;
|
|
||||||
|
|
||||||
function init() {
|
_massInputId;
|
||||||
|
_massInputFormSubmitHandler;
|
||||||
|
_massInputForm;
|
||||||
|
|
||||||
|
constructor(element, app) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Mass Input utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
massInputId = element.dataset.massInputIdent || '_';
|
this._massInputId = this._element.dataset.massInputIdent || '_';
|
||||||
massInputForm = element.closest('form');
|
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>!');
|
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
|
// setup submit buttons inside this massinput so browser
|
||||||
// uses correct submit button for form submission.
|
// uses correct submit button for form submission.
|
||||||
var buttons = getMassInputSubmitButtons();
|
const buttons = this._getMassInputSubmitButtons();
|
||||||
buttons.forEach(function(button) {
|
buttons.forEach(function(button) {
|
||||||
setupSubmitButton(button);
|
this.setupSubmitButton(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
massInputForm.addEventListener('submit', massInputFormSubmitHandler);
|
this._massInputForm.addEventListener('submit', this._massInputFormSubmitHandler);
|
||||||
massInputForm.addEventListener('keypress', keypressHandler);
|
this._massInputForm.addEventListener('keypress', this._keypressHandler);
|
||||||
|
|
||||||
// mark initialized
|
// mark initialized
|
||||||
element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
|
this._element.classList.add(MASS_INPUT_INITIALIZED_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: MASS_INPUT_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {
|
|
||||||
reset();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSubmitHandler() {
|
destroy() {
|
||||||
var method = massInputForm.getAttribute('method') || 'POST';
|
this._reset();
|
||||||
var url = massInputForm.getAttribute('action') || window.location.href;
|
}
|
||||||
var enctype = massInputForm.getAttribute('enctype') || 'application/json';
|
|
||||||
|
|
||||||
var requestFn;
|
_makeSubmitHandler() {
|
||||||
if (app.httpClient[method.toLowerCase()]) {
|
const method = this._massInputForm.getAttribute('method') || 'POST';
|
||||||
requestFn = app.httpClient[method.toLowerCase()].bind(app.httpClient);
|
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) {
|
return function(event) {
|
||||||
var activeElement;
|
let activeElement;
|
||||||
|
|
||||||
// check if event occured from either a mass input add/delete button or
|
// 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)
|
// 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) {
|
if (!activeElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the according massinput cell thats hosts the element that triggered the submit
|
// 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) {
|
if (!massInputCell) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
const submitButton = massInputCell.querySelector('.' + MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||||
if (!submitButton) {
|
if (!submitButton) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR);
|
const isAddCell = massInputCell.matches(MASS_INPUT_ADD_CELL_SELECTOR);
|
||||||
var submitButtonIsActive = submitButton.matches(':focus, :active');
|
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 the cell is not an add cell the active element must at least be the cells submit button
|
||||||
if (!isAddCell && !submitButtonIsActive) {
|
if (!isAddCell && !submitButtonIsActive) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var requestBody = serializeForm(submitButton, enctype);
|
const requestBody = this.serializeForm(submitButton, enctype);
|
||||||
|
|
||||||
if (requestFn && requestBody) {
|
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;
|
headers['Content-Type'] = enctype;
|
||||||
|
}
|
||||||
|
|
||||||
requestFn({
|
requestFn({
|
||||||
url: url,
|
url: url,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
}).then(function(response) {
|
}).then(function(response) {
|
||||||
return app.htmlHelpers.parseResponse(response);
|
return this._app.htmlHelpers.parseResponse(response);
|
||||||
}).then(function(response) {
|
}).then(function(response) {
|
||||||
processResponse(response.element);
|
this.processResponse(response.element);
|
||||||
if (isAddCell) {
|
if (isAddCell) {
|
||||||
reFocusAddCell();
|
this.reFocusAddCell();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function keypressHandler(event) {
|
_keypressHandler(event) {
|
||||||
if (event.keyCode !== 13) {
|
if (event.keyCode !== 13) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (massInputFormSubmitHandler) {
|
if (this._massInputFormSubmitHandler) {
|
||||||
return massInputFormSubmitHandler(event);
|
return this._massInputFormSubmitHandler(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMassInputSubmitButtons() {
|
_getMassInputSubmitButtons() {
|
||||||
return Array.from(element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));
|
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.setAttribute('type', 'button');
|
||||||
button.classList.add(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
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.setAttribute('type', 'submit');
|
||||||
button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
button.classList.remove(MASS_INPUT_SUBMIT_BUTTON_CLASS);
|
||||||
button.removeEventListener('click', massInputFormSubmitHandler);
|
button.removeEventListener('click', this._massInputFormSubmitHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processResponse(responseElement) {
|
_processResponse(responseElement) {
|
||||||
element.innerHTML = "";
|
this._element.innerHTML = "";
|
||||||
element.appendChild(responseElement);
|
this._element.appendChild(responseElement);
|
||||||
|
|
||||||
reset();
|
this._reset();
|
||||||
|
|
||||||
app.utilRegistry.setupAll(element);
|
this._app.utilRegistry.setupAll(this._element);
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeForm(submitButton, enctype) {
|
_serializeForm(submitButton, enctype) {
|
||||||
var formData = new FormData(massInputForm);
|
const formData = new FormData(this._massInputForm);
|
||||||
|
|
||||||
// manually add name and value of submit button to formData
|
// manually add name and value of submit button to formData
|
||||||
formData.append(submitButton.name, submitButton.value);
|
formData.append(submitButton.name, submitButton.value);
|
||||||
@ -184,35 +171,27 @@ var massInputUtil = function(element, app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reFocusAddCell() {
|
_reFocusAddCell() {
|
||||||
var addCell = element.querySelector(MASS_INPUT_ADD_CELL_SELECTOR);
|
const addCell = this._element.querySelector(MASS_INPUT_ADD_CELL_SELECTOR);
|
||||||
if (!addCell) {
|
if (!addCell) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var addCellInput = addCell.querySelector('input:not([type="hidden"])');
|
const addCellInput = addCell.querySelector('input:not([type="hidden"])');
|
||||||
if (addCellInput) {
|
if (addCellInput) {
|
||||||
// Clearing of add-inputs is done in the backend
|
// Clearing of add-inputs is done in the backend
|
||||||
addCellInput.focus();
|
addCellInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
_reset() {
|
||||||
element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
this._element.classList.remove(MASS_INPUT_INITIALIZED_CLASS);
|
||||||
massInputForm.removeEventListener('submit', massInputFormSubmitHandler);
|
this._massInputForm.removeEventListener('submit', this._massInputFormSubmitHandler);
|
||||||
massInputForm.removeEventListener('keypress', keypressHandler);
|
this._massInputForm.removeEventListener('keypress', this._keypressHandler);
|
||||||
|
|
||||||
var buttons = getMassInputSubmitButtons();
|
const buttons = this._getMassInputSubmitButtons();
|
||||||
buttons.forEach(function(button) {
|
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,
|
|
||||||
};
|
|
||||||
|
|||||||
17
frontend/src/utils/mass-input/mass-input.md
Normal file
17
frontend/src/utils/mass-input/mass-input.md
Normal 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">
|
||||||
|
```
|
||||||
@ -1,203 +1,182 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './modal.scss';
|
import './modal.scss';
|
||||||
|
|
||||||
/**
|
const MODAL_HEADERS = {
|
||||||
*
|
|
||||||
* 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 = {
|
|
||||||
'Is-Modal': 'True',
|
'Is-Modal': 'True',
|
||||||
};
|
};
|
||||||
|
|
||||||
var MODAL_INITIALIZED_CLASS = 'modal--initialized';
|
const MODAL_INITIALIZED_CLASS = 'modal--initialized';
|
||||||
var MODAL_CLASS = 'modal';
|
const MODAL_CLASS = 'modal';
|
||||||
var MODAL_OPEN_CLASS = 'modal--open';
|
const MODAL_OPEN_CLASS = 'modal--open';
|
||||||
var MODAL_TRIGGER_CLASS = 'modal__trigger';
|
const MODAL_TRIGGER_CLASS = 'modal__trigger';
|
||||||
var MODAL_CONTENT_CLASS = 'modal__content';
|
const MODAL_CONTENT_CLASS = 'modal__content';
|
||||||
var MODAL_OVERLAY_CLASS = 'modal__overlay';
|
const MODAL_OVERLAY_CLASS = 'modal__overlay';
|
||||||
var MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
|
const MODAL_OVERLAY_OPEN_CLASS = 'modal__overlay--open';
|
||||||
var MODAL_CLOSER_CLASS = 'modal__closer';
|
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
|
// one singleton wrapper to keep all the modals to avoid CSS bug
|
||||||
// with blurry text due to `transform: translate(-50%, -50%)`
|
// with blurry text due to `transform: translate(-50%, -50%)`
|
||||||
// will be created (and reused) for the first modal that gets initialized
|
// will be created (and reused) for the first modal that gets initialized
|
||||||
var MODALS_WRAPPER_CLASS = 'modals-wrapper';
|
const MODALS_WRAPPER_CLASS = 'modals-wrapper';
|
||||||
var MODALS_WRAPPER_SELECTOR = '.' + MODALS_WRAPPER_CLASS;
|
const MODALS_WRAPPER_SELECTOR = '.' + MODALS_WRAPPER_CLASS;
|
||||||
var MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
|
const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
|
||||||
|
|
||||||
var modalUtil = function(element, app) {
|
@Utility({
|
||||||
|
selector: '[uw-modal]',
|
||||||
|
})
|
||||||
|
export class Modal {
|
||||||
|
|
||||||
var modalsWrapper;
|
_element;
|
||||||
var modalOverlay;
|
_app;
|
||||||
var modalUrl;
|
|
||||||
|
|
||||||
function _init() {
|
_modalsWrapper;
|
||||||
|
_modalOverlay;
|
||||||
|
_modalUrl;
|
||||||
|
|
||||||
|
constructor(element, app) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Modal utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureModalWrapper();
|
this._ensureModalWrapper();
|
||||||
|
|
||||||
// param modalTrigger
|
// param modalTrigger
|
||||||
if (!element.dataset.modalTrigger) {
|
if (!this._element.dataset.modalTrigger) {
|
||||||
throw new Error('Modal utility cannot be setup without a trigger element!');
|
throw new Error('Modal utility cannot be setup without a trigger element!');
|
||||||
} else {
|
} else {
|
||||||
setupTrigger();
|
this._setupTrigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
// param modalCloseable
|
// param modalCloseable
|
||||||
if (element.dataset.modalCloseable !== undefined) {
|
if (this._element.dataset.modalCloseable !== undefined) {
|
||||||
setupCloser();
|
this._setupCloser();
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark as initialized and add modal class for styling
|
// mark as initialized and add modal class for styling
|
||||||
element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
|
this._element.classList.add(MODAL_INITIALIZED_CLASS, MODAL_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: MODAL_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureModalWrapper() {
|
destroy() {
|
||||||
modalsWrapper = document.querySelector(MODALS_WRAPPER_SELECTOR);
|
// TODO
|
||||||
if (!modalsWrapper) {
|
}
|
||||||
|
|
||||||
|
_ensureModalWrapper() {
|
||||||
|
this._modalsWrapper = document.querySelector(MODALS_WRAPPER_SELECTOR);
|
||||||
|
if (!this._modalsWrapper) {
|
||||||
// create modal wrapper
|
// create modal wrapper
|
||||||
modalsWrapper = document.createElement('div');
|
this._modalsWrapper = document.createElement('div');
|
||||||
modalsWrapper.classList.add(MODALS_WRAPPER_CLASS);
|
this._modalsWrapper.classList.add(MODALS_WRAPPER_CLASS);
|
||||||
document.body.appendChild(modalsWrapper);
|
document.body.appendChild(this._modalsWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
modalOverlay = modalsWrapper.querySelector('.' + MODAL_OVERLAY_CLASS);
|
this._modalOverlay = this._modalsWrapper.querySelector('.' + MODAL_OVERLAY_CLASS);
|
||||||
if (!modalOverlay) {
|
if (!this._modalOverlay) {
|
||||||
// create modal overlay
|
// create modal overlay
|
||||||
modalOverlay = document.createElement('div');
|
this._modalOverlay = document.createElement('div');
|
||||||
modalOverlay.classList.add(MODAL_OVERLAY_CLASS);
|
this._modalOverlay.classList.add(MODAL_OVERLAY_CLASS);
|
||||||
modalsWrapper.appendChild(modalOverlay);
|
this._modalsWrapper.appendChild(this._modalOverlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTrigger() {
|
_setupTrigger() {
|
||||||
var triggerSelector = element.dataset.modalTrigger;
|
let triggerSelector = this._element.dataset.modalTrigger;
|
||||||
if (!triggerSelector.startsWith('#')) {
|
if (!triggerSelector.startsWith('#')) {
|
||||||
triggerSelector = '#' + triggerSelector;
|
triggerSelector = '#' + triggerSelector;
|
||||||
}
|
}
|
||||||
var triggerElement = document.querySelector(triggerSelector);
|
const triggerElement = document.querySelector(triggerSelector);
|
||||||
|
|
||||||
if (!triggerElement) {
|
if (!triggerElement) {
|
||||||
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
|
throw new Error('Trigger element for Modal not found: "' + triggerSelector + '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
triggerElement.classList.add(MODAL_TRIGGER_CLASS);
|
||||||
triggerElement.addEventListener('click', onTriggerClicked, false);
|
triggerElement.addEventListener('click', this._onTriggerClicked, false);
|
||||||
modalUrl = triggerElement.getAttribute('href');
|
this._modalUrl = triggerElement.getAttribute('href');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCloser() {
|
_setupCloser() {
|
||||||
var closerElement = document.createElement('div');
|
const closerElement = document.createElement('div');
|
||||||
element.insertBefore(closerElement, null);
|
this._element.insertBefore(closerElement, null);
|
||||||
closerElement.classList.add(MODAL_CLOSER_CLASS);
|
closerElement.classList.add(MODAL_CLOSER_CLASS);
|
||||||
closerElement.addEventListener('click', onCloseClicked, false);
|
closerElement.addEventListener('click', this._onCloseClicked, false);
|
||||||
modalOverlay.addEventListener('click', onCloseClicked, false);
|
this._modalOverlay.addEventListener('click', this._onCloseClicked, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTriggerClicked(event) {
|
_onTriggerClicked = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
open();
|
this._open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCloseClicked(event) {
|
_onCloseClicked = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
close();
|
this._close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyUp(event) {
|
_onKeyUp = (event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
close();
|
this._close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
_open() {
|
||||||
element.classList.add(MODAL_OPEN_CLASS);
|
this._element.classList.add(MODAL_OPEN_CLASS);
|
||||||
modalOverlay.classList.add(MODAL_OVERLAY_OPEN_CLASS);
|
this._modalOverlay.classList.add(MODAL_OVERLAY_OPEN_CLASS);
|
||||||
modalsWrapper.classList.add(MODALS_WRAPPER_OPEN_CLASS);
|
this._modalsWrapper.classList.add(MODALS_WRAPPER_OPEN_CLASS);
|
||||||
modalsWrapper.appendChild(element);
|
this._modalsWrapper.appendChild(this._element);
|
||||||
|
|
||||||
if (modalUrl) {
|
if (this._modalUrl) {
|
||||||
fillModal(modalUrl);
|
this._fillModal(this._modalUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keyup', onKeyUp);
|
document.addEventListener('keyup', this._onKeyUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
_close() {
|
||||||
modalOverlay.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
|
this._modalOverlay.classList.remove(MODAL_OVERLAY_OPEN_CLASS);
|
||||||
element.classList.remove(MODAL_OPEN_CLASS);
|
this._element.classList.remove(MODAL_OPEN_CLASS);
|
||||||
modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS);
|
this._modalsWrapper.classList.remove(MODALS_WRAPPER_OPEN_CLASS);
|
||||||
|
|
||||||
document.removeEventListener('keyup', onKeyUp);
|
document.removeEventListener('keyup', this._onKeyUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
function fillModal(url) {
|
_fillModal(url) {
|
||||||
app.httpClient.get({
|
this._app.httpClient.get({
|
||||||
url: url,
|
url: url,
|
||||||
headers: MODAL_HEADERS,
|
headers: MODAL_HEADERS,
|
||||||
}).then(function(response) {
|
}).then(
|
||||||
return app.htmlHelpers.parseResponse(response);
|
(response) => this._app.htmlHelpers.parseResponse(response)
|
||||||
}).then(function(response) {
|
).then(
|
||||||
processResponse(response.element);
|
(response) => this._processResponse(response.element)
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processResponse(responseElement) {
|
_processResponse(responseElement) {
|
||||||
var modalContent = document.createElement('div');
|
const modalContent = document.createElement('div');
|
||||||
modalContent.classList.add(MODAL_CONTENT_CLASS);
|
modalContent.classList.add(MODAL_CONTENT_CLASS);
|
||||||
|
|
||||||
var contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
|
const contentBody = responseElement.querySelector('.' + MAIN_CONTENT_CLASS);
|
||||||
|
|
||||||
if (contentBody) {
|
if (contentBody) {
|
||||||
modalContent.innerHTML = contentBody.innerHTML;
|
modalContent.innerHTML = contentBody.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousModalContent = element.querySelector('.' + MODAL_CONTENT_CLASS);
|
const previousModalContent = this._element.querySelector('.' + MODAL_CONTENT_CLASS);
|
||||||
if (previousModalContent) {
|
if (previousModalContent) {
|
||||||
previousModalContent.remove();
|
previousModalContent.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
element.insertBefore(modalContent, null);
|
this._element.insertBefore(modalContent, null);
|
||||||
|
|
||||||
// setup any newly arrived utils
|
// 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,
|
|
||||||
};
|
|
||||||
|
|||||||
16
frontend/src/utils/modal/modal.md
Normal file
16
frontend/src/utils/modal/modal.md
Normal 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
|
||||||
|
```
|
||||||
@ -1,109 +1,91 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './show-hide.scss';
|
import './show-hide.scss';
|
||||||
|
|
||||||
var SHOW_HIDE_UTIL_NAME = 'showHide';
|
const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
|
||||||
var SHOW_HIDE_UTIL_SELECTOR = '[uw-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';
|
@Utility({
|
||||||
var SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
|
selector: '[uw-show-hide]',
|
||||||
var SHOW_HIDE_COLLAPSED_CLASS = 'show-hide--collapsed';
|
})
|
||||||
var SHOW_HIDE_TOGGLE_CLASS = 'show-hide__toggle';
|
export class ShowHide {
|
||||||
var SHOW_HIDE_TOGGLE_RIGHT_CLASS = 'show-hide__toggle--right';
|
|
||||||
|
|
||||||
/**
|
_showHideId;
|
||||||
*
|
_element;
|
||||||
* 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) {
|
|
||||||
|
|
||||||
var showHideId;
|
constructor(element) {
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('ShowHide utility cannot be setup without an 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// register click listener
|
// register click listener
|
||||||
addClickListener();
|
this._addClickListener();
|
||||||
|
|
||||||
// param showHideId
|
// param showHideId
|
||||||
if (element.dataset.showHideId) {
|
if (this._element.dataset.showHideId) {
|
||||||
showHideId = element.dataset.showHideId;
|
this._showHideId = this._element.dataset.showHideId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// param showHideCollapsed
|
// param showHideCollapsed
|
||||||
var collapsed = false;
|
let collapsed = false;
|
||||||
if (element.dataset.showHideCollapsed !== undefined) {
|
if (this._element.dataset.showHideCollapsed !== undefined) {
|
||||||
collapsed = true;
|
collapsed = true;
|
||||||
}
|
}
|
||||||
if (showHideId) {
|
|
||||||
var localStorageCollapsed = getLocalStorage()[showHideId];
|
if (this._showHideId) {
|
||||||
|
let localStorageCollapsed = this._getLocalStorage()[this._showHideId];
|
||||||
if (typeof localStorageCollapsed !== 'undefined') {
|
if (typeof localStorageCollapsed !== 'undefined') {
|
||||||
collapsed = localStorageCollapsed;
|
collapsed = localStorageCollapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
|
this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
|
||||||
|
|
||||||
// param showHideAlign
|
// param showHideAlign
|
||||||
var alignment = element.dataset.showHideAlign;
|
const alignment = this._element.dataset.showHideAlign;
|
||||||
if (alignment === 'right') {
|
if (alignment === 'right') {
|
||||||
element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
|
this._element.classList.add(SHOW_HIDE_TOGGLE_RIGHT_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark as initialized
|
// mark as initialized
|
||||||
element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
this._element.classList.add(SHOW_HIDE_INITIALIZED_CLASS, SHOW_HIDE_TOGGLE_CLASS);
|
||||||
|
|
||||||
return {
|
|
||||||
name: SHOW_HIDE_UTIL_NAME,
|
|
||||||
element: element,
|
|
||||||
destroy: function() {},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addClickListener() {
|
destroy() {
|
||||||
element.addEventListener('click', function clickListener() {
|
this._removeClickListener();
|
||||||
var newState = element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
|
|
||||||
|
|
||||||
if (showHideId) {
|
|
||||||
setLocalStorage(showHideId, newState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocalStorage(id, state) {
|
_removeClickListener() {
|
||||||
var lsData = getLocalStorage();
|
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;
|
lsData[id] = state;
|
||||||
window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData));
|
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 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,
|
|
||||||
};
|
|
||||||
|
|||||||
21
frontend/src/utils/show-hide/show-hide.md
Normal file
21
frontend/src/utils/show-hide/show-hide.md
Normal 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
|
||||||
|
```
|
||||||
@ -1,7 +1,10 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
import './tooltips.scss';
|
import './tooltips.scss';
|
||||||
|
|
||||||
export default {
|
// empty shell to be able to load styles
|
||||||
name: 'tooltips',
|
@Utility({
|
||||||
selector: '[not-something-that-would-be-found]',
|
selector: '[not-something-that-would-be-found]',
|
||||||
setup: () => {},
|
})
|
||||||
|
export class Tooltip {
|
||||||
|
destroy() {}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,38 +1,26 @@
|
|||||||
import alerts from './alerts/alerts';
|
import { Alerts } from "./alerts/alerts";
|
||||||
import asidenav from './asidenav/asidenav';
|
import { Asidenav } from "./asidenav/asidenav";
|
||||||
import asyncForm from './async-form/async-form';
|
import { AsyncForm } from "./async-form/async-form";
|
||||||
import asyncTable from './async-table/async-table';
|
import { ShowHide } from "./show-hide/show-hide";
|
||||||
import checkAll from './check-all/check-all';
|
import { AsyncTable } from "./async-table/async-table";
|
||||||
import massInput from './mass-input/mass-input';
|
import { CheckAll } from "./check-all/check-all";
|
||||||
import { fileInput, checkbox } from './inputs/inputs';
|
import { FormUtils } from "./form/form";
|
||||||
import modal from './modal/modal';
|
import { InputUtils } from "./inputs/inputs";
|
||||||
import showHide from './show-hide/show-hide';
|
import { MassInput } from "./mass-input/mass-input";
|
||||||
import {
|
import { Modal } from "./modal/modal";
|
||||||
interactiveFieldset,
|
import { Tooltip } from "./tooltips/tooltips";
|
||||||
navigateAwayPrompt,
|
|
||||||
autoSubmitButton,
|
|
||||||
autoSubmitInput,
|
|
||||||
formErrorRemover,
|
|
||||||
datepicker,
|
|
||||||
} from './form/form';
|
|
||||||
import tooltips from './tooltips/tooltips';
|
|
||||||
|
|
||||||
export const Utils = [
|
export const Utils = [
|
||||||
alerts,
|
Alerts,
|
||||||
asidenav,
|
Asidenav,
|
||||||
asyncForm,
|
AsyncForm,
|
||||||
asyncTable,
|
AsyncTable,
|
||||||
checkAll,
|
CheckAll,
|
||||||
massInput,
|
ShowHide,
|
||||||
fileInput,
|
...FormUtils,
|
||||||
checkbox,
|
...InputUtils,
|
||||||
modal,
|
MassInput,
|
||||||
showHide,
|
Modal,
|
||||||
interactiveFieldset,
|
ShowHide,
|
||||||
navigateAwayPrompt,
|
Tooltip,
|
||||||
autoSubmitButton,
|
|
||||||
autoSubmitInput,
|
|
||||||
formErrorRemover,
|
|
||||||
datepicker,
|
|
||||||
tooltips,
|
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user