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
JavaScript
/* 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