UNPKG

w2ui

Version:

UI widgets for modern apps. Data table, forms, toolbars, sidebar, tabs, tooltips, popups. All under 120kb (gzipped).

1,255 lines (1,253 loc) 993 kB
/* w2ui 2.0.0 (4/26/2023, 10:40:17 AM) (c) http://w2ui.com, vitmalina@gmail.com */ /** * Part of w2ui 2.0 library * - Dependencies: w2utils * - on/off/trigger methods id not showing in help * - refactored with event object */ class w2event { constructor(owner, edata) { Object.assign(this, { type: edata.type ?? null, detail: edata, owner, target: edata.target ?? null, phase: edata.phase ?? 'before', object: edata.object ?? null, execute: null, isStopped: false, isCancelled: false, onComplete: null, listeners: [] }) delete edata.type delete edata.target delete edata.object this.complete = new Promise((resolve, reject) => { this._resolve = resolve this._reject = reject }) // needed empty catch function so that promise will not show error in the console this.complete.catch(() => {}) } finish(detail) { if (detail) { w2utils.extend(this.detail, detail) } this.phase = 'after' this.owner.trigger.call(this.owner, this) } done(func) { this.listeners.push(func) } preventDefault() { this._reject() this.isCancelled = true } stopPropagation() { this.isStopped = true } } class w2base { /** * Initializes base object for w2ui, registers it with w2ui object * * @param {string} name - name of the object * @returns */ constructor(name) { this.activeEvents = [] // events that are currently processing this.listeners = [] // event listeners // register globally if (typeof name !== 'undefined') { if (!w2utils.checkName(name)) return w2ui[name] = this } this.debug = false // if true, will trigger all events } /** * Adds event listener, supports event phase and event scoping * * @param {*} edata - an object or string, if string "eventName:phase.scope" * @param {*} handler * @returns itself */ on(events, handler) { if (typeof events == 'string') { events = events.split(/[,\s]+/) // separate by comma or space } else { events = [events] } events.forEach(edata => { let name = typeof edata == 'string' ? edata : (edata.type + ':' + edata.execute + '.' + edata.scope) if (typeof edata == 'string') { let [eventName, scope] = edata.split('.') let [type, execute] = eventName.replace(':complete', ':after').replace(':done', ':after').split(':') edata = { type, execute: execute ?? 'before', scope } } edata = w2utils.extend({ type: null, execute: 'before', onComplete: null }, edata) // errors if (!edata.type) { console.log('ERROR: You must specify event type when calling .on() method of '+ this.name); return } if (!handler) { console.log('ERROR: You must specify event handler function when calling .on() method of '+ this.name); return } if (!Array.isArray(this.listeners)) this.listeners = [] this.listeners.push({ name, edata, handler }) if (this.debug) { console.log('w2base: add event', { name, edata, handler }) } }) return this } /** * Removes event listener, supports event phase and event scoping * * @param {*} edata - an object or string, if string "eventName:phase.scope" * @param {*} handler * @returns itself */ off(events, handler) { if (typeof events == 'string') { events = events.split(/[,\s]+/) // separate by comma or space } else { events = [events] } events.forEach(edata => { let name = typeof edata == 'string' ? edata : (edata.type + ':' + edata.execute + '.' + edata.scope) if (typeof edata == 'string') { let [eventName, scope] = edata.split('.') let [type, execute] = eventName.replace(':complete', ':after').replace(':done', ':after').split(':') edata = { type: type || '*', execute: execute || '', scope: scope || '' } } edata = w2utils.extend({ type: null, execute: null, onComplete: null }, edata) // errors if (!edata.type && !edata.scope) { console.log('ERROR: You must specify event type when calling .off() method of '+ this.name); return } if (!handler) { handler = null } let count = 0 // remove listener this.listeners = this.listeners.filter(curr => { if ( (edata.type === '*' || edata.type === curr.edata.type) && (edata.execute === '' || edata.execute === curr.edata.execute) && (edata.scope === '' || edata.scope === curr.edata.scope) && (edata.handler == null || edata.handler === curr.edata.handler) ) { count++ // how many listeners removed return false } else { return true } }) if (this.debug) { console.log(`w2base: remove event (${count})`, { name, edata, handler }) } }) return this // needed for chaining } /** * Triggers even listeners for a specific event, loops through this.listeners * * @param {Object} edata - Object * @returns modified edata */ trigger(eventName, edata) { if (arguments.length == 1) { edata = eventName } else { edata.type = eventName edata.target = edata.target ?? this } if (w2utils.isPlainObject(edata) && edata.phase == 'after') { // find event edata = this.activeEvents.find(event => { if (event.type == edata.type && event.target == edata.target) { return true } return false }) if (!edata) { console.log(`ERROR: Cannot find even handler for "${edata.type}" on "${edata.target}".`) return } console.log('NOTICE: This syntax "edata.trigger({ phase: \'after\' })" is outdated. Use edata.finish() instead.') } else if (!(edata instanceof w2event)) { edata = new w2event(this, edata) this.activeEvents.push(edata) } let args, fun, tmp if (!Array.isArray(this.listeners)) this.listeners = [] if (this.debug) { console.log(`w2base: trigger "${edata.type}:${edata.phase}"`, edata) } // process events in REVERSE order for (let h = this.listeners.length-1; h >= 0; h--) { let item = this.listeners[h] if (item != null && (item.edata.type === edata.type || item.edata.type === '*') && (item.edata.target === edata.target || item.edata.target == null) && (item.edata.execute === edata.phase || item.edata.execute === '*' || item.edata.phase === '*')) { // add extra params if there Object.keys(item.edata).forEach(key => { if (edata[key] == null && item.edata[key] != null) { edata[key] = item.edata[key] } }) // check handler arguments args = [] tmp = new RegExp(/\((.*?)\)/).exec(String(item.handler).split('=>')[0]) if (tmp) args = tmp[1].split(/\s*,\s*/) if (args.length === 2) { item.handler.call(this, edata.target, edata) // old way for back compatibility if (this.debug) console.log(' - call (old)', item.handler) } else { item.handler.call(this, edata) // new way if (this.debug) console.log(' - call', item.handler) } if (edata.isStopped === true || edata.stop === true) return edata // back compatibility edata.stop === true } } // main object events let funName = 'on' + edata.type.substr(0,1).toUpperCase() + edata.type.substr(1) if (edata.phase === 'before' && typeof this[funName] === 'function') { fun = this[funName] // check handler arguments args = [] tmp = new RegExp(/\((.*?)\)/).exec(String(fun).split('=>')[0]) if (tmp) args = tmp[1].split(/\s*,\s*/) if (args.length === 2) { fun.call(this, edata.target, edata) // old way for back compatibility if (this.debug) console.log(' - call: on[Event] (old)', fun) } else { fun.call(this, edata) // new way if (this.debug) console.log(' - call: on[Event]', fun) } if (edata.isStopped === true || edata.stop === true) return edata // back compatibility edata.stop === true } // item object events if (edata.object != null && edata.phase === 'before' && typeof edata.object[funName] === 'function') { fun = edata.object[funName] // check handler arguments args = [] tmp = new RegExp(/\((.*?)\)/).exec(String(fun).split('=>')[0]) if (tmp) args = tmp[1].split(/\s*,\s*/) if (args.length === 2) { fun.call(this, edata.target, edata) // old way for back compatibility if (this.debug) console.log(' - call: edata.object (old)', fun) } else { fun.call(this, edata) // new way if (this.debug) console.log(' - call: edata.object', fun) } if (edata.isStopped === true || edata.stop === true) return edata } // execute onComplete if (edata.phase === 'after') { if (typeof edata.onComplete === 'function') edata.onComplete.call(this, edata) for (let i = 0; i < edata.listeners.length; i++) { if (typeof edata.listeners[i] === 'function') { edata.listeners[i].call(this, edata) if (this.debug) console.log(' - call: done', fun) } } edata._resolve(edata) if (this.debug) { console.log(`w2base: trigger "${edata.type}:${edata.phase}"`, edata) } } return edata } } /** * Part of w2ui 2.0 library * - Dependencies: none * * These are the master locale settings that will be used by w2utils * * "locale" should be the IETF language tag in the form xx-YY, * where xx is the ISO 639-1 language code ( see https://en.wikipedia.org/wiki/ISO_639-1 ) and * YY is the ISO 3166-1 alpha-2 country code ( see https://en.wikipedia.org/wiki/ISO_3166-2 ) */ const w2locale = { 'locale' : 'en-US', 'dateFormat' : 'm/d/yyyy', 'timeFormat' : 'hh:mi pm', 'datetimeFormat' : 'm/d/yyyy|hh:mi pm', 'currencyPrefix' : '$', 'currencySuffix' : '', 'currencyPrecision' : 2, 'groupSymbol' : ',', // aka "thousands separator" 'decimalSymbol' : '.', 'shortmonths' : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 'fullmonths' : ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], 'shortdays' : ['M', 'T', 'W', 'T', 'F', 'S', 'S'], 'fulldays' : ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], 'weekStarts' : 'S', // can be "M" for Monday or "S" for Sunday // phrases used in w2ui, should be empty for original language // keep these up-to-date and in sorted order // value = "---" to easier see what to translate 'phrases': { '${count} letters or more...': '---', 'Add new record': '---', 'Add New': '---', 'Advanced Search': '---', 'after': '---', 'AJAX error. See console for more details.': '---', 'All Fields': '---', 'All': '---', 'Any': '---', 'Are you sure you want to delete ${count} ${records}?': '---', 'Attach files by dragging and dropping or Click to Select': '---', 'before': '---', 'begins with': '---', 'begins': '---', 'between': '---', 'buffered': '---', 'Cancel': '---', 'Close': '---', 'Column': '---', 'Confirmation': '---', 'contains': '---', 'Copied': '---', 'Copy to clipboard': '---', 'Current Date & Time': '---', 'Delete selected records': '---', 'Delete': '---', 'Do you want to delete search item "${item}"?': '---', 'Edit selected record': '---', 'Edit': '---', 'Empty list': '---', 'ends with': '---', 'ends': '---', 'Field should be at least ${count} characters.': '---', 'Hide': '---', 'in': '---', 'is not': '---', 'is': '---', 'less than': '---', 'Line #': '---', 'Load ${count} more...': '---', 'Loading...': '---', 'Maximum number of files is ${count}': '---', 'Maximum total size is ${count}': '---', 'Modified': '---', 'more than': '---', 'Multiple Fields': '---', 'Name': '---', 'No items found': '---', 'No matches': '---', 'No': '---', 'none': '---', 'Not a float': '---', 'Not a hex number': '---', 'Not a valid date': '---', 'Not a valid email': '---', 'Not alpha-numeric': '---', 'Not an integer': '---', 'Not in money format': '---', 'not in': '---', 'Notification': '---', 'of': '---', 'Ok': '---', 'Opacity': '---', 'Record ID': '---', 'record': '---', 'records': '---', 'Refreshing...': '---', 'Reload data in the list': '---', 'Remove': '---', 'Remove This Field': '---', 'Request aborted.': '---', 'Required field': '---', 'Reset': '---', 'Restore Default State': '---', 'Returned data is not in valid JSON format.': '---', 'Save changed records': '---', 'Save Grid State': '---', 'Save': '---', 'Saved Searches': '---', 'Saving...': '---', 'Search took ${count} seconds': '---', 'Search': '---', 'Select Hour': '---', 'Select Minute': '---', 'selected': '---', 'Server Response ${count} seconds': '---', 'Show/hide columns': '---', 'Show': '---', 'Size': '---', 'Skip': '---', 'Sorting took ${count} seconds': '---', 'Type to search...': '---', 'Type': '---', 'Yes': '---', 'Yesterday': '---', 'Your remote data source record count has changed, reloading from the first record.': '---' } } /* mQuery 0.7 (nightly) (10/10/2022, 11:30:36 AM), vitmalina@gmail.com */ class Query { static version = 0.7 constructor(selector, context, previous) { this.context = context ?? document this.previous = previous ?? null let nodes = [] if (Array.isArray(selector)) { nodes = selector } else if (selector instanceof Node || selector instanceof Window) { // any html element or Window nodes = [selector] } else if (selector instanceof Query) { nodes = selector.nodes } else if (typeof selector == 'string') { if (typeof this.context.querySelector != 'function') { throw new Error('Invalid context') } nodes = Array.from(this.context.querySelectorAll(selector)) } else if (selector == null) { nodes = [] } else { // if selector is itterable, then try to create nodes from it, also supports jQuery let arr = Array.from(selector ?? []) if (typeof selector == 'object' && Array.isArray(arr)) { nodes = arr } else { throw new Error(`Invalid selector "${selector}"`) } } this.nodes = nodes this.length = nodes.length // map nodes to object propoerties this.each((node, ind) => { this[ind] = node }) } static _fragment(html) { let tmpl = document.createElement('template') tmpl.innerHTML = html tmpl.content.childNodes.forEach(node => { let newNode = Query._scriptConvert(node) if (newNode != node) { tmpl.content.replaceChild(newNode, node) } }) return tmpl.content } // innerHTML, append, etc. script tags will not be executed unless they are proper script tags static _scriptConvert(node) { let convert = (txtNode) => { let doc = txtNode.ownerDocument let scNode = doc.createElement('script') scNode.text = txtNode.text let attrs = txtNode.attributes for (let i = 0; i < attrs.length; i++) { scNode.setAttribute(attrs[i].name, attrs[i].value) } return scNode } if (node.tagName == 'SCRIPT') { node = convert(node) } if (node.querySelectorAll) { node.querySelectorAll('script').forEach(textNode => { textNode.parentNode.replaceChild(convert(textNode), textNode) }) } return node } static _fixProp(name) { let fixes = { cellpadding: 'cellPadding', cellspacing: 'cellSpacing', class: 'className', colspan: 'colSpan', contenteditable: 'contentEditable', for: 'htmlFor', frameborder: 'frameBorder', maxlength: 'maxLength', readonly: 'readOnly', rowspan: 'rowSpan', tabindex: 'tabIndex', usemap: 'useMap' } return fixes[name] ? fixes[name] : name } _insert(method, html) { let nodes = [] let len = this.length if (len < 1) return let self = this // TODO: need good unit test coverage for this function if (typeof html == 'string') { this.each(node => { let clone = Query._fragment(html) nodes.push(...clone.childNodes) node[method](clone) }) } else if (html instanceof Query) { let single = (len == 1) // if inserting into a single container, then move it there html.each(el => { this.each(node => { // if insert before a single node, just move new one, else clone and move it let clone = (single ? el : el.cloneNode(true)) nodes.push(clone) node[method](clone) Query._scriptConvert(clone) }) }) if (!single) html.remove() } else if (html instanceof Node) { // any HTML element this.each(node => { // if insert before a single node, just move new one, else clone and move it let clone = (len === 1 ? html : Query._fragment(html.outerHTML)) nodes.push(...(len === 1 ? [html] : clone.childNodes)) node[method](clone) }) if (len > 1) html.remove() } else { throw new Error(`Incorrect argument for "${method}(html)". It expects one string argument.`) } if (method == 'replaceWith') { self = new Query(nodes, this.context, this) // must return a new collection } return self } _save(node, name, value) { node._mQuery = node._mQuery ?? {} if (Array.isArray(value)) { node._mQuery[name] = node._mQuery[name] ?? [] node._mQuery[name].push(...value) } else if (value != null) { node._mQuery[name] = value } else { delete node._mQuery[name] } } get(index) { if (index < 0) index = this.length + index let node = this[index] if (node) { return node } if (index != null) { return null } return this.nodes } eq(index) { if (index < 0) index = this.length + index let nodes = [this[index]] if (nodes[0] == null) nodes = [] return new Query(nodes, this.context, this) // must return a new collection } then(fun) { let ret = fun(this) return ret != null ? ret : this } find(selector) { let nodes = [] this.each(node => { let nn = Array.from(node.querySelectorAll(selector)) if (nn.length > 0) { nodes.push(...nn) } }) return new Query(nodes, this.context, this) // must return a new collection } filter(selector) { let nodes = [] this.each(node => { if (node === selector || (typeof selector == 'string' && node.matches && node.matches(selector)) || (typeof selector == 'function' && selector(node)) ) { nodes.push(node) } }) return new Query(nodes, this.context, this) // must return a new collection } next() { let nodes = [] this.each(node => { let nn = node.nextElementSibling if (nn) { nodes.push(nn) } }) return new Query(nodes, this.context, this) // must return a new collection } prev() { let nodes = [] this.each(node => { let nn = node.previousElementSibling if (nn) { nodes.push(nn)} }) return new Query(nodes, this.context, this) // must return a new collection } shadow(selector) { let nodes = [] this.each(node => { // select shadow root if available if (node.shadowRoot) nodes.push(node.shadowRoot) }) let col = new Query(nodes, this.context, this) return selector ? col.find(selector) : col } closest(selector) { let nodes = [] this.each(node => { let nn = node.closest(selector) if (nn) { nodes.push(nn) } }) return new Query(nodes, this.context, this) // must return a new collection } host(all) { let nodes = [] // find shadow root or body let top = (node) => { if (node.parentNode) { return top(node.parentNode) } else { return node } } let fun = (node) => { let nn = top(node) nodes.push(nn.host ? nn.host : nn) if (nn.host && all) fun(nn.host) } this.each(node => { fun(node) }) return new Query(nodes, this.context, this) // must return a new collection } parent(selector) { return this.parents(selector, true) } parents(selector, firstOnly) { let nodes = [] let add = (node) => { if (nodes.indexOf(node) == -1) { nodes.push(node) } if (!firstOnly && node.parentNode) { return add(node.parentNode) } } this.each(node => { if (node.parentNode) add(node.parentNode) }) let col = new Query(nodes, this.context, this) return selector ? col.filter(selector) : col } add(more) { let nodes = more instanceof Query ? more.nodes : (Array.isArray(more) ? more : [more]) return new Query(this.nodes.concat(nodes), this.context, this) // must return a new collection } each(func) { this.nodes.forEach((node, ind) => { func(node, ind, this) }) return this } append(html) { return this._insert('append', html) } prepend(html) { return this._insert('prepend', html) } after(html) { return this._insert('after', html) } before(html) { return this._insert('before', html) } replace(html) { return this._insert('replaceWith', html) } remove() { // remove from dom, but keep in current query this.each(node => { node.remove() }) return this } css(key, value) { let css = key let len = arguments.length if (len === 0 || (len === 1 && typeof key == 'string')) { if (this[0]) { let st = this[0].style // do not do computedStyleMap as it is not what on immediate element if (typeof key == 'string') { let pri = st.getPropertyPriority(key) return st.getPropertyValue(key) + (pri ? '!' + pri : '') } else { return Object.fromEntries( this[0].style.cssText .split(';') .filter(a => !!a) // filter non-empty .map(a => { return a.split(':').map(a => a.trim()) // trim strings }) ) } } else { return undefined } } else { if (typeof key != 'object') { css = {} css[key] = value } this.each((el, ind) => { Object.keys(css).forEach(key => { let imp = String(css[key]).toLowerCase().includes('!important') ? 'important' : '' el.style.setProperty(key, String(css[key]).replace(/\!important/i, ''), imp) }) }) return this } } addClass(classes) { this.toggleClass(classes, true) return this } removeClass(classes) { this.toggleClass(classes, false) return this } toggleClass(classes, force) { // split by comma or space if (typeof classes == 'string') classes = classes.split(/[,\s]+/) this.each(node => { let classes2 = classes // if not defined, remove all classes if (classes2 == null && force === false) classes2 = Array.from(node.classList) classes2.forEach(className => { if (className !== '') { let act = 'toggle' if (force != null) act = force ? 'add' : 'remove' node.classList[act](className) } }) }) return this } hasClass(classes) { // split by comma or space if (typeof classes == 'string') classes = classes.split(/[,\s]+/) if (classes == null && this.length > 0) { return Array.from(this[0].classList) } let ret = false this.each(node => { ret = ret || classes.every(className => { return Array.from(node.classList ?? []).includes(className) }) }) return ret } on(events, options, callback) { if (typeof options == 'function') { callback = options options = undefined } let delegate if (options?.delegate) { delegate = options.delegate delete options.delegate // not to pass to addEventListener } events = events.split(/[,\s]+/) // separate by comma or space events.forEach(eventName => { let [ event, scope ] = String(eventName).toLowerCase().split('.') if (delegate) { let fun = callback callback = (event) => { // event.target or any ancestors match delegate selector let parent = query(event.target).parents(delegate) if (parent.length > 0) { event.delegate = parent[0] } else { event.delegate = event.target } if (event.target.matches(delegate) || parent.length > 0) { fun(event) } } } this.each(node => { this._save(node, 'events', [{ event, scope, callback, options }]) node.addEventListener(event, callback, options) }) }) return this } off(events, options, callback) { if (typeof options == 'function') { callback = options options = undefined } events = (events ?? '').split(/[,\s]+/) // separate by comma or space events.forEach(eventName => { let [ event, scope ] = String(eventName).toLowerCase().split('.') this.each(node => { if (Array.isArray(node._mQuery?.events)) { for (let i = node._mQuery.events.length - 1; i >= 0; i--) { let evt = node._mQuery.events[i] if (scope == null || scope === '') { // if no scope, has to be exact match if ((evt.event == event || event === '') && (evt.callback == callback || callback == null)) { node.removeEventListener(evt.event, evt.callback, evt.options) node._mQuery.events.splice(i, 1) } } else { if ((evt.event == event || event === '') && evt.scope == scope) { node.removeEventListener(evt.event, evt.callback, evt.options) node._mQuery.events.splice(i, 1) } } } } }) }) return this } trigger(name, options) { let event, mevent = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove'], kevent = ['keydown', 'keyup', 'keypress'] if (name instanceof Event || name instanceof CustomEvent) { // MouseEvent and KeyboardEvent are instances of Event, no need to explicitly add event = name } else if (mevent.includes(name)) { event = new MouseEvent(name, options) } else if (kevent.includes(name)) { event = new KeyboardEvent(name, options) } else { event = new Event(name, options) } this.each(node => { node.dispatchEvent(event) }) return this } attr(name, value) { if (value === undefined && typeof name == 'string') { return this[0] ? this[0].getAttribute(name) : undefined } else { let obj = {} if (typeof name == 'object') obj = name; else obj[name] = value this.each(node => { Object.entries(obj).forEach(([nm, val]) => { node.setAttribute(nm, val) }) }) return this } } removeAttr() { this.each(node => { Array.from(arguments).forEach(attr => { node.removeAttribute(attr) }) }) return this } prop(name, value) { if (value === undefined && typeof name == 'string') { return this[0] ? this[0][name] : undefined } else { let obj = {} if (typeof name == 'object') obj = name; else obj[name] = value this.each(node => { Object.entries(obj).forEach(([nm, val]) => { let prop = Query._fixProp(nm) node[prop] = val if (prop == 'innerHTML') { Query._scriptConvert(node) } }) }) return this } } removeProp() { this.each(node => { Array.from(arguments).forEach(prop => { delete node[Query._fixProp(prop)] }) }) return this } data(key, value) { if (key instanceof Object) { Object.entries(key).forEach(item => { this.data(item[0], item[1]) }) return } if (key && key.indexOf('-') != -1) { console.error(`Key "${key}" contains "-" (dash). Dashes are not allowed in property names. Use camelCase instead.`) } if (arguments.length < 2) { if (this[0]) { let data = Object.assign({}, this[0].dataset) Object.keys(data).forEach(key => { if (data[key].startsWith('[') || data[key].startsWith('{')) { try { data[key] = JSON.parse(data[key]) } catch (e) {} } }) return key ? data[key] : data } else { return undefined } } else { this.each(node => { if (value != null) { node.dataset[key] = value instanceof Object ? JSON.stringify(value) : value } else { delete node.dataset[key] } }) return this } } removeData(key) { if (typeof key == 'string') key = key.split(/[,\s]+/) this.each(node => { key.forEach(k => { delete node.dataset[k] }) }) return this } show() { return this.toggle(true) } hide() { return this.toggle(false) } toggle(force) { return this.each(node => { let prev = node.style.display let dsp = getComputedStyle(node).display let isHidden = (prev == 'none' || dsp == 'none') if (isHidden && (force == null || force === true)) { // show let def = node instanceof HTMLTableRowElement ? 'table-row' : node instanceof HTMLTableCellElement ? 'table-cell' : 'block' node.style.display = node._mQuery?.prevDisplay ?? (prev == dsp && dsp != 'none' ? '' : def) this._save(node, 'prevDisplay', null) } if (!isHidden && (force == null || force === false)) { // hide if (dsp != 'none') this._save(node, 'prevDisplay', dsp) node.style.setProperty('display', 'none') } }) } empty() { return this.html('') } html(html) { return this.prop('innerHTML', html) } text(text) { return this.prop('textContent', text) } val(value) { return this.prop('value', value) // must be prop } change() { return this.trigger('change') } click() { return this.trigger('click') } } // create a new object each time let query = function (selector, context) { // if a function, use as onload event if (typeof selector == 'function') { if (document.readyState == 'complete') { selector() } else { window.addEventListener('load', selector) } } else { return new Query(selector, context) } } // str -> doc-fragment query.html = (str) => { let frag = Query._fragment(str); return query(frag.children, frag) } query.version = Query.version /** * Part of w2ui 2.0 library * - Dependencies: mQuery, w2utils, w2base, w2locale * * == TODO == * - add w2utils.lang wrap for all captions in all buttons. * - check transition (also with layout) * - deprecate w2utils.tooltip * * == 2.0 changes * - CSP - fixed inline events (w2utils.tooltip still has it) * - transition returns a promise * - removed jQuery * - refactores w2utils.message() * - added w2utils.confirm() * - added isPlainObject * - added stripSpaces * - implemented marker * - cssPrefix - deprecated * - w2utils.debounce * - w2utils.prepareParams */ // variable that holds all w2ui objects let w2ui = {} class Utils { constructor () { this.version = '2.0.x' this.tmp = {} this.settings = this.extend({}, { 'dataType' : 'HTTPJSON', // can be HTTP, HTTPJSON, RESTFULL, JSON (case sensitive) 'dateStartYear' : 1950, // start year for date-picker 'dateEndYear' : 2030, // end year for date picker 'macButtonOrder' : false, // if true, Yes on the right side 'warnNoPhrase' : false, // call console.warn if lang() encounters a missing phrase }, w2locale, { phrases: null }), // if there are no phrases, then it is original language this.i18nCompare = Intl.Collator().compare this.hasLocalStorage = testLocalStorage() // some internal variables this.isMac = /Mac/i.test(navigator.platform) this.isMobile = /(iphone|ipod|ipad|mobile|android)/i.test(navigator.userAgent) this.isIOS = /(iphone|ipod|ipad)/i.test(navigator.platform) this.isAndroid = /(android)/i.test(navigator.userAgent) this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) // Formatters: Primarily used in grid this.formatters = { 'number'(value, params) { if (parseInt(params) > 20) params = 20 if (parseInt(params) < 0) params = 0 if (value == null || value === '') return '' return w2utils.formatNumber(parseFloat(value), params, true) }, 'float'(value, params) { return w2utils.formatters.number(value, params) }, 'int'(value, params) { return w2utils.formatters.number(value, 0) }, 'money'(value, params) { if (value == null || value === '') return '' let data = w2utils.formatNumber(Number(value), w2utils.settings.currencyPrecision) return (w2utils.settings.currencyPrefix || '') + data + (w2utils.settings.currencySuffix || '') }, 'currency'(value, params) { return w2utils.formatters.money(value, params) }, 'percent'(value, params) { if (value == null || value === '') return '' return w2utils.formatNumber(value, params || 1) + '%' }, 'size'(value, params) { if (value == null || value === '') return '' return w2utils.formatSize(parseInt(value)) }, 'date'(value, params) { if (params === '') params = w2utils.settings.dateFormat if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, params, true) if (dt === false) dt = w2utils.isDate(value, params, true) return '<span title="'+ dt +'">' + w2utils.formatDate(dt, params) + '</span>' }, 'datetime'(value, params) { if (params === '') params = w2utils.settings.datetimeFormat if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, params, true) if (dt === false) dt = w2utils.isDate(value, params, true) return '<span title="'+ dt +'">' + w2utils.formatDateTime(dt, params) + '</span>' }, 'time'(value, params) { if (params === '') params = w2utils.settings.timeFormat if (params === 'h12') params = 'hh:mi pm' if (params === 'h24') params = 'h24:mi' if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, params, true) if (dt === false) dt = w2utils.isDate(value, params, true) return '<span title="'+ dt +'">' + w2utils.formatTime(value, params) + '</span>' }, 'timestamp'(value, params) { if (params === '') params = w2utils.settings.datetimeFormat if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, params, true) if (dt === false) dt = w2utils.isDate(value, params, true) return dt.toString ? dt.toString() : '' }, 'gmt'(value, params) { if (params === '') params = w2utils.settings.datetimeFormat if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, params, true) if (dt === false) dt = w2utils.isDate(value, params, true) return dt.toUTCString ? dt.toUTCString() : '' }, 'age'(value, params) { if (value == null || value === 0 || value === '') return '' let dt = w2utils.isDateTime(value, null, true) if (dt === false) dt = w2utils.isDate(value, null, true) return '<span title="'+ dt +'">' + w2utils.age(value) + (params ? (' ' + params) : '') + '</span>' }, 'interval'(value, params) { if (value == null || value === 0 || value === '') return '' return w2utils.interval(value) + (params ? (' ' + params) : '') }, 'toggle'(value, params) { return (value ? 'Yes' : '') }, 'password'(value, params) { let ret = '' for (let i = 0; i < value.length; i++) { ret += '*' } return ret } } return function testLocalStorage() { // test if localStorage is available, see issue #1282 let str = 'w2ui_test' try { localStorage.setItem(str, str) localStorage.removeItem(str) return true } catch (e) { return false } } } isBin(val) { let re = /^[0-1]+$/ return re.test(val) } isInt(val) { let re = /^[-+]?[0-9]+$/ return re.test(val) } isFloat(val) { if (typeof val === 'string') { val = val.replace(this.settings.groupSymbol, '') .replace(this.settings.decimalSymbol, '.') } return (typeof val === 'number' || (typeof val === 'string' && val !== '')) && !isNaN(Number(val)) } isMoney(val) { if (typeof val === 'object' || val === '') return false if (this.isFloat(val)) return true let se = this.settings let re = new RegExp('^'+ (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') + '[-+]?'+ (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') + '[0-9]*[\\'+ se.decimalSymbol +']?[0-9]+'+ (se.currencySuffix ? '\\' + se.currencySuffix + '?' : '') +'$', 'i') if (typeof val === 'string') { val = val.replace(new RegExp(se.groupSymbol, 'g'), '') } return re.test(val) } isHex(val) { let re = /^(0x)?[0-9a-fA-F]+$/ return re.test(val) } isAlphaNumeric(val) { let re = /^[a-zA-Z0-9_-]+$/ return re.test(val) } isEmail(val) { let email = /^[a-zA-Z0-9._%\-+]+@[а-яА-Яa-zA-Z0-9.-]+\.[а-яА-Яa-zA-Z]+$/ return email.test(val) } isIpAddress(val) { let re = new RegExp('^' + '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}' + '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' + '$') return re.test(val) } isDate(val, format, retDate) { if (!val) return false let dt = 'Invalid Date' let month, day, year if (format == null) format = this.settings.dateFormat if (typeof val.getFullYear === 'function') { // date object year = val.getFullYear() month = val.getMonth() + 1 day = val.getDate() } else if (parseInt(val) == val && parseInt(val) > 0) { val = new Date(parseInt(val)) year = val.getFullYear() month = val.getMonth() + 1 day = val.getDate() } else { val = String(val) // convert month formats if (new RegExp('mon', 'ig').test(format)) { format = format.replace(/month/ig, 'm').replace(/mon/ig, 'm').replace(/dd/ig, 'd').replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase() val = val.replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase() for (let m = 0, len = this.settings.fullmonths.length; m < len; m++) { let t = this.settings.fullmonths[m] val = val.replace(new RegExp(t, 'ig'), (parseInt(m) + 1)).replace(new RegExp(t.substr(0, 3), 'ig'), (parseInt(m) + 1)) } } // format date let tmp = val.replace(/-/g, '/').replace(/\./g, '/').toLowerCase().split('/') let tmp2 = format.replace(/-/g, '/').replace(/\./g, '/').toLowerCase() if (tmp2 === 'mm/dd/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } if (tmp2 === 'm/d/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } if (tmp2 === 'dd/mm/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] } if (tmp2 === 'd/m/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] } if (tmp2 === 'yyyy/dd/mm') { month = tmp[2]; day = tmp[1]; year = tmp[0] } if (tmp2 === 'yyyy/d/m') { month = tmp[2]; day = tmp[1]; year = tmp[0] } if (tmp2 === 'yyyy/mm/dd') { month = tmp[1]; day = tmp[2]; year = tmp[0] } if (tmp2 === 'yyyy/m/d') { month = tmp[1]; day = tmp[2]; year = tmp[0] } if (tmp2 === 'mm/dd/yy') { month = tmp[0]; day = tmp[1]; year = tmp[2] } if (tmp2 === 'm/d/yy') { month = tmp[0]; day = tmp[1]; year = parseInt(tmp[2]) + 1900 } if (tmp2 === 'dd/mm/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 } if (tmp2 === 'd/m/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 } if (tmp2 === 'yy/dd/mm') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 } if (tmp2 === 'yy/d/m') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 } if (tmp2 === 'yy/mm/dd') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 } if (tmp2 === 'yy/m/d') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 } } if (!this.isInt(year)) return false if (!this.isInt(month)) return false if (!this.isInt(day)) return false year = +year month = +month day = +day dt = new Date(year, month - 1, day) dt.setFullYear(year) // do checks if (month == null) return false if (String(dt) === 'Invalid Date') return false if ((dt.getMonth() + 1 !== month) || (dt.getDate() !== day) || (dt.getFullYear() !== year)) return false if (retDate === true) return dt; else return true } isTime(val, retTime) { // Both formats 10:20pm and 22:20 if (val == null) return false let max, am, pm // -- process american format val = String(val) val = val.toUpperCase() am = val.indexOf('AM') >= 0 pm = val.indexOf('PM') >= 0 let ampm = (pm || am) if (ampm) max = 12; else max = 24 val = val.replace('AM', '').replace('PM', '').trim() // --- let tmp = val.split(':') let h = parseInt(tmp[0] || 0), m = parseInt(tmp[1] || 0), s = parseInt(tmp[2] || 0) // accept edge case: 3PM is a good timestamp, but 3 (without AM or PM) is NOT: if ((!ampm || tmp.length !== 1) && tmp.length !== 2 && tmp.length !== 3) { return false } if (tmp[0] === '' || h < 0 || h > max || !this.isInt(tmp[0]) || tmp[0].length > 2) { return false } if (tmp.length > 1 && (tmp[1] === '' || m < 0 || m > 59 || !this.isInt(tmp[1]) || tmp[1].length !== 2)) { return false } if (tmp.length > 2 && (tmp[2] === '' || s < 0 || s > 59 || !this.isInt(tmp[2]) || tmp[2].length !== 2)) { return false } // check the edge cases: 12:01AM is ok, as is 12:01PM, but 24:01 is NOT ok while 24:00 is (midnight; equivalent to 00:00). // meanwhile, there is 00:00 which is ok, but 0AM nor 0PM are okay, while 0:01AM and 0:00AM are. if (!ampm && max === h && (m !== 0 || s !== 0)) { return false } if (ampm && tmp.length === 1 && h === 0) { return false } if (retTime === true) { if (pm && h !== 12) h += 12 // 12:00pm - is noon if (am && h === 12) h += 12 // 12:00am - is midnight return { hours: h, minutes: m, seconds: s } } return true } isDateTime(val, format, retDate) { if (typeof val.getFullYear === 'function') { // date object if (retDate !== true) return true return val } let intVal = parseInt(val) if (intVal === val) { if (intVal < 0) re