3835 lines
122 KiB
JavaScript
Executable File
3835 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,'"'): ''}" /></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) {
|
|
let button = ui.create(`<button eicbutton rounded 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);
|
|
}
|
|
}
|