Files
P42_UI/app/thirdparty/eicui/eicui-2.1.js
T
2026-06-21 21:09:21 +00:00

3836 lines
122 KiB
JavaScript
Executable File

/**
* @classdesc The main EICUI libray. Available as a global static object ui
* @author Michael Fallise
* @version 2.1
* @hideconstructor
* @category EICUI/Core
*/
class ui {
/** @ignore */
static initiated = false;
static _components = {};
static mutations = null;
/**
* Default options
* @property {boolean} ariaEnabled Aria support
*/
static options = {
ariaEnabled: true
}
/**
* Format helper functions
* @type FormatHelper
*/
static format = null;
/**
* Growler helper
* @type Growler
*/
static growl = null;
/**
* Sets up the required layer elements used by the library
* @private
* @param options options to use when EICUI is instanciated
* @param options.ariaEnabled Wheter components should automatically generate Aria attributes
*/
static init(options) {
if(!ui.initiated) {
this.layers = {};
this.growl = new Growler(this.create('<div class="eic-growler"></div>'));
this.layers['growler'] = this.growl.el;
document.body.append(this.growl.el);
this.layers['blocker'] = this.create('<div class="eic-global-blocker"></div>');
document.body.appendChild(this.layers['blocker']);
document.body.setAttribute('eicapp', '');
this.mutations = new MutationObserver(ui.onDOMMutation) //no bind for static
this.mutations.observe(document.body, { childList: true, subtree: true })
ui.initiated = true;
}
ui.format = new FormatHelper();
//backward compatibility. please consider using ui.format instead of ui.dates
ui.dates = ui.format
}
/**
* Locks the window prohibiting any user interaction
*/
static lock() { this.layers['blocker'].classList.add('active'); }
/**
* Unlocks the window
*/
static unlock() { this.layers['blocker'].classList.remove('active'); }
/**
* Automatically scans a DOM structure and initiates known components
*
* @param {Element} container a DOM node structure
* @returns {array<EicComponent>}
*/
static eicfy(container) {
let elements = [];
let badges = container.querySelectorAll('[eicbadge]');
for(let i = 0; i < badges.length; i++) {
elements.push(new Badge(badges[i]));
}
let chips = container.querySelectorAll('[eicchip]');
for(let i = 0; i < chips.length; i++) {
elements.push(new Chip(chips[i]));
}
let buttons = container.querySelectorAll('[eicbutton]');
for(let i = 0; i < buttons.length; i++) {
elements.push(new Button(buttons[i]));
}
let inputs = container.querySelectorAll('[eicinput],[eiccheckbox]');
for(let i = 0; i < inputs.length; i++) {
let type = inputs[i].getAttribute('type');
switch(type) {
case 'checkbox': elements.push(new Checkbox(inputs[i])); break;
case 'hidden': elements.push(new InputHidden(inputs[i])); break;
case 'search': elements.push(new InputSearch(inputs[i])); break;
case 'date': elements.push(new InputDate(inputs[i])); break;
case 'number': elements.push(new InputNumber(inputs[i])); break;
case 'currency': elements.push(new InputCurrency(inputs[i])); break;
case 'toggler': elements.push(new InputToggler(inputs[i])); break;
default: elements.push(new Input(inputs[i]));
}
}
let textareas = container.querySelectorAll('[eictextarea]');
for(let i = 0; i < textareas.length; i++) {
elements.push(new Textarea(textareas[i]));
}
let selects = container.querySelectorAll('[eicselect]');
for(let i = 0; i < selects.length; i++) {
elements.push(new Select(selects[i]));
}
let cards = container.querySelectorAll('article[eiccard]');
for(let i = 0; i < cards.length; i++) {
elements.push(new Card(cards[i]));
}
let dropdowns = container.querySelectorAll('[eicdropdown]');
for(let i = 0; i < dropdowns.length; i++) {
elements.push(new DropDown(dropdowns[i]));
}
let alerts = container.querySelectorAll('[eicalert][closable]');
for(let i = 0; i < alerts.length; i++) {
elements.push(new Alert(alerts[i]));
}
return elements;
}
/**
* Converts a markup string into corresponding DOM structure
* @param {string} markup
* @returns {DOMElement} The first root element (and its childs if any) generated by the string.
*/
static create(markup, options) {
markup = markup.trim();
let el = null;
if(markup.indexOf('<') != 0) {
el = document.createElement(markup);
for(let prop in options) {
switch(prop) {
case 'html':
case 'text':
el.innerHTML = options[prop];
break;
case 'class':
el.className = options[prop];
break;
default:
if(el.hasOwnProperty(prop)) {
el[prop] = options[prop];
} else {
el.setAttribute(prop, options[prop]);
}
}
}
} else {
let doc = el = document.createElement('div');
doc.innerHTML = markup;
el = doc.firstElementChild;
}
return el;
}
/**
*
* @param {string} uid
* @returns {EicComponent|null}
*/
static getComponent(uid) { return ui._components.hasOwnProperty(uid) ? ui._components[uid] : null; }
/**
* Adds a component to the registered list
* @param {EicComponent} component
* @returns {string} Component's new uid
*/
static registerComponent(component) {
let uid = crypto.randomUUID();
ui._components[uid] = component;
return uid;
}
/**
* Removes a component from registered list
* @param {string} uid
*/
static unregisterComponent(uid) {
if(ui._components.hasOwnProperty(uid))
delete(ui._components[uid]);
}
/**
* Handler for DOM MutationObserver.
* Scans removed nodes and unregisters embedded EICUI components
* @param {*} mutationList
*/
static onDOMMutation(mutationList) {
for(let mutation of mutationList) {
for(let i = 0; i < mutation.removedNodes.length; i++) {
let node = mutation.removedNodes[i]
// skip text nodes
if(node.nodeName == '#text') break;
if(node.hasAttribute('data-eicui-id'))
ui.unregisterComponent(node.getAttribute('data-eicui-id'));
let components = node.querySelectorAll('[data-eicui-id]');
for(let component of components)
ui.unregisterComponent(component.getAttribute('data-eicui-id'));
}
}
}
/**
* Hides an Element
* @param {Element} element
*/
static hide(element) { element.style.display = 'none'; }
/**
* Shows (unhides) an Element
* @param {Element} element
* @param {boolean} [display] Forces the display property value. Default is 'block'.
*/
static show(element, mode) { element.style.display = mode ? mode: 'block'; }
/**
* Hides an Element in the DOM with a fade out effect.
* @param {Element} element
* @param {boolean} [detach] If true, removes the element from the DOM once fade animation is completed
*/
static fadeOut(el, detach) {
let intId = setInterval(function () {
if(!el.style.opacity) { el.style.opacity = 1; }
if(el.style.opacity > 0) {
el.style.opacity -= 0.1;
} else {
ui.hide(el);
el.style.opacity = 1;
clearInterval(intId);
if(detach) { el.remove(); }
}
}, 20);
}
/**
* Returns an element's nearest parent matching selector rule
* @param {Element} element The source element
* @param {string} selector The selector rule matching the targeted parent
* @returns {Element|null} The first (bottom/up) matching parent if any
*/
static queryParent(child, selector) {
if (selector === undefined) {
selector = document;
}
var parents = [];
var p = child.parentNode;
while (p !== parentSelector) {
var o = p;
parents.push(o);
p = o.parentNode;
}
parents.push(selector);
return parents[parents.length-1];
}
/**
* Returns the actual index of an Element within its parent
* @param {Element} element The target Element
* @returns {number} The element child index
*/
static childIndex(el) {
let childs = el.parentNode.childNodes;
let index = 0;
for(let i = 0; i < childs.length; i++) {
if (childs[i] == el) return index;
if (childs[i].nodeType == 1) index++;
}
return -1;
}
/**
* Copies the styling properties of an Element to another
* @param {Element} source The source Element
* @param {Element} target The target Element
*/
static copyEicuiStyling(fromEl, toEl) {
let m
for(let eicuiAttr of ['[danger]', '[primary]', '[secondary]', '[success]', '[warning]', '[accent]', '[info]', '[disabled]',
'[xxsmall]','[xsmall]','[small]','[medium]','[large]','[xlarge]','xxl[arge','[xxxlarge]','[xxxxlarge]',
'.required'] ) {
m = eicuiAttr.match(/\[(\w+)\]/)
if(m && fromEl.hasAttribute(m[1])) toEl.setAttribute(m[1],fromEl.getAttribute(m[1]))
m = eicuiAttr.match(/\.(\w+)/)
if(m && fromEl.classList.contains(m[1])) toEl.classList.add(m[1])
}
}
}
/**
* Special types formatting helper class
* @hideconstructor
* @category EICUI/Helpers
*/
class FormatHelper {
defaultCurrency = '€';
/**
* Formats a number
* @param {string|number} str The number to be formatted
* @returns {string} The formatted number
*/
number(str) {
str += '';
let s = parseFloat(0 + str).toString();
let x = s.split('.');
let x1 = x[0];
let x2 = (x.length > 1) ? '.' + x[1]: '';
let r = /(\d+)(\d{3})/;
while (r.test(x1)) {
x1 = x1.replace(r, '$1' + ' ' + '$2');
}
return x1 + x2;
}
/**
* Formats a currency. Uses the number() format method.
* @param {string|number} str The amount to be formatted
* @param {string} [currency] The currency symbol to apply. Default is '€'.
* @returns {string} The formatted amount
*/
currency(str, currency) { return this.number(str) + ' ' + (currency ? currency: this.defaultCurrency) ; }
/**
* Formats a date string
* @param {string} str The date string to format
* @returns {string} The formatted date
*/
date(str) {
let d = new Date(str);
return d.toLocaleDateString('en-DE', { year: 'numeric', month: 'short', day: 'numeric' });
}
/**
* Formats a date and time string
* @param {string} str The date and time string to format
* @returns {string} The formatted date
*/
dateTime(str) {
let d = new Date(str);
return d.toLocaleDateString('en-DE', { year: 'numeric', month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString('en-DE');
}
/**
* Formats a time string
* @param {string} str The time string to format
* @returns {string} The formatted time
*/
time(str) {
let d = new Date(str);
return d.toLocaleTimeString('en-DE');
}
url(str) {
if(str && !/^(?:f|ht)tps?\:\/\//.test(str)) {
str = "https://" + str;
}
return str;
}
// backward compatibility
currency2String = this.currency
date2String = this.date
dateTime2String = this.dateTime
time2String = this.time
}
/**
* @classdesc Component base (ancestor) class
* @category EICUI/Core
*
*/
class EicComponent {
/**
* @type {object}
* @property {boolean} aria-enabled Aligns to global ui setting by default. see {@link ui#options}
*/
options = {
'aria-enabled': false
};
_el = null;
/**
* @param {DOMElement|null} [element] a dom element attached to the component
* @param {Object} [options] see [options]{@link EicComponent#options}
*/
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
this._uid = ui.registerComponent(this);
this.el = el;
this.setOptions(options || {});
this.attributes({ 'aria-enabled': ui.options.ariaEnabled });
}
get el() { return this._el; }
set el(markup) {
if(typeof markup == 'string') {
const placeholder = document.createElement("div");
placeholder.innerHTML = markup;
const node = placeholder.firstElementChild ;
this._el = node;
} else {
this._el = markup;
}
if(this._el) {
this._el.setAttribute('data-eicui-id', this._uid);
}
}
/**
* Merges passed options to component's options
*/
setOptions(options) { this.mergeOptionsNode(this.options, options); }
/**
* @ignore
*/
mergeOptionsNode(target, source) {
for(let option in source) {
if(typeof source[option] === 'object' && !Array.isArray(source[option]) && (option in target) && !(target[option] instanceof(Element))) {
this.mergeOptionsNode(target[option], source[option]);
} else {
target[option] = source[option];
}
}
}
/** @type {boolean} */
set disabled(state) { state ? this.el.setAttribute('disabled',''): this.el.removeAttribute('disabled'); }
get disabled() { return this.el.hasAttribute('disabled'); }
/**
* Enables a component. same as obj.disabled = false
*/
enable() { this.disabled = false; }
/**
* @typedef {string} UISize
* Semantic UI size values. based on EUI semantic.
*
* Available values:
* <pre>
* 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | 'xxxlarge' | 'xxxxlarge'
* </pre>
*/
/**
* The component size attribute. See EUI semantic size values.
* @type {UISize}
*/
set size(value) {
if(!this.el) return;
let sizes = [ 'xxsmall','xsmall','small','medium','large','xlarge','xxlarge','xxxlarge','xxxxlarge' ];
sizes.forEach(val => this.el.removeAttribute(val));
this.el.setAttribute(value, '');
}
get size() {
if(!this.el) return('');
for(let size of [ 'xxsmall','xsmall','small','medium','large','xlarge','xxlarge','xxxlarge','xxxxlarge' ]) {
if(this.el.hasAttribute(size)) return(size)
}
return('')
}
/**
* @typedef {string} UISeverity
* UI severity color code. Based EUI severity semantic. Available values:
* <pre>
* 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'accent' | 'info'
* </pre>
*/
/**
* The severity attribute for the component
* @type {UISeverity}
*/
set severity(value) {
if(!this.el) return;
let values = [ 'primary','secondary','success','warning','danger','accent','info' ];
values.forEach(val => this.el.removeAttribute(val));
this.el.setAttribute(value, '');
}
get severity() {
if(!this.el) return('');
for(let severity of [ 'primary','secondary','success','warning','danger','accent','info' ]) {
if(this.el.hasAttribute(severity)) return(severity)
}
return('')
}
/**
* Sets rounded angles for the component.
* @type {boolean} */
set rounded(value) {
if (value) {
this.el.setAttribute('rounded', '');
} else {
this.el.removeAttribute('rounded');
}
}
/** @ignore */
get position() { return this.el.getBoundingClientRect(); }
/**
* Pastes attributes
* @param {*} properties
* @example
* let attr = {
* 'data-id': 1,
* 'data-name': 'test'
* }
* obj.attributes(attr)
*
*/
attributes(properties) {
if(this.el)
for(let property in properties) {
this.el.setAttribute(property, properties[property]);
}
}
/**
* Hides the element
*/
hide() {
this._baseDisplay = this.el.style.display;
this.el.style.display = 'none';
}
/**
* Shows the element. Restores the display property value catched when using hide(), uses 'block' otherwise.
* @todo allow forcing display value
*/
show() { this.el.style.display = this._baseDisplay ? this._baseDisplay: 'block'; }
/**
* Adds an event listener
* @param {string} type Type of event
* @param {function} callback callback function
*/
addEventListener(type, callback) { this.el.addEventListener(type, callback); }
}
/**
* Alert boxes
* @augments EicComponent
* @category EICUI/Components
*
* @todo check issue with options collision
*/
class Alert extends EicComponent {
/** @inheritdoc */
/**
*
* @param {Element|null} element
* @param {*} options
*/
constructor(el, options) {
/**
* @type {object}
* @property {string} [severity]
* @property {boolean} [closable]
* @property {string} [message]
*/
let defaultOptions = {
severity: '',
closable: false,
message: ""
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {})});
this.setOptions(options);
if(!this.el) { this.el = ui.create(`<div eicalert ${this.options.severity}>${options.message || ''}</div>`); }
if(this.el.hasAttribute('closable')) this.options.closable = true;
if(this.options.severity) this.severity = this.options.severity;
if(this.options.closable) {
this.el.setAttribute('closable','');
this.el.addEventListener('click', this.close.bind(this));
}
this.attributes({
'aria-enabled': ui.options.ariaEnabled,
'role': 'status',
'aria-label': 'Alert'
});
}
/** @ignore */
close(event) {
event.preventDefault();
event.stopPropagation();
this.el.remove();
this.el.dispatchEvent(new Event('closed'));
}
}
/**
* A badge component
* @augments EicComponent
* @category EICUI/Components
*/
class Badge extends EicComponent {
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, options);
if(!this.el) this.el = ui.create(`<span eicbadge></span>`);
this.value = parseInt(this.el.innerHTML) || 0;
if(this.options.size) this.size = this.options.size;
this.attributes({
'aria-enabled': ui.options.ariaEnabled,
'role': 'status',
'aria-label': 'Badge'
});
}
/**
* Value of the badge
*/
set value(val) {
this._value = val;
this.el.innerHTML = this._value;
if(val == 0)
ui.hide(this.el);
else
ui.show(this.el,'inline-flex');
}
get value() { return this._value; }
/**
* Increase badge value by 1
*/
increment() { if(this._value) this.value = this._value++; }
/**
* Decrease badge value by 1
*/
decrement() { if(this._value) this.value = this._value--; }
}
/**
* Main UI message growler. Automatically instanciated by ui and available through ui.growl
* @see ui#growl
* @static
* @augments EicComponent
* @category EICUI/Core
* @hideconstructor
*/
class Growler extends EicComponent {
defaultLifeTime = 6000;
/**
* Growls a message
* @param {string} message
* @param {string} severity
* @param {number} lifetime Value in ms. Default 10s.
*/
append(message, severity, lifetime) {
let alert = new Alert(null, { message: message, severity: severity, closable: true });
alert.attributes({
'aria-enabled': ui.options.ariaEnabled,
'role': 'status',
'aria-label': message
})
this.el.appendChild(alert.el);
if(lifetime !== 0) setTimeout(this.remove.bind(this,alert), lifetime || this.defaultLifeTime);
}
/**
* @ignore
* @param {*} alert
* @param {*} event
*/
remove(alert, event) { ui.fadeOut(alert.el, true); }
}
/**
* A card container. Cards contain a title header, a content section and optionally a footer.
* @augments EicComponent
* @category EICUI/Components
*
*/
class Card extends EicComponent {
/**
*
* @param {Element|null} element
* @param {object} options see [options]{@link Card#options}
*/
constructor(el, options) {
/**
*
* @type {object}
* @property {boolean} options.collapsable
* @property {boolean} options.collapsed
* @property {boolean} options.busy
*/
let defaultOptions = {
collapsable: false,
collapsed: false,
busy: false
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {})});
this.setOptions(options);
this.header = this.el.querySelector('header');
this.content = this.el.querySelector('section');
this.footer = this.el.querySelector('footer');
if(this.el.hasAttribute('collapsable')) this.options.collapsable = true;
if(this.el.hasAttribute('collapsed')) this.options.collapsed = true;
if(this.options.collapsable) {
this.expander = ui.create(`<button eicbutton rounded class="collapser icon-angle-up"></button>`);
this.expander.addEventListener('click', this.toggle.bind(this));
this.header.appendChild(this.expander);
if(this.options.collapsed)
this.collapse();
else
this.expand();
}
}
/** @type {string} */
set title(str) { if(this.header) this.header.querySelector('h1').innerHTML = str; }
/** @type {string} */
set subtitle(str) { if(this.header) this.header.querySelector('h2').innerHTML = str; }
/** @type {boolean} */
get collapsed() { return this.options.collapsed; }
/** @type {boolean} */
set loading(value) {
if(value) {
this.el.classList.add('loading')
} else {
this.el.classList.remove('loading')
}
}
/**
*
*/
clear() {}
/**
*
* @param {*} event
*/
toggle(event) {
event.stopPropagation();
event.preventDefault();
if(this.options.collapsed){
this.expand();
} else {
this.collapse();
}
}
/**
*
*/
collapse() {
this.expander.classList.remove('arrow-expand', 'arrow-collapse');
void this.expander.offsetWidth;
this.expander.classList.add('arrow-collapse');
this.el.setAttribute('collapsed','');
this.content.classList.add('collapsed');
this.options.collapsed = true;
}
/**
*
*/
expand() {
this.expander.classList.remove('arrow-expand', 'arrow-collapse');
void this.expander.offsetWidth;
this.expander.classList.add('arrow-expand');
this.el.removeAttribute('collapsed');
this.content.classList.remove('collapsed');
this.options.collapsed = false;
}
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class Chip extends EicComponent {
constructor(el, options) {
let defaultOptions = {
label: null,
severity: '',
size: '',
icon: null,
badge: null,
destroyable: false,
data: {}
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {})});
if(!this.el)
this.el = ui.create(`<span eicchip><label>${this.options.label}<label></span>`);
for(let parm in this.options.data) {
this.el.setAttribute('data-' + parm, this.options.data[parm]);
}
if(this.el.hasAttribute('destroyable')) this.options.destroyable = true;
if(this.el.hasAttribute('severity')) this.options.severity = this.el.getAttribute('severity');
if(this.el.hasAttribute('size')) this.options.severity = this.el.getAttribute('size');
if(this.el.hasAttribute('destroyable')) this.options.destroyable = true;
if(this.el.hasAttribute('icon')) this.options.icon = this.el.getAttribute('icon');
if(this.el.hasAttribute('badge')) this.options.badge = this.el.getAttribute('badge');
if(this.options.destroyable) {
let button = ui.create(`<button class="close"><i class="icon-close"></i></button>`);
button.addEventListener('click', this.onDestroy.bind(this));
this.el.appendChild(button);
}
if(this.options.icon) { this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`)); }
if(this.options.badge) {
this.badge = new Badge(null, {size: 'xxsmall'});
this.el.appendChild(this.badge.el);
this.badge.value = this.options.badge;
}
if(this.options.severity) { this.severity = this.options.severity; }
if(this.options.size) { this.size = this.options.size; }
if(this.options.icon) { this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`)); }
this.attributes({
'aria-enabled': ui.options.ariaEnabled,
'role': 'status',
'aria-label': 'Chip'
});
}
onDestroy(event) {
event.stopPropagation();
event.preventDefault();
this.el.parentElement.removeChild(this.el);
this.el.dispatchEvent(new CustomEvent('destroy', {detail: this.options.data}));
this.onDestroyed(this)
}
onDestroyed(chip) { }
}
/**
* Tab Controlled Content component
* @category EICUI/Components
* @author Michael Fallise
* @version 1.0
*/
class Tab {
/**
* @constructor
*/
constructor() {
this.triggers = [];
this.contents = [];
}
/**
* Links a new set of tab control and content to the collection
* @param {Object} trigger a DOM element used to requet the content
* @param {Object} content a DOM element to be displayed by the trigger
*/
addTab(trigger, content) {
this.triggers.push(trigger);
this.contents.push(content);
ui.hide(content);
trigger.setAttribute('tab-id', this.triggers.length);
trigger.addEventListener('click',this.onTriggerClick.bind(this));
}
/**
* Links an array of tab controls to an array of contents to the collection
* @param {Array} trigger a DOM element used to requet the content
* @param {Array} content a DOM element to be displayed by the trigger
*/
addTabs(triggers, contents) {
if(triggers.length > 0) {
for(let i = 0; i < triggers.length; i++) {
this.addTab(triggers[i], contents[i])
}
this.selectByIndex(0);
}
}
/**
* click event handler
* @param {Object} event a mouse event object fired by the trigger element
*/
onTriggerClick(event) {
var button = event.currentTarget;
for(var i = 0; i < this.triggers.length; i++) {
if(this.triggers[i].getAttribute('tab-id') == button.getAttribute('tab-id')) {
this.selectByIndex(i);
break;
}
}
}
/**
* Activates toggler corresponding to the provided index
* @param {number} index the index of the trigger to activate
*/
selectByIndex(index) {
for(var i = 0; i < this.triggers.length; i++) {
this.triggers[i].classList.remove('tab-selected');
ui.hide(this.contents[i]);
}
this.triggers[index].classList.add('tab-selected');
ui.show(this.contents[index]);
this.triggers[index].dispatchEvent(new Event('selected'));
}
/**
* Activates toggler containing provided class
* @param {string} classname the css class name of the trigger to activate
*/
selectByClass(classname) {
for(var i = 0; i < this.triggers.length; i++) {
if(this.triggers[i].classList.has(classname)) {
this.triggers[i].classList.add('tab-selected');
ui.show(this.contents[i]);
} else {
this.triggers[i].classList.remove('tab-selected');
ui.hide(this.contents[i]);
}
}
}
/**
* Retrieves the index of the active trigger
* @return {number} the current index
*/
getSelectedIndex() {
for(var i = 0; i < this.triggers.length; i++) {
if(this.triggers[i].classList.contains('tab-selected')) return i;
}
return -1;
}
/**
* Retrieves the active trigger
* @return {Element} the current trigger DOM Element
*/
getSelected() { return this.triggers.find(el => el.classList.contains('tab-selected')); }
/*
* Disables the toggler corresponding to the provided index
* @param {integer} index the toggler index to activate
*
*/
disableByIndex(index) {
let trigger = this.triggers[index];
trigger.setAttribute('disabled', '');
}
}
/**
* Button component
* @augments EicComponent
* @category EICUI/Components
*/
class Button extends EicComponent {
constructor(el, options) {
let defaultOptions = {
icon: null,
severity: '',
disabled: false,
badge: false,
rounded: false,
size: null,
hint: null,
label: '',
onclick: null
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {})});
if(!this.el) { this.el = ui.create('<button eicbutton></button>'); }
if(this.el.hasAttribute('icon')) this.options.icon = this.el.getAttribute('icon');
if(this.el.hasAttribute('disabled')) this.options.disabled = true;
if(this.el.hasAttribute('badge')) this.options.badge = this.el.getAttribute('badge') || true;
if(options && options.label) this.label = options.label;
if(options && options.severity) this.severity = options.severity;
if(this.options.icon) {
let icon = this.el.querySelector('i');
if(icon) icon.remove();
this.el.appendChild(ui.create(`<i class="${this.options.icon}"></i>`));
}
if(this.options.rounded) { this.el.setAttribute('rounded', ''); }
if(this.options.size) { this.el.setAttribute(this.options.size, ''); }
if(this.options.hint) { this.el.setAttribute('title', this.options.hint); }
if(this.options.badge) {
this.badge = this.el.querySelector('[eicbage]');
if(this.badge) this.badge.remove();
this.badge = new Badge(ui.create(`<span eicbadge danger>${this.options.badge}</i>`));
this.el.appendChild(this.badge.el);
}
if(options && options.onclick) this.click = options.onclick;
this.disabled = this.options.disabled;
this.attributes({
'aria-enabled': ui.options.ariaEnabled,
'role': 'button',
'aria-label': this.el.getAttribute('title') || 'Button'
});
if(this.el.hasAttribute('loading')) this.loading = this.el.getAttribute('loading');
this.el.addEventListener('click', this.onClick.bind(this));
}
set label(label) {
this.options.label = label
this.el.innerHTML = `<span>${label}</span>`;
}
set hint(hint) {
this.options.hint = hint
this.el.setAttribute('title', hint)
}
set loading(value) {
let icon = this.el.querySelector('i');
if(icon) {
icon.className = value ? 'icon-spinner spin': this.options.icon;
} else {
this.el.setAttribute('loading', value);
}
this.disabled = value;
}
show() {
this.el.style.display = 'inline-flex';
}
onClick(event) {
event.stopPropagation();
event.preventDefault();
this.click(event);
}
// By Nike: be silent by default in places with old-style addEventListener shuts-up :-)
// To trace those places : change it to { console.log(event); console.trace() }
click(event) { }
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class Select extends EicComponent {
/**
* @param {element|string} el A SELECT element or the selector string for the SELECT which will be binded to the component.
* This passed SELECT element will be hidden (display: none) by the wrapper but will still accessible by the DOM.
* The component will use the OPTION and OPTGROUP nodes contained in the parent SELECT to feed its list.
* @param {object} [options] Component options:
* @param {boolean} [options.multi] Set to true to enable mutiple value selection. Can also be enabled by adding a "mutiple" class to the original SELECT element
* @param {boolean} [options.lookup] Set to true to enable value lookup (search engine). Can also be enabled by adding a "lookup" class to the original SELECT element
*
*/
listenVanilla = true;
activeEditor = SelectEditor;
constructor(el, options) {
let defaultOptions = {
multi: false,
editable: false,
lookup: false,
maxItems: 0,
placeholder: '',
hint: '',
editor: null
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {})});
let self = this;
if(el.hasAttribute('placeholder')) this.options.placeholder = el.getAttribute('placeholder');
if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');
if(el.hasAttribute('multiple')) { this.options.multi = true; }
if(el.hasAttribute('lookup')) { this.options.lookup = true; }
if(el.hasAttribute('editable')) {
this.options.editable = true;
this.options.multi = true;
el.setAttribute('multiple', '')
}
this._value = [];
this.selectedItems = [];
this.coat(el);
this.disabled = el.hasAttribute('disabled');
// sync selected options
el.querySelectorAll(':checked').forEach( (el) => el.value ? this.selectedItems.push({value: el.value || el.text, label: el.text}): el=el );
this.catalog = !this.options.editable ? this.getCatalog(): null;
//ui.hide(this.catalog);
// tracks changes if vanilla select is modified
el.addEventListener('change', this.onVanillaChange.bind(this))
//el.addEventListener('DOMNodeInserted', this.onVanillaChange.bind(this))
this.mutationList = { attributes: true, childList: true, subtree: false };
this.observer = new MutationObserver(this.onVanillaChange.bind(this));
this.observer.observe(el, this.mutationList);
this.selection.addEventListener('remove', function() { ui.hide(self.catalog); });
this.update();
if(this.options.editable) {
this.selection.addEventListener('click', this.onOpenEditor.bind(this));
}
this.visibilityObserver = new IntersectionObserver( this.onVisibilityChanged.bind(this), {root: null, rootMargin: "0px", threshold:0})
}
coat(el) {
if(this.el.classList.contains('required')) {
this.options.hint = 'This field is required. ' + this.options.hint;
}
this.container = ui.create(`<div class="eicui-input-container"></div>`)
if(el.parentNode) el.parentNode.insertBefore(this.container, el);
this.container.append(this.el);
this.selection = ui.create(`<div class="eicui-select-selection ${this.el.getAttribute('class')}"></div>`);
//this.selection.classList.remove('eicui-select');
this.container.append(this.selection);
if(this.options.hint != '') {
this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
this.container.append(this.hint);
}
this.selection.setAttribute('placeholder', this.options.placeholder)
ui.hide(el);
}
/**
* Vanilla Select value change handler (value assignment goes upstream)
* @param {*} event
*/
onVanillaChange(event) {
if(this.listenVanilla) {
let options = this.el.querySelectorAll('option[selected="selected"], option[selected]')
//if(options.length > 0) this.value = Array.prototype.slice.call(options).map(el => el.value);
}
}
getCatalog() {
let catalog = document.querySelector('#eicui-select-catalog');
// Build catalog window if it has not been set up already (catalog window is shared between all select components)
if(!catalog) {
catalog = ui.create(`<div id="eicui-select-catalog" class="eicui-select-catalog"></div>`);
document.body.appendChild(catalog);
catalog.appendChild(ui.create(`<div class="content"></div>`));
// close catalog when loosing focus
window.addEventListener('click', this.closeCatalog.bind(this));
catalog.addEventListener('click', function(event) { event.stopPropagation(); });
}
this.selection.addEventListener('click', this.onOpenCatalog.bind(this));
return catalog
}
onOpenCatalog(event) {
event.stopPropagation();
this.openCatalog(this.options.lookup);
}
/**
* Opens the items selection dialog
*/
openCatalog(withLookup) {
// resetting lookup input
let search = this.catalog.querySelector('.search');
if(search) {
search.remove();
}
this.searchInput = ui.create(`<input type="text" class="search" />`);
this.searchInput.addEventListener('keyup',this.lookup.bind(this));
this.catalog.prepend( this.searchInput);
if(withLookup) {
this.catalog.classList.add('lookup');
}
else {
this.catalog.classList.remove('lookup');
}
search = this.catalog.querySelector('.search');
search.value = '';
let content = this.catalog.querySelector('.content');
content.innerHTML = '';
this.loadItems(this.el, content);
this.catalog.style.left = this.offset.left + 'px';
this.catalog.style.top = this.offset.top + 'px';
this.catalog.width = this.selection.getBoundingClientRect().width + 'px';
ui.show(this.catalog);
this.catalog.setAttribute('opened', '');
this.visibilityObserver.observe(this.container)
}
/**
* Event handler for selection list closing. Can be used as a standard method when no event is provided.
* @private
*/
closeCatalog(event) {
if(this.catalog.hasAttribute('opened')) {
if(event) {
event.stopPropagation();
event.preventDefault();
}
ui.fadeOut(this.catalog);
this.catalog.removeAttribute('opened');
}
if(this._keyPressed) this.catalog.removeEventListener('keypressed', this._keyPressed)
this.visibilityObserver.disconnect()
}
/**
* Event handler called when the (coated) selector visibility changes.
* Used to make sure to close the catalog when the selector dissapears for any reason.
* @private
*/
onVisibilityChanged(intersections, obs){
if(intersections[0].isIntersecting) return
// This closes the catalog also when scrolling-out the selector. (not bad as catalog is floating)
// if you still want to avoid this, add some condition on the computed visibility of this.container
if(this.catalog.hasAttribute('opened')) this.closeCatalog()
}
/**
*
* @returns {Object}
*/
getEditor() {
if(!this.editor) {
this.editor = new this.activeEditor();
this.editor.initUI();
this.editor.initEvents();
this.selection.after(this.editor.el);
window.addEventListener('click', this.editor.onClose.bind(this.editor));
this.editor.addEventListener('click', function(event) { event.stopPropagation(); });
}
return this.editor;
}
/**
*
* @param {*} event
*/
async onOpenEditor(event) {
event.stopPropagation();
let response = await this.openEditor();
if(response) { this.addItem(response); }
}
onEditorOpened() {}
/**
*
*/
openEditor() {
let self = this;
let promise = new Promise(function(resolve, reject) {
self.listenVanilla = false;
self.catalog = self.getEditor();
self.catalog.open();
self.onEditorOpened(self.catalog);
self.catalog.el.querySelector('input').value = "";
self.catalog.el.querySelector('input').focus();
function commit(result) {
self.catalog.close();
resolve(result);
}
function abort(result) {
self.catalog.close();
reject(result);
}
self.catalog.commit = commit;
self.catalog.abort = abort;
});
return promise;
}
/**
*
*/
closeEditor() {
ui.hide(this.catalog.el);
this.catalog.el.removeAttribute('opened', '');
//this.listenVanilla = true;
}
/**
*
* @param {*} event
*/
onKeyPressed(event) {
event.stopPropagation();
if(event.keyCode == 13) {
this.addItem(this.catalog.el.querySelector('input').value);
this.closeEditor();
}
}
/**
*
*/
set disabled(value) {
if(value)
this.selection.setAttribute('disabled','');
else
this.selection.removeAttribute('disabled');
}
/**
*
* @returns NodeList
*/
get items() { return this.el.querySelectorAll('options'); }
/**
* Finds Y axis position of element in the page
* @returns number
*/
get offset() {
if (!this.selection.getClientRects().length) {
return { top: 0, left: 0 };
}
let rect = this.selection.getBoundingClientRect();
let win = this.selection.ownerDocument.defaultView;
return ( { top: rect.top + win.scrollY, left: rect.left + win.scrollX });
}
/**
* Returns selected value(s)
* @returns {string|Array}
*/
get value() {
return this.options.multi ? this._value: this._value.length > 0 ? this._value[0]: null ;
}
set value(val) {
if(val === this._value) return;
if(typeof val == "boolean") {
val = val.toString();
}
if(val === '' || val === null) {
this.clear();
return;
}
if(this.options.multi) {
if(!Array.isArray(val)) {
val = val.split(',');
}
} else {
val = val != '' ? [ val ]: [];
}
this.selectedItems = [];
for(let i = 0; i < val.length; i++) {
let option
if(this.options.editable) {
option = ui.create('<option value="' + val[i] + '" selected="selected">"' + val[i] + '"</option>');
this.listenVanilla = false;
this.el.append(option);
//this.listenVanilla = true;
} else {
option = this.el.querySelector('option[value="' + val[i] + '"]');
}
if(val[i] != '' && option)
this.selectedItems.push({value: val[i], label: option.label});
}
this.update();
return;
}
/**
* Wrapper for jQuery element.change() event handler method
* Triggered when the selection is changed (through a user interaction).
* @param {function} handler The call back function called when event is triggered
*/
change(handler) { this.el.addEventListener('change', handler); }
/**
* Selects an item in the collection
* @param {item} The selected LI element
* @param {boolean|null} forcedValue if set, selects (with true) or unselects (with false) the item. If null, toggles the selection state.
*/
select(item, forcedValue) {
this.listenVanilla = false;
// check if max selection reached
if(this.options.multi && this.options.maxItems > 0 && !item.classList.contains('selected')) {
if(this.selectedItems.length == this.options.maxItems) return;
}
if(forcedValue == null) {
item.classList.toggle('selected');
} else {
if(forcedValue) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
}
forcedValue = item.classList.contains('selected');
let value = item.getAttribute('data-value');
let label = item.querySelector('span');
let childs = item.querySelectorAll('ul > li');
if(item.classList.contains('selected')) {
if(!this.options.multi) {
this.selectedItems = [];
this.catalog.querySelector('.selected').classList.remove('selected');
item.classList.add('selected');
this.closeCatalog();
}
if(this.selectedItems.findIndex(x => x.value == value) == -1) {
this.selectedItems.push({value: value, label: label.innerHTML});
}
} else {
var index = this.selectedItems.findIndex(x => x.value == value);
this.selectedItems.splice(index, 1);
}
for(var i = 0; i < childs.length ; i++) {
this.select(childs[i], forcedValue);
}
this.update();
this.listenVanilla = true;
}
/**
* Event handler for item removal from selection
*/
remove(event) {
event.stopPropagation();
event.preventDefault();
this.listenVanilla = false;
if(this.options.multi) {
let id = event.detail.id;
this.selectedItems.splice(this.selectedItems.findIndex(x => x.value == id), 1);
} else {
this.clear();
}
this.update();
this.listenVanilla = true;
}
/**
* Remove all options
*/
empty() {
this.el.innerHTML = '';
this.clear();
}
/**
* Clears the selection
*/
clear(noevent) {
this._value = '';
this.selectedItems = [];
this.update(noevent);
}
addItem(newItem) {
this.listenVanilla = false;
if(this.options.maxItems > 0 && this.selectedItems.length == this.options.maxItems) return;
if(!this.selectedItems.find(item => item.value == newItem.value)) {
this.selectedItems.push(newItem);
let option = this.el.querySelector(`option[value="${newItem.value}"]`)
if(!option) {
option = ui.create(`<option value="${newItem.value}">${newItem.label}</option>`)
this.el.append(option);
}
option.setAttribute('selected','selected')
this.update();
}
//this.catalog.el.querySelector('input').value = "";
}
/*
* Fills catalog window with the related SELECT.OPTION fields content (recurses on optgroup subsets)
* @private
*/
loadItems(el, container, filter) {
let self = this;
let tags = el.children;
let list = [];
let ul = ui.create('<ul/>');
let latest = null;
this.listenVanilla = false;
container.appendChild(ul);
for(var i = 0; i < tags.length; i++) {
let tag = tags[i];
if(!filter || filter == '') {
if(tag.nodeName == 'OPTION' && tag.getAttribute('value')) {
let li = ui.create(`<li data-value="${tag.getAttribute('value') || tag.innerText}" ${tag.getAttribute('data-ref') ? `data-ref="${tag.getAttribute('data-ref')}"`: ''}><span>${tag.innerText}</span></li>`);
li.addEventListener('click',function(event) {
event.stopPropagation();
event.preventDefault();
self.select(this);
});
ul.appendChild(li);
latest = li;
if(this.selectedItems.findIndex(x => x.value == tag.getAttribute('value')) != -1) li.classList.add('selected');
}
if(tag.nodeName == 'OPTGROUP') {
let lane = latest
if(tag.getAttribute('data-parent') != '') {
lane = this.catalog.querySelector('.content').querySelector('li[data-ref="' + tag.getAttribute('data-parent') + '"]')
ul.appendChild(lane);
}
this.loadItems(tag, lane, filter);
}
} else {
if(tag.nodeName == 'OPTION' && tag.getAttribute('value')) {
if(tag.innerText.toLowerCase().indexOf(filter.toLowerCase()) != -1) {
let li = ui.create(`<li data-value="${tag.getAttribute('value') || tag.innerText}" ${tag.getAttribute('data-ref') ? `data-ref="${tag.getAttribute('data-ref')}"`: ''}><span>${tag.innerText}</span></li>`);
li.addEventListener('click',function(event) {
event.stopPropagation();
event.preventDefault();
self.select(this);
});
ul.appendChild(li);
latest = li;
if(this.selectedItems.findIndex(x => x.value == tag.getAttribute('value')) != -1) li.classList.add('selected');
}
}
if(tag.nodeName == 'OPTGROUP') { this.loadItems(tag, ul, filter); }
}
}
this.listenVanilla = true;
return list;
}
/*
* Filters to catalog window values
* @private
*/
lookup(event) {
let content = this.catalog.querySelector('.content');
content.innerHTML = '';
this.loadItems(this.el, content, event.target.value);
}
/**
* Refreshes displayed selection with stored values
* @private
*/
update(silent) {
this.selection.innerHTML = this.selectedItems.length == 0 ? this.options.placeholder:'';
this._value = [];
this.listenVanilla = false;
// resetting all select options
this.el.querySelectorAll('option[selected="selected"], option[selected]').forEach(el => el.removeAttribute('selected'));
for(let i = 0; i < this.selectedItems.length; i++) {
if(this.selectedItems[i].value != "") {
this.el.querySelector('option[value="' + this.selectedItems[i].value + '"]').setAttribute('selected', 'selected');
if(this.options.multi || this.options.editable ) {
let item = new Chip(null,{label: this.selectedItems[i].label, destroyable:true, size: 'small', data: {id: this.selectedItems[i].value}})
item.addEventListener('destroy',this.remove.bind(this));
this.selection.appendChild(item.el);
} else {
let item = ui.create(`<span class="item"><span>${this.selectedItems[i].label}</span><button class="clear"><i class="icon-close"></i></button></span>`)
let button = item.querySelector('button');
button.addEventListener('click',this.remove.bind(this));
this.selection.appendChild(item);
}
this._value.push(this.selectedItems[i].value);
}
}
if(!silent) this.el.dispatchEvent(new CustomEvent('change'));
this.listenVanilla = true;
}
}
class Select2 extends EicComponent {
selectedItems = []
constructor(el, options) {
let defaultOptions = {
multi: false,
editable: false,
lookup: false,
maxItems: 0,
placeholder: '',
hint: '',
editor: null
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(el.hasAttribute('placeholder')) this.options.placeholder = el.getAttribute('placeholder');
if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');
if(el.hasAttribute('multiple')) { this.options.multi = true; }
if(el.hasAttribute('lookup')) { this.options.lookup = true; }
if(el.hasAttribute('editable')) {
this.options.editable = true;
this.options.multi = true;
el.setAttribute('multiple', '')
}
this.selectedItems = [];
this.coat(el);
this.disabled = el.hasAttribute('disabled');
// sync selected options
//el.querySelectorAll(':checked').forEach( (el) => el.value ? this.selectedItems.push({value: el.value || el.text, label: el.text}): el=el );
// tracks changes if vanilla select is modified
//el.addEventListener('change', this.onVanillaChange.bind(this))
//el.addEventListener('DOMNodeInserted', this.onVanillaChange.bind(this))
//this.mutationList = { attributes: true, childList: true, subtree: false };
//this.observer = new MutationObserver(this.onVanillaChange.bind(this));
//this.observer.observe(el, this.mutationList);
//this.selection.addEventListener('remove', function() { ui.hide(self.catalog); });
this.update();
this.selection.addEventListener('click', this.onMenuOpen.bind(this));
}
coat(el) {
if(this.el.classList.contains('required')) {
this.options.hint = 'This field is required. ' + this.options.hint;
}
this.container = ui.create(`<div class="eicui-input-container"></div>`)
if(el.parentNode) el.parentNode.insertBefore(this.container, el);
this.container.append(this.el);
this.selection = ui.create(`<div class="eicui-select-selection ${this.el.getAttribute('class') || ''}"></div>`);
this.container.append(this.selection);
if(this.options.hint != '') {
this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
this.container.append(this.hint);
}
this.selection.setAttribute('placeholder', this.options.placeholder)
ui.hide(el);
}
onMenuOpen(event) {
event.stopPropagation();
event.preventDefault();
}
onMenuClose(event) {
event.stopPropagation();
event.preventDefault();
}
addOption(option, selected) {
let tag = ui.create(`<option value="${option.value}" ${selected ? 'selected=selected': ''}>${option.label}</option>`)
this.el.append(tag);
if(selected) this.el.dispatchEvent(new CustomEvent('change'))
}
loadOptions() { this.selectedItems = []; }
saveOptions() { }
get value() {
return (
this.options.multiple ?
this.selectItems.map(item => item.value) :
this.selectItems.length != 0 ?
this.selectItems[0].value:
null
)
}
set value(val) {}
clear() {
this.selectedItems = [];
this.saveOptions();
this.refresh();
}
refresh() { }
}
/**
* @augments EicComponent
* @category EICUI/Plugins/Select
*/
class SelectEditor extends EicComponent {
_template = `<div class="eicui-select-editor"><input eicinput data-type="ignore" /></div>`;
_value = '';
constructor(el, options) {
super();
if(el) this._template = el;
this.setOptions(options);
}
initUI() {
if(!this.el) {
this.el = ui.create(this._template);
this.components = ui.eicfy(this.el);
}
}
initEvents() { this.el.querySelector('input').addEventListener('keypress', this.onKeyPressed.bind(this)); }
get valid() { return this.el.querySelector('input').value.trim != ''; }
get value() { return { value: this.el.querySelector('input').value, label: this.el.querySelector('input').value } }
/**
*
* @param {*} event
*/
onKeyPressed(event) {
event.stopPropagation();
if(event.keyCode == 13 && this.valid) this.commit(this.value);
}
open() {
ui.show(this.el, 'grid');
}
onClose(event) {
event.stopPropagation();
this.commit();
}
close() { ui.hide(this.el); }
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class Menu extends EicComponent {
constructor(el, options) {
let defaultOptions = {
collapsable: true,
lookup: false,
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(!el) this.el = ui.create(`<menu eicmenu></menu>`);
if(el.hasAttribute('lookup')) this.options.lookup = true;
if(this.options.lookup) {
this.search = ui.create(`<li class="menu-lookup"><input eicinput type="search" placeholder="Search filter" /></li>`);
this.search.querySelector('input').addEventListener('keyup', this.filter.bind(this))
this.el.append(this.search);
}
}
get collapsed() { return this.el.hasAttribute('collapsed'); }
set collapsed(value) {
if(value)
this.el.setAttribute('collapsed', '');
else
this.el.removeAttribute('collapsed');
}
parse(data, node) {
node = node || this.el;
for(let i = 0; i < data.length; i++) {
let item = data[i];
this.addItem(node, item);
}
}
clear() {
let items = this.el.querySelectorAll('li[menuitem]');
for(let item of items) item.remove();
}
/**
* @typedef {Object} Menu.DataItem
* @property {string} label Display text for the item
* @property {string} [icon] Icon to use for item
* @property {string} [severity]
* @property {boolean} [collapsed] forces collapsing of the item (only appliable to parents of a sub list)
* @property {Array<Menu.DataItem>} [items] Array of child items (IMPORTANT: only 1 sub level supported)
*/
/**
* Adds an entry to the list
* @param {DOMElement} target The UL element to paste the entry to
* @param {Menu.DataItem} item The entry description
*/
addItem(target, item) {
let node = new MenuItem();
let parent = target;
let el = node.el;
parent.appendChild(el);
parent = el;
if(item.severity) {
node.el.setAttribute(item.severity, '');
}
if(item.route || item.onclick) {
el = ui.create(`<a href="${item.route || '#'}" title="${item.label.replace(/<\/?[^>]+(>|$)/g, "")}"></a>`);
if(item.onclick) el.addEventListener('click', item.onclick);
node.el.classList.add('menu-link');
} else {
el = ui.create(`<div class="nolink"></div>`);
}
parent.appendChild(el);
parent = el;
if(item.icon) { parent.appendChild(ui.create(`<i class="${item.icon}"></i>`)); }
parent.appendChild(ui.create(`<label>${item.label}</label>`));
if(item.items) {
el = ui.create(`<button eicbutton rounded xsmall class="icon-angle-up"></button>`);
el.addEventListener('click', this.toggle.bind(this));
parent.appendChild(el);
el = ui.create(`<ul></ul>`)
parent.parentElement.appendChild(el);
this.parse(item.items, el)
if(item.collapsed) { this.collapse(el); }
}
}
/**
* Event handler for collapsed state change
* @param {*} event
*/
toggle(event) {
event.preventDefault();
event.stopPropagation();
let list = event.currentTarget.parentElement.parentElement.querySelector('ul');
if(list.hasAttribute('collapsed'))
this.expand(list);
else
this.collapse(list);
}
//get collapsed() { return }
/**
* Collapses a menu item
* @param {*} el
*/
collapse(el) {
el.setAttribute('collapsed', '');
let button = el.parentElement.querySelector('button');
button.classList.remove('arrow-expand');
// hack for resetting animation
void button.offsetWidth;
button.classList.add('arrow-collapse');
}
/**
* Expands a menu item
* @param {*} el
*/
expand(el) {
el.removeAttribute('collapsed');
let button = el.parentElement.querySelector('button');
button.classList.add('arrow-expand');
void button.offsetWidth;
button.classList.remove('arrow-collapse');
}
/**
* Event handler for menu filtering
* @param {*} event
*/
filter(event) {
let query = event.currentTarget.value.toLowerCase();
let entries = this.el.querySelectorAll(`[menuitem].menu-link`);
for(let entry of entries) {
if(query == '') {
ui.show(entry);
} else {
let label = entry.querySelector('a > label').innerText;
if(label.toLowerCase().indexOf(query) != -1)
ui.show(entry, 'flex');
else
ui.hide(entry);
}
}
}
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class MenuItem extends EicComponent {
constructor(el, options) {
let defaultOptions = {
collapsable: true,
disabled: false,
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(!el) { this.el = ui.create(`<li menuitem></li>`) }
}
addItem(el) { }
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class DropDown extends EicComponent {
constructor(el, options) {
let defaultOptions = {
badge: null,
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(this.el.querySelector('[eicbutton]'))
this.button = new Button(this.el.querySelector('button'));
if(this.el.querySelector('[eicuser]'))
this.button = new UserIcon(this.el.querySelector('button'));
this.options.badge = this.button.badge;
this.button.click = this.toggle.bind(this)
this.menu = new Menu(this.el.querySelector('menu'));
this.el.append(this.menu.el);
this.menu.el.addEventListener('click', this.onMenuClick.bind(this));
}
onMenuClick(event) {
event.preventDefault();
event.stopPropagation();
this.collapse();
}
toggle(event) {
event.preventDefault();
event.stopPropagation();
this.menu.el.setAttribute('align', this.position.top > (window.innerHeight / 2) ? 'top': 'bottom' );
this.menu.el.setAttribute('justify', this.position.left > (window.innerWidth / 2) ? 'right': 'left' );
if(this.el.hasAttribute('expanded'))
this.collapse();
else
this.expand();
}
collapse() { this.el.removeAttribute('expanded'); }
expand() { this.el.setAttribute('expanded', '');}
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class Input extends EicComponent {
constructor(el, options) {
let defaultOptions = {
type: 'text',
value: '',
maxlen:null,
maxbytes:null,
maxwords:null,
hint: null
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(!el) el = ui.create(`<input eicinput type="${this.options.type}" value="${this.options.value}" />`);
this.coat(el);
}
get value() { return this.input.el.value; }
set value(val) {
this.input.el.value = val;
this.update();
}
set disabled(state) {
super.disabled = state;
if(this.badge) {
if(state == true) {
ui.hide(this.badge.el);
} else {
ui.show(this.badge.el);
}
}
if(this.hint) {
if(state == true) {
ui.hide(this.hint);
} else {
ui.show(this.hint);
}
}
}
get disabled() { return super.disabled; }
coat(el) {
if(el.hasAttribute('maxlen')) this.options.maxlen = el.getAttribute('maxlen');
if(el.hasAttribute('maxbytes')) this.options.maxbytes = el.getAttribute('maxbytes');
if(el.hasAttribute('maxwords')) this.options.maxwords = el.getAttribute('maxwords');
if(el.hasAttribute('hint')) this.options.hint = el.getAttribute('hint');
if(this.el.classList.contains('required')) {
this.options.hint = 'This field is required. ' + (this.options.hint ? this.options.hint: '');
}
this.input = new EicComponent(el);
this.el = this.input.el;
this.container = ui.create(`<div class="eicui-input-container"></div>`)
if(el.parentNode) el.parentNode.insertBefore(this.container, el);
this.container.append(this.el);
if(this.options.hint) {
this.hint = ui.create(`<div class="eicui-input-hint">${this.options.hint}</div>`);
this.container.append(this.hint);
}
if(this.options.maxbytes || this.options.maxwords || this.options.maxlen) {
this.badge = new Badge();
this.update();
this.container.insertBefore(this.badge.el, this.el);
this.input.addEventListener('keyup', this.onKeyPressed.bind(this));
}
}
update() {
let remaining = 0;
let unit = '';
if(this.options.maxlen) {
remaining = this.options.maxlen - this.value.length;
unit = ' chars';
} else
if(this.options.maxwords) {
remaining = this.options.maxwords - this.value.split(' ').filter(e => e != '').length;
unit = ' words';
} else
if(this.options.maxbytes) {
remaining = this.options.maxbytes - (new TextEncoder().encode(this.value)).length;
unit = ' bytes';
}
if(this.badge) {
if(remaining < 0) {
this.badge.value = 'too long';
this.badge.severity = 'danger';
this.input.severity = 'danger';
this.input.el.classList.add('invalid');
} else {
this.badge.value = remaining + unit;
this.badge.severity = 'secondary';
this.input.severity = 'secondary';
this.input.el.classList.remove('invalid');
}
}
}
onKeyPressed(event) { this.update(); }
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputHidden extends Input {
constructor(el, options) {
super(el, options);
this.container.classList.add('eic-input-hidden')
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputSearch extends Input {
_loading = false;
constructor(el, options) {
let defaultOptions = {
type: 'text',
value: '',
maxlen:null,
hint: null,
placeholder: '',
allowEmptyString: true,
allowQueryOnKey: false
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
// wrapping input with icon overlay
let wrapper = ui.create('<div class="input-wrapper"></div>');
this.icon = ui.create('<i class="icon-search"></i>');
this.container.prepend(wrapper);
wrapper.append(this.icon);
wrapper.append(this.input.el);
//this.container.prepend(this.icon);
this.container.classList.add('eic-input-search');
this.input.addEventListener('keypress', this.onKeyPressed.bind(this));
}
onKeyPressed(event) {
if(this._loading) {
event.preventDefault();
event.stopPropagation();
return;
}
if(this.options.allowQueryOnKey || event.keyCode == 13) this.onQuery()
}
onQuery() {}
get loading() { return this._loading; }
set loading(active) {
this._loading = active;
if(active) {
this.icon.className = "icon-spinner spin"
} else {
this.icon.className = "icon-search"
}
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputDate extends Input {
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, options);
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class Checkbox extends Input {
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, options);
this.container.classList.add('eic-checkbox');
if(el.hasAttribute('label'))
this.container.append(ui.create(`<label>${el.getAttribute('label')}</label>`))
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputToggler extends Input {
/**
* @event onToggle#onBeforeUploadAllStart
* @type {event}
* @param {string} value
* @param {Object} the toggler component
*/
onToggle(value, object) { }
constructor(el, options) {
let defaultOptions = {
value: false,
labelLeft:'',
labelRight:'',
title:'',
trueValue: 'yes',
falseValue: 'no',
classOn:'',
classOff:'',
tabindex:0,
disabled:false
}
options = options || {}
if(el.hasAttribute('labelright')) options.labelRight = el.getAttribute('labelright');
if(el.hasAttribute('labelleft')) options.labelLeft = el.getAttribute('labelleft');
if(el.hasAttribute('classOn')) options.classOn = el.getAttribute('classOn');
if(el.hasAttribute('classOff')) options.classOff = el.getAttribute('classOff');
if(el.hasAttribute('title')) options.title = el.getAttribute('title');
if(el.hasAttribute('trueValue')) options.trueValue = el.getAttribute('truevalue');
if(el.hasAttribute('falseValue')) options.falseValue = el.getAttribute('falsevalue');
if(el.hasAttribute('value')) options.value = el.getAttribute('value');
if(el.hasAttribute('tabindex')) options.tabindex = el.getAttribute('tabindex');
if(el.hasAttribute('disabled')) options.disabled = true;
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...options});
if(this.options.value == this.options.trueValue) this.turnOn()
else this.turnOff()
}
coat(el) {
super.coat(el)
this.container.classList.add('input-toggler')
el.style.display = 'none'
this.labelRight = ui.create(`<div class="toggle-label-right">${this.options.labelRight}</div>`)
this.labelLeft = ui.create(`<div class="toggle-label-left">${this.options.labelLeft}</div>`)
this.switch = ui.create(`
<div class="toggle-switch" title="${this.options.title}" ${this.options.disabled ? 'disabled' : ''}>
<span class="toggle-bar">
<span class="toggle-thumb"></span>
</span>
</div>
`)
this.container.appendChild(this.labelLeft)
this.container.appendChild(this.switch)
this.container.appendChild(this.labelRight)
this.container.setAttribute('tabindex',this.options.tabindex)
ui.copyEicuiStyling(this.el, this.container)
this.thumb = this.container.querySelector(".toggle-thumb")
this.switch.addEventListener('click', this.toggle.bind(this))
}
/**
* The severity attribute for the component
* @type {UISeverity}
*/
set severity(value) {
if(!this.container) return;
let values = [ 'primary','secondary','success','warning','danger','accent','info' ];
values.forEach(item => this.container.removeAttribute(item));
this.container.setAttribute(value, '');
}
turnOn() {
if(this.options.classOff) this.switch.classList.remove(this.options.classOff)
if(this.options.classOn) this.switch.classList.add(this.options.classOn)
this.thumb.classList.add('turned-on')
this._value = this.options.trueValue
this.el.value = this._value
this.onToggle(this._value, this)
this.container.focus()
}
turnOff() {
if(this.options.classOn) this.switch.classList.remove(this.options.classOn)
if(this.options.classOff)this.switch.classList.add(this.options.classOff)
this.thumb.classList.remove('turned-on')
this._value = this.options.falseValue
this.el.value = this._value
this.onToggle(this._value, this)
this.container.focus()
}
toggle(event) {
if(event) { event.preventDefault(); event.stopPropagation(); }
if(this.options.disabled) return
if(this._value == this.options.trueValue) this.turnOff()
else this.turnOn()
}
get value() { return(this._value) }
set value(v) {
if(v == this.options.trueValue ) this.turnOn()
else this.turnOff()
}
set disabled(value){
this.options.disabled = value
if(value) this.switch.setAttribute('disabled','') // for the css
else this.switch.removeAttribute('disabled')
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputNumber extends Input {
constructor(el, options) {
let defaultOptions = {
type: 'number',
value: '',
max: null,
min: null,
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
if(el.hasAttribute('min')) this.options.min = parseFloat(el.getAttribute('min'));
if(el.hasAttribute('max')) this.options.min = parseFloat(el.getAttribute('max'));
this.input.el.addEventListener('blur', this.validate.bind(this));
}
validate() {
let valid = true
if(this.options.min)
if(this.input.el.value < this.options.min) valid = false;
if(this.options.max)
if(this.input.el.value > this.options.max) valid = false;
if(!valid) {
this.input.el.classList.add('invalid')
this.container.classList.add('validation-failed')
} else {
this.input.el.classList.remove('invalid')
this.container.classList.remove('validation-failed')
}
if(!valid)
return valid;
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class InputCurrency extends Input {
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, options);
el.setAttribute('type', 'number');
this.container.classList.add('input-currency');
let marker = ui.create(`<i class="currency-marker">€</i>`);
this.container.insertBefore(marker, el);
}
}
/**
* @augments Input
* @category EICUI/Components
*/
class Textarea extends Input {
constructor(el, options) {
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el || `<textarea eictextarea>${options.value || ''}</textarea>`, options);
}
}
/**
* @augments EicComponent
* @category EICUI/Components
*/
class Form extends EicComponent {
constructor(el,options) {
let defaultOptions = {
mode: 'edit',
skipValidation: false
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
}
get mode() { return this.options.mode; }
set mode(value) { this.options.mode = value; }
validate(extendedReport) {
let fields = this.scan();
let globalReport = { valid: true, issues: 0 }
for(let field of fields) {
let report = this.validateField(field);
field.container.classList.remove('validation-failed');
if(!report.valid) {
field.container.classList.add('validation-failed');
globalReport.issues++;
}
globalReport.valid = globalReport.valid && report.valid;
}
return extendedReport ? globalReport: globalReport.valid;
}
get value() {
let fields = this.scan();
let values = {};
function add(path, value) {
let segments = path.split('.');
let field = segments.pop();
let current = values;
let re = /\[([^)]+)\]/;
let isArray = null;
for(let segment of segments) {
isArray = re.exec(segment);
if(isArray) {
// remove index from segment string
segment = segment.replace(isArray[0], '');
if(!(current.hasOwnProperty(segment)))
current[segment] = [];
current = current[segment];
// fill array with empty items until length matches selected index
while(current.length < (parseInt(isArray[1]) + 1)) current.push({});
current = current[parseInt(isArray[1])];
} else {
if(!(current.hasOwnProperty(segment)))
current[segment] = {};
current = current[segment];
}
}
// checking if field is array
isArray = re.exec(field);
if(isArray) {
field = field.replace(isArray[0], '');
if(!(Array.isArray(current[field]))) current[field] = [];
// fill array with empty items until length matches selected index
while(current[field].length < (isArray[0] + 1)) current = current[field].push({});
current[field][parseInt(isArray[1])] = value;
} else {
current[field] = value;
}
}
for(let field of fields) {
// skip checkboxes either unchecked or without a value
//if(field.el.type == 'checkbox' && (field.el.value && !field.el.checked)) continue;
let value = field.el.type == 'checkbox' ? field.el.checked : field.el.value;
if(field.el.nodeName == 'SELECT') {
if(field.el.hasAttribute('multiple')) {
// this one is nasty... getting all selected values of a select in an array
value = Array.prototype.slice.call(field.el.querySelectorAll('option[selected="selected"], option[selected]'),0).map(function(v,i,a) { return v.value; })
} else {
let option = field.el.querySelector('option[selected="selected"], option[selected]')
value = option ? option.value : '';
}
}
switch(field.el.dataset.type) {
case 'boolean': value = value !== '' ? value === 'true': null; break;
case 'number': value = isNaN(parseFloat(value)) ? null : parseFloat(value); break;
case 'ignore': continue;
}
add((field.el.dataset.path || '') + (field.el.name ? (field.el.dataset.path ? '.': '') + field.el.name: ''), value);
}
return values;
}
fill(components,data) {
let fields = this.scan();
for(let field of fields) {
let component = this.element2component(components, field.el.name, field.el.dataset.path);
if(component) component.disabled = this.options.mode == 'read' || component.disabled;
let item = this.findDataField(data, field.el.dataset.path, field.el.name)
if(item !== null && typeof item !== 'undefined') {
component.value = item;
}
}
}
findDataField(data, path, name) {
let levels = path && path != '' && path !== undefined ? path.split('.'): [];
let tree = data;
for(let level of levels)
if(tree.hasOwnProperty(level)) tree = tree[level];
if(tree.hasOwnProperty(name)) return tree[name];
}
element2component(components, name, path) {
return (
components
.find(o =>
o.el.hasAttribute('name') && o.el.getAttribute('name') == name &&
( !path || (path && o.el.dataset.path == path))
)
)
}
/**
* Scans form and returns all avaliable fields
*/
scan(includeIgnored) {
let fields = [];
let elements =
includeIgnored ?
this.el.querySelectorAll('input,select,textarea'):
this.el.querySelectorAll('input:not(.ignore),select:not(.ignore),textarea:not(.ignore)');
for(let element of elements) {
let container = element;
if(
element.hasAttribute('eicinput') ||
element.hasAttribute('eiccheckbox') ||
element.hasAttribute('eictextarea')
) { container = element.parentElement; }
if(element.hasAttribute('eicselect')) { container = element.nextSibling; }
fields.push({
el: element,
container: container
});
}
return fields;
}
validateField(field) {
let report = {
target: field.el,
valid: true,
errors: []
};
let value = report.target.value;
let nodeType = report.target.nodeName.toLowerCase();
let type = 'text';
switch(nodeType) {
case 'input':
type = report.target.getAttribute('type') || 'text';
break;
case 'textarea':
type = 'text';
break;
case 'select':
// workaround for Safari: forcing value based on selected option (el.value sending wrong value)
let option = field.el.querySelector('option[selected="selected"], option[selected]')
if(option) value = option.value
break;
}
if(report.target.classList.contains('required')) {
if(['text', 'email','url', 'date', 'number', 'currency'].includes(type)) {
let test = (value.length != 0);
if(!test) report.errors.push('field cannot be empty')
report.valid = report.valid && test;
}
if(type == 'checkbox') {
report.valid = report.valid && report.target.checked;
}
}
if(['number', 'currency'].includes(type)) {
let test = (value.length == 0 || (!isNaN(value) && !isNaN(parseFloat(value))));
if(!test) report.errors.push('field must be numeric')
report.valid = report.valid && test;
if(report.target.getAttribute('min')) {
let test = (
value.length == 0 ||
(
!isNaN(value) &&
!isNaN(report.target.getAttribute('min')) &&
parseFloat(value) >= parseFloat(report.target.getAttribute('min'))
)
);
if(!test) report.errors.push('lower than min range')
report.valid = report.valid && test;
}
}
if(report.target.classList.contains('invalid')) {
report.valid = false;
report.errors.push('content is invalid')
}
return report;
}
}
/**
* As a TABLE replacer, the data grid component delivers a flexible way to present data sets.
* The component features:
* - sortable columns
* - filtrable content based on definable column types (freetext, lists, ...)
* - draggable rows (reordering)
* - inline row editing
* - flexible "by row" actions
* - CCS3 grid based layout providing flexible designs and styling
*
* @extends EicComponent
* @category EICUI/AdvancedComponents
* @version 2.0
*/
class DataGrid extends EicComponent {
/**
* Triggered when a row is clicked
*
* @event DataGrid#rowClick
* @type {mouseevent}
* @property {element} currentTarget The target grid-row DOM element
*/
/**
* Triggered when selection changes
*
* @event DataGrid#change
* @type {event}
*/
/**
* @param {element|string} el A DOM element or the selector string for the element which will contain the component
* @param {object} [options] The grid options
* @param {array} [options.headers] Defines the grid column headers
* @param {string} [options.height] Forces the css height property of the component
* @param {boolean} [options.editable] The grid options
* @param {number} [options.maxRows] Sets the maximum amount of rows the content can hold
* @param {boolean} [options.allowRowDrag] Enables graggable rows
* @param {boolean} [options.allowRowDrag] Enables graggable rows
*
*/
constructor(el, options) {
let defaultOptions = {
height: -1,
editable: false,
maxRows: -1,
minRows: -1,
allowRowDrag: false,
headers: [],
rowActions: [],
};
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
this.totalRows = 0;
this.activeRows = 0;
this.setOptions(options);
this.el.setAttribute('eicdatagrid', '');
this.header = ui.create(`<header><ul class="rows"><li class="row"><div class="cell"><input type="checkbox" /></div></li></ul></header>`);
this.el.appendChild(this.header);
this.body = ui.create(`<div class="dataset"><ul class="rows"></ul></div>`);
this.el.appendChild(this.body);
this.footer = ui.create(`<footer><div><span class="displayed">0</span> rows out of <span class="total">0</span></div></footer>`);
this.el.appendChild(this.footer);
let headerRow = this.header.querySelector('.row');
if(this.options.height != -1) {
this.body.style.maxHeight = this.options.height;
}
this.dataset = this.body.querySelector('.rows');
if(this.options.allowRowDrag) {
this.el.classList.add('indexed');
}
this.globalSelector = this.header.querySelector('.cell');
this.globalSelector.addEventListener('click', this.onGlobalClick.bind(this));
this.filters = [];
for(let i = 0; i < this.options.headers.length; i ++) {
let column = this.options.headers[i];
let safeLabel = ui.create(`<div>${column.label}</div>`).innerText
let cell = ui.create(`<div class="cell"><span title="${safeLabel}">${column.label}</span></div>`)
if(column.sortable) {
let label = cell.querySelector('span');
label.setAttribute('data-order', 'asc');
label.classList.add('sortable');
if(column.type && (column.type == 'number' || column.type == 'currency')) label.classList.add('numeric');
label.addEventListener('click', this.onColumnSort.bind(this));
}
this.filters.push(null);
if(column.filter && column.filter == 'text') {
let input = ui.create('<input type="text" />');
this.filters[i] = input;
cell.appendChild(input);
input.addEventListener('keyup', this.filter.bind(this));
}
if(column.filter && column.filter == 'list') {
let select = ui.create('<select><option></option></select>');
this.filters[i] = select;
cell.appendChild(select);
select.addEventListener('change', this.filter.bind(this));
}
this.header.querySelector('.row').appendChild(cell);
}
this.editing = false;
let actions = ui.create('<div class="cell actions"></div>');
this.header.querySelector('.row').appendChild(actions);
if(this.options.editable) {
this.btNew = ui.create(`<button eicbutton primary>Add</button>`);
this.btNew.addEventListener('click', this.onRowAdd.bind(this));
actions.appendChild(this.btNew);
this.options.rowActions.push({ icon: '<i class="icon-edit"></i>', title: 'Edit item', callback: this.onRowEdit.bind(this) });
this.options.rowActions.push({ class: 'remove', icon: '<i danger class="icon-bin"></i>', title: 'Delete item', callback: this.onRowDelete.bind(this) });
this.enableEditing(this.rows().length < this.options.maxRows);
}
}
set loading(value) {
if(value)
this.el.setAttribute('loading', true);
else
this.el.removeAttribute('loading');
}
set enableFooter(state) { state ? this.el.removeAttribute('footer'): this.el.setAttribute('footer', 'hidden')}
set enableHeader(state) { state ? this.el.removeAttribute('header'): this.el.setAttribute('header', 'hidden')}
/**
* Clears the grid content
*/
clear() {
this.dataset.innerHTML = '';
for(let filter of this.filters) {
if(filter) filter.value='';
}
for(let header of this.header.querySelectorAll('.cell span')) {
header.classList.remove('active')
}
this.activeRows = 0;
this.totalRows = 0;
this.updateFooter();
}
/**
* Applies column filters to the content
*/
filter() {
let rows = this.dataset.querySelectorAll('.row');
this.activeRows = 0;
for(let r = 0; r < rows.length; r++) {
let valid = true;
let row = rows[r];
for(let i = 0; i < this.filters.length; i++) {
if(this.filters[i]) {
let value = this.filters[i].value;
if(value != '') {
if(this.filters[i].nodeName == 'SELECT' && row.querySelectorAll('.cell')[i+1].innerText != value) valid = false;
if(this.filters[i].nodeName == 'INPUT') {
var str = value.replace('(', '\\(').replace(')', '\\)');
var re = RegExp(str, 'i');
if(!re.test(row.querySelectorAll('.cell')[i+1].innerText)) valid = false;
}
}
}
}
if(!valid) {
this.hideRow(row);
} else {
this.showRow(row);
this.activeRows++;
}
}
this.updateFooter();
this.onRowFiltered();
}
/**
* checks grid validity: - minRows criteria
*/
isValid() {
let valid = true;
if(this.options.minRows != -1) {
valid = valid && this.rows().length >= this.options.minRows;
}
return valid;
}
/**
* Event handler when grid is filtered. overridable.
*/
onRowFiltered() { }
/**
* Sort the content
* @param {number} col The column index to sort on
* @param {string} order Sets the sort order. Either 'asc' or 'desc'.
* @param {boolean} isNumber If true, cell values will be sorted as numbers. If false or null values will be sorted as strings.
*/
sort(col, order, isNumber) {
let index = [];
let i = 0;
let rows = this.rows();
for(i = 0; i < rows.length; i++) {
let current = rows[i];
let value = current.querySelector('.cell:nth-child(' + (col + 1) + ')').innerText.toLowerCase();
if(isNumber) {
let n = parseFloat(value);
value = !isNaN(n) ? n: -1;
}
index.push({value: value, row: current});
}
if(order == 'asc') {
index.sort((a,b) => (a.value > b.value) ? 1 : ((b.value > a.value) ? -1 : 0));
} else {
index.sort((a,b) => (a.value < b.value) ? 1 : ((b.value < a.value) ? -1 : 0));
}
for(i = 0; i < index.length; i++) {
let item = index[i];
let row = item.row.parentElement.removeChild(item.row);
this.dataset.appendChild(row);
}
}
/**
* Rebuilds the columns filter lists.
*/
updateFilters() {
for(let i = 0; i < this.filters.length; i++) {
let filter = this.filters[i];
if(filter && filter.nodeName == 'SELECT') {
filter.innerHTML = '';
filter.appendChild(ui.create('<option></option>'));
var values = this.getColValues(i);
for(let o = 0; o < values.length; o++) {
var val = values[o];
filter.appendChild(ui.create(`<option value="${val}">${val}</option>`));
}
}
}
}
updateFooter() {
this.footer.querySelector('.displayed').innerText = this.filteredRows.length;
this.footer.querySelector('.total').innerText = this.rows().length;
}
/**
* @private
*/
enableEditing(allowEditing) {
if(allowEditing) {
this.btNew.classList.remove('disabled');
} else {
this.btNew.classList.add('disabled');
}
}
/**
* Renders the inline edit form
* @private
* @param {element} row the targetted grid-row element to be edited
*/
buildEditor(row) {
let editor = ui.create(`<div class="row editor" data-id=""><div class="cell"><i class="icon-edit"></div></div></div>`);
let values = row ? row.querySelectorAll('.cell'): null;
for(let i = 0; i < this.options.headers.length; i++) {
let header = this.options.headers[i];
// TODO use header type to generate either inputs or selects
let cell = ui.create(`<div class="cell"><input type="text" value="${values ? values[i+1].innerHTML.replace(/"/g,'&quot;'): ''}" /></div>`);
editor.appendChild(cell)
}
let btSave = ui.create(`<button eicbutton primary>Save</button>`);
let btCancel = ui.create(`<button eicbutton secondary>Cancel</button>`);
if(values) {
btSave.addEventListener('click', this.onRowUpdate.bind(this));
btCancel.addEventListener('click', this.onRowCancelEdit.bind(this));
} else {
btSave.addEventListener('click', this.onRowCreate.bind(this));
btCancel.addEventListener('click', this.onRowCancelCreate.bind(this));
}
let actions = $(`<div class="cell actions"></div>`);
actions.appendChild(btCancel);
actions.appendChild(btSave);
editor.appendChild(actions);
return editor;
}
/**
* Gets a grid-row element based on its id
* @param {string} id the target id
* @return {element|null} the matched grid-row element
*/
getRowById(id) {
let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
for(let row of rows) {
if(row.getAttribute('data-id') == id) return row;
}
}
/**
* Gets an array of all distinct values of a column
* @param {number} num The index of the column to be scanned (starting from 0)
* @return {array} An array of available values
*/
getColValues(col) {
let values = [];
this.dataset.querySelectorAll('.cell:nth-child(' + (col + 2) + ')').forEach( function(el) {
if(values.indexOf(el.innerText) == -1) values.push(el.innerText);
});
return values.sort();
}
/**
* Hides a row element
* @param {element} row the target grid-row element
*/
hideRow(row) { row.classList.add('hidden'); }
/**
* Shows a grid-row element
* @param {element} row the target grid-row element
*/
showRow(row) { row.classList.remove('hidden'); }
/**
* Shows the inline editor for adding a new row
*/
newRow() {
this.editing = true;
this.dataset.appendChild(this.buildEditor());
}
/**
* Shows the inline editor for editing an existing row
* @param {element} row the target grid-row element
*/
editRow(row) {
this.editing = true;
this.editedRow = row;
this.hideRow(row);
row.parentNode.insertBefore(this.buildEditor(row), row.nextSibling);
}
/**
* Validates and applies the inline editing
* @private
*/
updateRow() {
let values = this.dataset.querySelectorAll('.row.editor .cell input');
let cells = this.editedRow.querySelectorAll('.cell');
for(let i = 0; i < values.length; i++) {
$(values[i]).classList.remove('validation-failed')
if(values[i].value == '') {
$(values[i]).classList.add('validation-failed');
return;
}
}
for(let i = 0; i < values.length; i++) {
cells[i+1].innerHTML = values[i].value;
}
this.editing = false;
this.showRow(this.editedRow);
this.dataset.querySelector('.row.editor').remove();
}
/**
* Validates and adds the inline editing
*/
createRow() {
let fields = this.dataset.querySelectorAll('.row.editor .cell input');
let data = [];
for(let field of fields) {
field.classList.remove('validation-failed')
if(field.value != '') {
data.push(field.value);
} else {
field.classList.add('validation-failed');
return;
}
}
this.addRow('', data);
this.dataset.querySelector('.row.editor').remove();
this.enableEditing(this.rows().length < this.options.maxRows);
this.editing = false;
}
/**
* Adds a row to the content
* @param {string} id A unique identifier for the row (id value)
* @param {array} data An array of the column values. Item count must match column amount.
* @param {boolean} skipUpdate If set to true, avoids updating column filters.
* Consider using skipUpdate when adding a batch of rows, drastically increasing performances.
* Column filters can be updated afterwards suing the DataGrid.updateFilters() method.
* @return {element} The created grid-row element
*/
addRow(id, data, skipUpdate) {
let row = ui.create('<li class="row"></li>');
row.setAttribute('data-id', typeof id === 'object' ? JSON.stringify(id): id );
if(this.options.allowRowDrag) {
row.addEventListener('mousedown', this.onStartDragRow.bind(this));
}
// add selection cell
let cell = ui.create('<div class="cell"><input type="checkbox" /></div>');
cell.addEventListener('click', this.onCellClick.bind(this));
row.appendChild(cell);
for(let i = 0; i < data.length; i++) {
let cellAttr = '';
let cellFix = '';
let cellValue = data[i];
let cellTitle = ui.create(`<div>${cellValue}</div>`).innerText;
switch(this.options.headers[i].type) {
case 'currency':
cellAttr = 'currency';
cellFix = `<span style="font-size:0">${cellValue}</span>`;
cellValue = ui.format.currency(cellValue);
break;
case 'number':
cellAttr = 'number';
cellFix = `<span style="font-size:0">${cellValue}</span>`;
if(cellValue != '') cellValue = ui.format.number(cellValue);
break;
case 'date':
cellAttr = 'date';
cellFix = `<span style="font-size:0">${cellValue}</span>`
if(cellValue != '') cellValue = ui.format.date(cellValue);
break;
case 'dateTime':
cellAttr = 'date';
cellFix = `<span style="font-size:0">${cellValue}</span>`
if(cellValue != '') cellValue = ui.format.dateTime(cellValue);
break;
case 'time':
cellAttr = 'date';
cellFix = `<span style="font-size:0">${cellValue}</span>`
if(cellValue != '') cellValue = ui.format.time(cellValue);
break;
case 'markup':
cellTitle='';
break;
}
row.appendChild(ui.create(`<div ${cellAttr} class="cell" title="${cellTitle}">${cellFix}${cellValue}</div`));
}
let actions = ui.create(`<div class="cell actions"></div`);
if(this.options.rowActions && Array.isArray(this.options.rowActions)) {
for(let action of this.options.rowActions) {
const severityAttr = action.severity ? ` ${action.severity}` : ''
let button = ui.create(`<button eicbutton rounded${severityAttr} title="${action.title ? action.title: ''}">${action.icon}</button>`)
button.addEventListener('click', action.callback.bind(row.getAttribute('data-id') != '' ? JSON.parse(row.getAttribute('data-id')): this))
actions.appendChild(button)
}
}
row.appendChild(actions);
row.addEventListener('click', this.onRowClick.bind(this, row));
this.dataset.appendChild(row);
if(!skipUpdate) this.updateFilters();
this.updateFooter();
this.totalRows = this.dataset.querySelectorAll('.row').length;
this.activeRows = this.totalRows;
return row;
}
/**
* Adds a row to the content
* @param {number} idProperty Name of the id property in data
* @param {array} data An array of key-value objects.
* @param {boolean} skipUpdate If set to true, avoids updating column filters.
* Consider using skipUpdate when adding a batch of rows, drastically increasing performances.
* Column filters can be updated afterwards suing the DataGrid.updateFilters() method.
* @return {element} The grid
*/
addRows(idProperty, data, skipUpdate) {
let props = this.options.headers.map(obj=>obj.property);
let intersect;
for(let row of data){
intersect = props.reduce((acc, prop) => { acc.push(row[prop]); return(acc); } , []);
this.addRow(row[idProperty], intersect, skipUpdate)
}
return(this);
}
/**
* Removes a row from the content
* @param {element} row the target grid-row element
*
*/
removeRow(row) { row.remove(); }
/**
* Selects a row
* @param {element} row the target grid-row element
*/
selectRow(row) {
row.classList.add('selected');
row.querySelector('.cell:first-child input[type=checkbox]').setAttribute('checked', '');
}
/**
* Unselects a row
* @param {element} row the target grid-row element
*/
unselectRow(row) {
row.classList.remove('selected');
row.find('.cell:first-child input[type=checkbox]').setAttribute('checked', '');
}
/**
* Selects all visible rows
*/
selectAll() {
let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
for(let row of rows) this.selectRow(row);
}
/**
* Unselects all rows
*/
unselectAll() {
let rows = this.dataset.querySelectorAll('.row:not(.hidden)');
for(let row of rows) this.unselectRow(row);
}
/**
* Toggles all visible rows (based on first item value)
*/
toggleSelection() {
let setCheck = true;
let defined = false;
for(let row of this.rows) {
let cb = row.querySelector('input[type=checkbox]');
if(!row.classList.has('hidden')) {
// use first item value a reference
if(!defined) {
defined = true;
setCheck = !cb.checked;
}
cb.checked = setCheck;
} else {
cb.checked = false;
}
}
}
/**
* Gets all grid-row elements
* @return {array} An array of grid-row elements
*/
rows() { return this.dataset.querySelectorAll('.row'); }
/**
* Gets visible grid-row elements
* @return {array} An array of grid-row elements
*/
get filteredRows() { return this.dataset.querySelectorAll('.row:not(.hidden)'); }
/**
* Row click event handler default callback.
* Override this method with your own row click handler if needed.
* @param {event} event (callback event) The emitted mouse click event.
* @param {element} event.currentTarget The target grid-row element.
*/
onRowClick(event) { }
onRowSelect(row) { }
/**
* @private
*/
onRowEdit(event) {
event.preventDefault();
event.stopPropagation();
if(!this.editing) {
this.editRow(ui.queryParent(event.target, '.row'));
}
}
/**
* @private
*/
onRowUpdate(event) {
event.preventDefault();
event.stopPropagation();
this.updateRow();
}
/**
* @private
*/
onRowCreate(event) {
event.preventDefault();
event.stopPropagation();
this.createRow();
}
/**
* @private
*/
onRowAdd(event) {
event.preventDefault();
event.stopPropagation();
if(!this.btNew.classList.contains('disabled') && !this.editing) this.newRow();
}
/**
* @private
*/
onRowCancelEdit(event) {
event.preventDefault();
event.stopPropagation();
this.editing = false;
let row = ui.queryParent(event.target, '.row');
row.parentElement.removeChild(row);
this.showRow(this.editedRow);
this.editedRow = null;
}
/**
* @private
*/
onRowCancelCreate(event) {
event.preventDefault();
event.stopPropagation();
this.editing = false;
let row = ui.queryParent(event.target, '.row');
row.parentElement.removeChild(row);
}
/**
* @private
*/
onRowDelete(event) {
event.preventDefault();
event.stopPropagation();
if(this.editing) return;
let row = ui.queryParent(event.target, '.row');
row.parentElement.removeChild(row);
this.enableEditing(this.rows().length < this.options.maxRows);
}
/**
* @private
*/
onCellClick(event) {
event.stopPropagation();
if(this.editing) {
event.preventDefault();
return;
}
let row = ui.queryParent(event.target, '.row');
if(event.target.checked) {
this.selectRow(row);
this.onRowSelect(row);
} else {
this.unselectRow(row);
}
this.el.dispatchEvent(new Event('change'));
}
/**
* @private
*/
onColumnSort(event) {
if(this.editing) return;
let target = event.target.nodeName == 'SPAN' ? event.target: ui.queryParent(event.target, 'span');
let index = ui.childIndex(target.parentNode);
let order = target.getAttribute('data-order');
let number = target.classList.contains('numeric');
this.header.querySelectorAll('.cell > span').forEach(function(el) { el.classList.remove('active') }) ;
target.setAttribute('data-order', order == 'asc'? 'desc': 'asc' );
target.classList.add('active');
this.sort(index, order, number);
}
/**
* @private
*/
onGlobalClick(event) {
event.stopPropagation();
if(this.editing) return;
if(event.target.checked) {
this.selectAll();
} else {
this.unselectAll();
}
}
/**
* @private
*/
onStartDragRow(e) {
e.preventDefault();
if(this.editing) {
e.preventDefault();
return;
}
if(e.target.nodeName == 'A') return;
if(e.target.nodeName == 'I' && e.target.parentElement.nodeName == 'A') return;
if(e.target.nodeName == 'INPUT') return;
if(!this.rowInsert) {
this.rowInsert = ui.create('<div class="row insert"></div>');
ui.hide(this.rowInsert);
}
let row = e.currentTarget;
this.rowDrag = { row: row, pos: 0 };
this.rowDrag.row.style.position = 'fixed';
this.rowDrag.row.style.top = e.clientY;
this.rowDrag.row.style.left = e.clientX + 10;
document.addEventListener('mousemove',this.onMoveRow.bind(this));
document.addEventListener('mouseup',this.onEndDragRow.bind(this));
}
/**
* @private
*/
onMoveRow(e) {
if(this.editing) return;
this.rowDrag.row.style.top = e.clientY;
this.rowDrag.row.style.left = e.clientX + 10;
ui.hide(this.rowInsert);
// check if target is row
if(e.target.classList.has('cell')) {
let target = e.target.parentElement;
let h = e.target.clientHeight;
this.rowDrag.pos = e.offsetY;
ui.show(this.rowInsert);
if(this.rowDrag.pos > h / 2) {
this.rowInsert.insertAfter(target)
} else {
this.rowInsert.insertBefore(target)
}
}
}
/**
* @private
*/
onEndDragRow(e) {
if(this.editing) return;
$(document).off('mousemove');
$(document).off('mouseup');
this.rowDrag.row.css({
'position': 'relative',
'top': 'auto', 'left': 'auto'
});
this.rowInsert.hide();
// check if target is row
if(e.target.classList.has('cell')) {
let target = $(e.target).parent();
let h = e.target.clientHeight;
if(this.rowDrag.pos > h / 2) {
this.rowDrag.row.insertAfter(target);
this.el.trigger('moved', [this.rowDrag.row.getAttribute('data-id'), 'after', target.getAttribute('data-id')])
} else {
this.rowDrag.row.insertBefore(target);
this.el.trigger('moved', [this.rowDrag.row.getAttribute('data-id'), 'before', target.getAttribute('data-id')])
}
}
}
/**
*
*/
on(type, callback) { this.el.on(type, callback); }
}
class UserIcon extends EicComponent {
_online = false;
constructor(el, options) {
let defaultOptions = {
uuid: '',
fullname: '',
online: false,
severity: '',
showStatus: false
}
if(el && (el.getAttribute('data-eicui-id') != null)
&& (Object.keys(ui._components).includes(el.getAttribute('data-eicui-id')))) {
return(ui._components[el.getAttribute('data-eicui-id')])
}
super(el, {...defaultOptions, ...(options || {} )});
this.coat();
}
coat() {
if(!this.el) {
let capitals = this.options.fullname.split(' ');
if(capitals.length > 0) {
this.el = ui.create(`<button eicuser><span></span></button>`);
this.showStatus = this.options.showStatus;
this.online = this.options.online;
this.uuid = this.options.uuid;
this.fullname = this.options.fullname;
}
}
if(this.options && this.options.onclick) this.click = options.onclick;
this.el.addEventListener('click', this.onClick.bind(this));
}
get uuid() { return this.options.uuid; }
set uuid(value) {
this.options.uuid = value;
this.el.dataset.uuid = value;
}
get fullname() { return this.options.fullname; }
set fullname(value) {
let capitals = value.split(' ');
this.label = ''
if(capitals.length > 0) {
this.label = capitals[0][0] + (capitals.length > 1 ? capitals[1][0]: capitals[0][1]);
this.label = this.label.toUpperCase();
}
this.el.setAttribute('title', value);
this.el.querySelector('span').innerHTML = this.label;
this.options.fullname = value;
}
set showStatus(value) {
if(value) {
if(!this.badge) {
this.badge = new Badge(null);
this.el.append(this.badge.el);
}
this.badge.show()
} else {
//this.badge.hide()
}
}
get online() { return this._online; }
set online(status) {
this._online = status;
if(this.badge) {
this.badge.value = `<i class="${status ? 'icon-check': 'icon-cancel'}"></i>`
}
if(this._online) {
this.el.classList.add('online')
} else {
this.el.classList.remove('online')
}
}
onClick(event) {
event.stopPropagation();
event.preventDefault();
this.click(event);
}
click() {}
}
// Array.includes polyfill
if(!Array.prototype.includes) {
Array.prototype.includes = function(search){
return !!~this.indexOf(search);
}
}