22349 lines
No EOL
981 KiB
JavaScript
22349 lines
No EOL
981 KiB
JavaScript
/* w2ui 2.0.x (nightly) (1/16/2023, 8:50:38 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
|
||
node.style.display = node._mQuery?.prevDisplay ?? (prev == dsp && dsp != 'none' ? '' : 'block')
|
||
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
|
||
*/
|
||
|
||
// 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) return false
|
||
else if (retDate !== true) return true
|
||
else return new Date(intVal)
|
||
}
|
||
let tmp = String(val).indexOf(' ')
|
||
if (tmp < 0) {
|
||
if (String(val).indexOf('T') < 0 || String(new Date(val)) == 'Invalid Date') return false
|
||
else if (retDate !== true) return true
|
||
else return new Date(val)
|
||
} else {
|
||
if (format == null) format = this.settings.datetimeFormat
|
||
let formats = format.split('|')
|
||
let values = [val.substr(0, tmp), val.substr(tmp).trim()]
|
||
formats[0] = formats[0].trim()
|
||
if (formats[1]) formats[1] = formats[1].trim()
|
||
// check
|
||
let tmp1 = this.isDate(values[0], formats[0], true)
|
||
let tmp2 = this.isTime(values[1], true)
|
||
if (tmp1 !== false && tmp2 !== false) {
|
||
if (retDate !== true) return true
|
||
tmp1.setHours(tmp2.hours)
|
||
tmp1.setMinutes(tmp2.minutes)
|
||
tmp1.setSeconds(tmp2.seconds)
|
||
return tmp1
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
age(dateStr) {
|
||
let d1
|
||
if (dateStr === '' || dateStr == null) return ''
|
||
if (typeof dateStr.getFullYear === 'function') { // date object
|
||
d1 = dateStr
|
||
} else if (parseInt(dateStr) == dateStr && parseInt(dateStr) > 0) {
|
||
d1 = new Date(parseInt(dateStr))
|
||
} else {
|
||
d1 = new Date(dateStr)
|
||
}
|
||
if (String(d1) === 'Invalid Date') return ''
|
||
let d2 = new Date()
|
||
let sec = (d2.getTime() - d1.getTime()) / 1000
|
||
let amount = ''
|
||
let type = ''
|
||
if (sec < 0) {
|
||
amount = 0
|
||
type = 'sec'
|
||
} else if (sec < 60) {
|
||
amount = Math.floor(sec)
|
||
type = 'sec'
|
||
if (sec < 0) { amount = 0; type = 'sec' }
|
||
} else if (sec < 60*60) {
|
||
amount = Math.floor(sec/60)
|
||
type = 'min'
|
||
} else if (sec < 24*60*60) {
|
||
amount = Math.floor(sec/60/60)
|
||
type = 'hour'
|
||
} else if (sec < 30*24*60*60) {
|
||
amount = Math.floor(sec/24/60/60)
|
||
type = 'day'
|
||
} else if (sec < 365*24*60*60) {
|
||
amount = Math.floor(sec/30/24/60/60*10)/10
|
||
type = 'month'
|
||
} else if (sec < 365*4*24*60*60) {
|
||
amount = Math.floor(sec/365/24/60/60*10)/10
|
||
type = 'year'
|
||
} else if (sec >= 365*4*24*60*60) {
|
||
// factor in leap year shift (only older then 4 years)
|
||
amount = Math.floor(sec/365.25/24/60/60*10)/10
|
||
type = 'year'
|
||
}
|
||
return amount + ' ' + type + (amount > 1 ? 's' : '')
|
||
}
|
||
interval(value) {
|
||
let ret = ''
|
||
if (value < 100) {
|
||
ret = '< 0.01 sec'
|
||
} else if (value < 1000) {
|
||
ret = (Math.floor(value / 10) / 100) + ' sec'
|
||
} else if (value < 10000) {
|
||
ret = (Math.floor(value / 100) / 10) + ' sec'
|
||
} else if (value < 60000) {
|
||
ret = Math.floor(value / 1000) + ' secs'
|
||
} else if (value < 3600000) {
|
||
ret = Math.floor(value / 60000) + ' mins'
|
||
} else if (value < 86400000) {
|
||
ret = Math.floor(value / 3600000 * 10) / 10 + ' hours'
|
||
} else if (value < 2628000000) {
|
||
ret = Math.floor(value / 86400000 * 10) / 10 + ' days'
|
||
} else if (value < 3.1536e+10) {
|
||
ret = Math.floor(value / 2628000000 * 10) / 10 + ' months'
|
||
} else {
|
||
ret = Math.floor(value / 3.1536e+9) / 10 + ' years'
|
||
}
|
||
return ret
|
||
}
|
||
date(dateStr) {
|
||
if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
|
||
let d1 = new Date(dateStr)
|
||
if (this.isInt(dateStr)) d1 = new Date(Number(dateStr)) // for unix timestamps
|
||
if (String(d1) === 'Invalid Date') return ''
|
||
let months = this.settings.shortmonths
|
||
let d2 = new Date() // today
|
||
let d3 = new Date()
|
||
d3.setTime(d3.getTime() - 86400000) // yesterday
|
||
let dd1 = months[d1.getMonth()] + ' ' + d1.getDate() + ', ' + d1.getFullYear()
|
||
let dd2 = months[d2.getMonth()] + ' ' + d2.getDate() + ', ' + d2.getFullYear()
|
||
let dd3 = months[d3.getMonth()] + ' ' + d3.getDate() + ', ' + d3.getFullYear()
|
||
let time = (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am')
|
||
let time2 = (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ':' + (d1.getSeconds() < 10 ? '0' : '') + d1.getSeconds() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am')
|
||
let dsp = dd1
|
||
if (dd1 === dd2) dsp = time
|
||
if (dd1 === dd3) dsp = this.lang('Yesterday')
|
||
return '<span title="'+ dd1 +' ' + time2 +'">'+ dsp +'</span>'
|
||
}
|
||
formatSize(sizeStr) {
|
||
if (!this.isFloat(sizeStr) || sizeStr === '') return ''
|
||
sizeStr = parseFloat(sizeStr)
|
||
if (sizeStr === 0) return 0
|
||
let sizes = ['Bt', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']
|
||
let i = parseInt( Math.floor( Math.log(sizeStr) / Math.log(1024) ) )
|
||
return (Math.floor(sizeStr / Math.pow(1024, i) * 10) / 10).toFixed(i === 0 ? 0 : 1) + ' ' + (sizes[i] || '??')
|
||
}
|
||
formatNumber(val, fraction, useGrouping) {
|
||
if (val == null || val === '' || typeof val === 'object') return ''
|
||
let options = {
|
||
minimumFractionDigits: parseInt(fraction),
|
||
maximumFractionDigits: parseInt(fraction),
|
||
useGrouping: !!useGrouping
|
||
}
|
||
if (fraction == null || fraction < 0) {
|
||
options.minimumFractionDigits = 0
|
||
options.maximumFractionDigits = 20
|
||
}
|
||
return parseFloat(val).toLocaleString(this.settings.locale, options)
|
||
}
|
||
formatDate(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
|
||
if (!format) format = this.settings.dateFormat
|
||
if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
|
||
let dt = new Date(dateStr)
|
||
if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps
|
||
if (String(dt) === 'Invalid Date') return ''
|
||
let year = dt.getFullYear()
|
||
let month = dt.getMonth()
|
||
let date = dt.getDate()
|
||
return format.toLowerCase()
|
||
.replace('month', this.settings.fullmonths[month])
|
||
.replace('mon', this.settings.shortmonths[month])
|
||
.replace(/yyyy/g, ('000' + year).slice(-4))
|
||
.replace(/yyy/g, ('000' + year).slice(-4))
|
||
.replace(/yy/g, ('0' + year).slice(-2))
|
||
.replace(/(^|[^a-z$])y/g, '$1' + year) // only y's that are not preceded by a letter
|
||
.replace(/mm/g, ('0' + (month + 1)).slice(-2))
|
||
.replace(/dd/g, ('0' + date).slice(-2))
|
||
.replace(/th/g, (date == 1 ? 'st' : 'th'))
|
||
.replace(/th/g, (date == 2 ? 'nd' : 'th'))
|
||
.replace(/th/g, (date == 3 ? 'rd' : 'th'))
|
||
.replace(/(^|[^a-z$])m/g, '$1' + (month + 1)) // only y's that are not preceded by a letter
|
||
.replace(/(^|[^a-z$])d/g, '$1' + date) // only y's that are not preceded by a letter
|
||
}
|
||
formatTime(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
|
||
if (!format) format = this.settings.timeFormat
|
||
if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
|
||
let dt = new Date(dateStr)
|
||
if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps
|
||
if (this.isTime(dateStr)) {
|
||
let tmp = this.isTime(dateStr, true)
|
||
dt = new Date()
|
||
dt.setHours(tmp.hours)
|
||
dt.setMinutes(tmp.minutes)
|
||
}
|
||
if (String(dt) === 'Invalid Date') return ''
|
||
let type = 'am'
|
||
let hour = dt.getHours()
|
||
let h24 = dt.getHours()
|
||
let min = dt.getMinutes()
|
||
let sec = dt.getSeconds()
|
||
if (min < 10) min = '0' + min
|
||
if (sec < 10) sec = '0' + sec
|
||
if (format.indexOf('am') !== -1 || format.indexOf('pm') !== -1) {
|
||
if (hour >= 12) type = 'pm'
|
||
if (hour > 12) hour = hour - 12
|
||
if (hour === 0) hour = 12
|
||
}
|
||
return format.toLowerCase()
|
||
.replace('am', type)
|
||
.replace('pm', type)
|
||
.replace('hhh', (hour < 10 ? '0' + hour : hour))
|
||
.replace('hh24', (h24 < 10 ? '0' + h24 : h24))
|
||
.replace('h24', h24)
|
||
.replace('hh', hour)
|
||
.replace('mm', min)
|
||
.replace('mi', min)
|
||
.replace('ss', sec)
|
||
.replace(/(^|[^a-z$])h/g, '$1' + hour) // only y's that are not preceded by a letter
|
||
.replace(/(^|[^a-z$])m/g, '$1' + min) // only y's that are not preceded by a letter
|
||
.replace(/(^|[^a-z$])s/g, '$1' + sec) // only y's that are not preceded by a letter
|
||
}
|
||
formatDateTime(dateStr, format) {
|
||
let fmt
|
||
if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
|
||
if (typeof format !== 'string') {
|
||
fmt = [this.settings.dateFormat, this.settings.timeFormat]
|
||
} else {
|
||
fmt = format.split('|')
|
||
fmt[0] = fmt[0].trim()
|
||
fmt[1] = (fmt.length > 1 ? fmt[1].trim() : this.settings.timeFormat)
|
||
}
|
||
// older formats support
|
||
if (fmt[1] === 'h12') fmt[1] = 'h:m pm'
|
||
if (fmt[1] === 'h24') fmt[1] = 'h24:m'
|
||
return this.formatDate(dateStr, fmt[0]) + ' ' + this.formatTime(dateStr, fmt[1])
|
||
}
|
||
stripSpaces(html) {
|
||
if (html == null) return html
|
||
switch (typeof html) {
|
||
case 'number':
|
||
break
|
||
case 'string':
|
||
html = String(html).replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\s\s+/g, ' ').trim()
|
||
break
|
||
case 'object':
|
||
// does not modify original object, but creates a copy
|
||
if (Array.isArray(html)) {
|
||
html = this.extend([], html)
|
||
html.forEach((key, ind) => {
|
||
html[ind] = this.stripSpaces(key)
|
||
})
|
||
} else {
|
||
html = this.extend({}, html)
|
||
Object.keys(html).forEach(key => {
|
||
html[key] = this.stripSpaces(html[key])
|
||
})
|
||
}
|
||
break
|
||
}
|
||
return html
|
||
}
|
||
stripTags(html) {
|
||
if (html == null) return html
|
||
switch (typeof html) {
|
||
case 'number':
|
||
break
|
||
case 'string':
|
||
html = String(html).replace(/<(?:[^>=]|='[^']*'|="[^"]*"|=[^'"][^\s>]*)*>/ig, '')
|
||
break
|
||
case 'object':
|
||
// does not modify original object, but creates a copy
|
||
if (Array.isArray(html)) {
|
||
html = this.extend([], html)
|
||
html.forEach((key, ind) => {
|
||
html[ind] = this.stripTags(key)
|
||
})
|
||
} else {
|
||
html = this.extend({}, html)
|
||
Object.keys(html).forEach(key => {
|
||
html[key] = this.stripTags(html[key])
|
||
})
|
||
}
|
||
break
|
||
}
|
||
return html
|
||
}
|
||
encodeTags(html) {
|
||
if (html == null) return html
|
||
switch (typeof html) {
|
||
case 'number':
|
||
break
|
||
case 'string':
|
||
html = String(html).replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"')
|
||
break
|
||
case 'object':
|
||
// does not modify original object, but creates a copy
|
||
if (Array.isArray(html)) {
|
||
html = this.extend([], html)
|
||
html.forEach((key, ind) => {
|
||
html[ind] = this.encodeTags(key)
|
||
})
|
||
} else {
|
||
html = this.extend({}, html)
|
||
Object.keys(html).forEach(key => {
|
||
html[key] = this.encodeTags(html[key])
|
||
})
|
||
}
|
||
break
|
||
}
|
||
return html
|
||
}
|
||
decodeTags(html) {
|
||
if (html == null) return html
|
||
switch (typeof html) {
|
||
case 'number':
|
||
break
|
||
case 'string':
|
||
html = String(html).replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/&/g, '&')
|
||
break
|
||
case 'object':
|
||
// does not modify original object, but creates a copy
|
||
if (Array.isArray(html)) {
|
||
html = this.extend([], html)
|
||
html.forEach((key, ind) => {
|
||
html[ind] = this.decodeTags(key)
|
||
})
|
||
} else {
|
||
html = this.extend({}, html)
|
||
Object.keys(html).forEach(key => {
|
||
html[key] = this.decodeTags(html[key])
|
||
})
|
||
}
|
||
break
|
||
}
|
||
return html
|
||
}
|
||
escapeId(id) {
|
||
// This logic is borrowed from jQuery
|
||
if (id === '' || id == null) return ''
|
||
let re = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g
|
||
return (id + '').replace(re, (ch, asCodePoint) => {
|
||
if (asCodePoint) {
|
||
if (ch === '\0') return '\uFFFD'
|
||
return ch.slice( 0, -1 ) + '\\' + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + ' '
|
||
}
|
||
return '\\' + ch
|
||
})
|
||
}
|
||
unescapeId(id) {
|
||
// This logic is borrowed from jQuery
|
||
if (id === '' || id == null) return ''
|
||
let re = /\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\([^\r\n\f])/g
|
||
return id.replace(re, (escape, nonHex) => {
|
||
let high = '0x' + escape.slice( 1 ) - 0x10000
|
||
return nonHex ? nonHex : high < 0
|
||
? String.fromCharCode(high + 0x10000 )
|
||
: String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00)
|
||
})
|
||
}
|
||
base64encode(input) {
|
||
// Fast Native support in Chrome since 2010
|
||
return btoa(input) // binary to ascii
|
||
}
|
||
base64decode(input) {
|
||
// Fast Native support in Chrome since 2010
|
||
return atob(input) // ascii to binary
|
||
}
|
||
async sha256(str) {
|
||
const utf8 = new TextEncoder().encode(str)
|
||
return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {
|
||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||
return hashArray.map((bytes) => bytes.toString(16).padStart(2, '0')).join('')
|
||
})
|
||
}
|
||
transition(div_old, div_new, type, callBack) {
|
||
return new Promise((resolve, reject) => {
|
||
let styles = getComputedStyle(div_old)
|
||
let width = parseInt(styles.width)
|
||
let height = parseInt(styles.height)
|
||
let time = 0.5
|
||
if (!div_old || !div_new) {
|
||
console.log('ERROR: Cannot do transition when one of the divs is null')
|
||
return
|
||
}
|
||
div_old.parentNode.style.cssText += 'perspective: 900px; overflow: hidden;'
|
||
div_old.style.cssText += '; position: absolute; z-index: 1019; backface-visibility: hidden'
|
||
div_new.style.cssText += '; position: absolute; z-index: 1020; backface-visibility: hidden'
|
||
switch (type) {
|
||
case 'slide-left':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: translate3d('+ width + 'px, 0, 0)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(-'+ width +'px, 0, 0)'
|
||
}, 1)
|
||
break
|
||
case 'slide-right':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: translate3d(-'+ width +'px, 0, 0)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0px, 0, 0)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d('+ width +'px, 0, 0)'
|
||
}, 1)
|
||
break
|
||
case 'slide-down':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; z-index: 1; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; z-index: 0; transform: translate3d(0, 0, 0)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, '+ height +'px, 0)'
|
||
}, 1)
|
||
break
|
||
case 'slide-up':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, '+ height +'px, 0)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
|
||
}, 1)
|
||
break
|
||
case 'flip-left':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: rotateY(-180deg)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: rotateY(0deg)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: rotateY(180deg)'
|
||
}, 1)
|
||
break
|
||
case 'flip-right':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: rotateY(180deg)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: rotateY(0deg)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: rotateY(-180deg)'
|
||
}, 1)
|
||
break
|
||
case 'flip-down':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: rotateX(180deg)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: rotateX(0deg)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: rotateX(-180deg)'
|
||
}, 1)
|
||
break
|
||
case 'flip-up':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: rotateX(-180deg)'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: rotateX(0deg)'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: rotateX(180deg)'
|
||
}, 1)
|
||
break
|
||
case 'pop-in':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(.8); opacity: 0;'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; transform: scale(1); opacity: 1;'
|
||
div_old.style.cssText += 'transition: '+ time +'s;'
|
||
}, 1)
|
||
break
|
||
case 'pop-out':
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(1); opacity: 1;'
|
||
div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); opacity: 0;'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; opacity: 1;'
|
||
div_old.style.cssText += 'transition: '+ time +'s; transform: scale(1.7); opacity: 0;'
|
||
}, 1)
|
||
break
|
||
default:
|
||
// init divs
|
||
div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
|
||
div_new.style.cssText += 'overflow: hidden; translate3d(0, 0, 0); opacity: 0;'
|
||
query(div_new).show()
|
||
// -- need a timing function because otherwise not working
|
||
setTimeout(() => {
|
||
div_new.style.cssText += 'transition: '+ time +'s; opacity: 1;'
|
||
div_old.style.cssText += 'transition: '+ time +'s'
|
||
}, 1)
|
||
break
|
||
}
|
||
setTimeout(() => {
|
||
if (type === 'slide-down') {
|
||
query(div_old).css('z-index', '1019')
|
||
query(div_new).css('z-index', '1020')
|
||
}
|
||
if (div_new) {
|
||
query(div_new)
|
||
.css({ 'opacity': '1' })
|
||
.css({ 'transition': '', 'transform' : '' })
|
||
}
|
||
if (div_old) {
|
||
query(div_old)
|
||
.css({ 'opacity': '1' })
|
||
.css({ 'transition': '', 'transform' : '' })
|
||
}
|
||
if (typeof callBack === 'function') callBack()
|
||
resolve()
|
||
}, time * 1000)
|
||
})
|
||
}
|
||
lock(box, options = {}) {
|
||
if (box == null) return
|
||
if (typeof options == 'string') {
|
||
options = { msg: options }
|
||
}
|
||
if (arguments[2]) {
|
||
options.spinner = arguments[2]
|
||
}
|
||
options = this.extend({
|
||
spinner: false
|
||
}, options)
|
||
// for backward compatibility
|
||
if (box?.[0] instanceof Node) {
|
||
box = Array.isArray(box) ? box : box.get()
|
||
}
|
||
if (!options.msg && options.msg !== 0) options.msg = ''
|
||
this.unlock(box)
|
||
let el = query(box).get(0)
|
||
let pWidth = el.scrollWidth
|
||
let pHeight = el.scrollHeight
|
||
// if it is body and only has absolute elements, its height will be 0, need to lock entire window
|
||
if (el.tagName == 'BODY') {
|
||
if (pWidth < innerWidth) pWidth = innerWidth
|
||
if (pHeight < innerHeight) pHeight = innerHeight
|
||
}
|
||
query(box).prepend(
|
||
`<div class="w2ui-lock" style="height: ${pHeight}px; width: ${pWidth}px"></div>` +
|
||
'<div class="w2ui-lock-msg"></div>'
|
||
)
|
||
let $lock = query(box).find('.w2ui-lock')
|
||
let $mess = query(box).find('.w2ui-lock-msg')
|
||
if (!options.msg) {
|
||
$mess.css({
|
||
'background-color': 'transparent',
|
||
'background-image': 'none',
|
||
'border': '0px',
|
||
'box-shadow': 'none'
|
||
})
|
||
}
|
||
if (options.spinner === true) {
|
||
options.msg = `<div class="w2ui-spinner" ${(!options.msg ? 'style="width: 35px; height: 35px"' : '')}></div>`
|
||
+ options.msg
|
||
}
|
||
if (options.msg) {
|
||
$mess.html(options.msg).css('display', 'block')
|
||
} else {
|
||
$mess.remove()
|
||
}
|
||
if (options.opacity != null) {
|
||
$lock.css('opacity', options.opacity)
|
||
}
|
||
$lock.css({ display: 'block' })
|
||
if (options.bgColor) {
|
||
$lock.css({ 'background-color': options.bgColor })
|
||
}
|
||
let styles = getComputedStyle($lock.get(0))
|
||
let opacity = styles.opacity ?? 0.15
|
||
$lock
|
||
.on('mousedown', function() {
|
||
if (typeof options.onClick == 'function') {
|
||
options.onClick()
|
||
} else {
|
||
$lock.css({
|
||
'transition': '.2s',
|
||
'opacity': opacity * 1.5
|
||
})
|
||
}
|
||
})
|
||
.on('mouseup', function() {
|
||
if (typeof options.onClick !== 'function') {
|
||
$lock.css({
|
||
'transition': '.2s',
|
||
'opacity': opacity
|
||
})
|
||
}
|
||
})
|
||
.on('mousewheel', function(event) {
|
||
if (event) {
|
||
event.stopPropagation()
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
}
|
||
unlock(box, speed) {
|
||
if (box == null) return
|
||
clearTimeout(box._prevUnlock)
|
||
// for backward compatibility
|
||
if (box?.[0] instanceof Node) {
|
||
box = Array.isArray(box) ? box : box.get()
|
||
}
|
||
if (this.isInt(speed) && speed > 0) {
|
||
query(box).find('.w2ui-lock').css({
|
||
transition: (speed/1000) + 's',
|
||
opacity: 0,
|
||
})
|
||
let _box = query(box).get(0)
|
||
clearTimeout(_box._prevUnlock)
|
||
_box._prevUnlock = setTimeout(() => {
|
||
query(box).find('.w2ui-lock').remove()
|
||
}, speed)
|
||
query(box).find('.w2ui-lock-msg').remove()
|
||
} else {
|
||
query(box).find('.w2ui-lock').remove()
|
||
query(box).find('.w2ui-lock-msg').remove()
|
||
}
|
||
}
|
||
/**
|
||
* Opens a context message, similar in parameters as w2popup.open()
|
||
*
|
||
* Sample Calls
|
||
* w2utils.message({ box: '#div' }, 'message').ok(() => {})
|
||
* w2utils.message({ box: '#div' }, { text: 'message', width: 300 }).ok(() => {})
|
||
* w2utils.message({ box: '#div' }, { text: 'message', actions: ['Save'] }).Save(() => {})
|
||
*
|
||
* Used in w2grid, w2form, w2layout (should be in w2popup too)
|
||
* should be called with .call(...) method
|
||
*
|
||
* @param where = {
|
||
* box, // where to open
|
||
* after, // title if any, adds title heights
|
||
* param // additional parameters, used in layouts for panel
|
||
* }
|
||
* @param options {
|
||
* width, // (int), width in px, if negative, then it is maxWidth - width
|
||
* height, // (int), height in px, if negative, then it is maxHeight - height
|
||
* text, // centered text
|
||
* body, // body of the message
|
||
* buttons, // buttons of the message
|
||
* html, // if body & buttons are not defined, then html is the entire message
|
||
* focus, // int or id with a selector, default is 0
|
||
* hideOn, // ['esc', 'click'], default is ['esc']
|
||
* actions, // array of actions (only if buttons is not defined)
|
||
* onOpen, // event when opened
|
||
* onClose, // event when closed
|
||
* onAction, // event on action
|
||
* }
|
||
*/
|
||
message(where, options) {
|
||
let closeTimer, openTimer, edata
|
||
let removeLast = () => {
|
||
let msgs = query(where?.box).find('.w2ui-message')
|
||
if (msgs.length == 0) return // no messages already
|
||
options = msgs.get(0)._msg_options || {}
|
||
if (typeof options?.close == 'function') {
|
||
options.close()
|
||
}
|
||
}
|
||
let closeComplete = (options) => {
|
||
let focus = options.box._msg_prevFocus
|
||
if (query(where.box).find('.w2ui-message').length <= 1) {
|
||
if (where.owner) {
|
||
where.owner.unlock(where.param, 150)
|
||
} else {
|
||
this.unlock(where.box, 150)
|
||
}
|
||
} else {
|
||
query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex-1}`).css('z-index', 1500)
|
||
}
|
||
if (focus) {
|
||
let msg = query(focus).closest('.w2ui-message')
|
||
if (msg.length > 0) {
|
||
let opt = msg.get(0)._msg_options
|
||
opt.setFocus(focus)
|
||
} else {
|
||
focus.focus()
|
||
}
|
||
} else {
|
||
if (typeof where.owner?.focus == 'function') where.owner.focus()
|
||
}
|
||
query(options.box).remove()
|
||
if (options.msgIndex === 0) {
|
||
head.css('z-index', options.tmp.zIndex)
|
||
query(where.box).css('overflow', options.tmp.overflow)
|
||
}
|
||
// event after
|
||
if (options.trigger) {
|
||
edata.finish()
|
||
}
|
||
}
|
||
if (typeof options == 'string' || typeof options == 'number') {
|
||
options = {
|
||
width : (String(options).length < 300 ? 350 : 550),
|
||
height: (String(options).length < 300 ? 170: 250),
|
||
text : String(options),
|
||
}
|
||
}
|
||
if (typeof options != 'object') {
|
||
removeLast()
|
||
return
|
||
}
|
||
if (options.text != null) options.body = `<div class="w2ui-centered w2ui-msg-text">${options.text}</div>`
|
||
if (options.width == null) options.width = 350
|
||
if (options.height == null) options.height = 170
|
||
if (options.hideOn == null) options.hideOn = ['esc']
|
||
// mix in events
|
||
if (options.on == null) {
|
||
let opts = options
|
||
options = new w2base()
|
||
w2utils.extend(options, opts) // needs to be w2utils
|
||
}
|
||
options.on('open', (event) => {
|
||
w2utils.bindEvents(query(options.box).find('.w2ui-eaction'), options) // options is w2base object
|
||
query(event.detail.box).find('button, input, textarea, [name=hidden-first]')
|
||
.off('.message')
|
||
.on('keydown.message', function(evt) {
|
||
if (evt.keyCode == 27 && options.hideOn.includes('esc')) {
|
||
if (options.cancelAction) {
|
||
options.action(options.cancelAction)
|
||
} else {
|
||
options.close()
|
||
}
|
||
}
|
||
})
|
||
// timeout is needed because messages opens over 0.3 seconds
|
||
setTimeout(() => options.setFocus(options.focus), 300)
|
||
})
|
||
options.off('.prom')
|
||
let prom = {
|
||
self: options,
|
||
action(callBack) {
|
||
options.on('action.prom', callBack)
|
||
return prom
|
||
},
|
||
close(callBack) {
|
||
options.on('close.prom', callBack)
|
||
return prom
|
||
},
|
||
open(callBack) {
|
||
options.on('open.prom', callBack)
|
||
return prom
|
||
},
|
||
then(callBack) {
|
||
options.on('open:after.prom', callBack)
|
||
return prom
|
||
}
|
||
}
|
||
if (options.actions == null && options.buttons == null && options.html == null) {
|
||
options.actions = { Ok(event) { event.detail.self.close() }}
|
||
}
|
||
options.off('.buttons')
|
||
if (options.actions != null) {
|
||
options.buttons = ''
|
||
Object.keys(options.actions).forEach((action) => {
|
||
let handler = options.actions[action]
|
||
let btnAction = action
|
||
if (typeof handler == 'function') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction" data-click='["action","${action}","event"]' name="${action}">${action}</button>`
|
||
}
|
||
if (typeof handler == 'object') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction ${handler.class || ''}" name="${action}" data-click='["action","${action}","event"]'
|
||
style="${handler.style ?? ''}" ${handler.attrs ?? ''}>${handler.text || action}</button>`
|
||
btnAction = Array.isArray(options.actions) ? handler.text : action
|
||
}
|
||
if (typeof handler == 'string') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction" name="${handler}" data-click='["action","${handler}","event"]'>${handler}</button>`
|
||
btnAction = handler
|
||
}
|
||
if (typeof btnAction == 'string') {
|
||
btnAction = btnAction[0].toLowerCase() + btnAction.substr(1).replace(/\s+/g, '')
|
||
}
|
||
prom[btnAction] = function (callBack) {
|
||
options.on('action.buttons', (event) => {
|
||
let target = event.detail.action[0].toLowerCase() + event.detail.action.substr(1).replace(/\s+/g, '')
|
||
if (target == btnAction) callBack(event)
|
||
})
|
||
return prom
|
||
}
|
||
})
|
||
}
|
||
// trim if any
|
||
Array('html', 'body', 'buttons').forEach(param => {
|
||
options[param] = String(options[param] ?? '').trim()
|
||
})
|
||
if (options.body !== '' || options.buttons !== '') {
|
||
options.html = `
|
||
<div class="w2ui-message-body">${options.body || ''}</div>
|
||
<div class="w2ui-message-buttons">${options.buttons || ''}</div>
|
||
`
|
||
}
|
||
let styles = getComputedStyle(query(where.box).get(0))
|
||
let pWidth = parseFloat(styles.width)
|
||
let pHeight = parseFloat(styles.height)
|
||
let titleHeight = 0
|
||
if (query(where.after).length > 0) {
|
||
styles = getComputedStyle(query(where.after).get(0))
|
||
titleHeight = parseInt(styles.display != 'none' ? parseInt(styles.height) : 0)
|
||
}
|
||
if (options.width > pWidth) options.width = pWidth - 10
|
||
if (options.height > pHeight - titleHeight) options.height = pHeight - 10 - titleHeight
|
||
options.originalWidth = options.width
|
||
options.originalHeight = options.height
|
||
if (parseInt(options.width) < 0) options.width = pWidth + options.width
|
||
if (parseInt(options.width) < 10) options.width = 10
|
||
if (parseInt(options.height) < 0) options.height = pHeight + options.height - titleHeight
|
||
if (parseInt(options.height) < 10) options.height = 10
|
||
// negative value means margin
|
||
if (options.originalHeight < 0) options.height = pHeight + options.originalHeight - titleHeight
|
||
if (options.originalWidth < 0) options.width = pWidth + options.originalWidth * 2 // x 2 because there is left and right margin
|
||
let head = query(where.box).find(where.after) // needed for z-index manipulations
|
||
if (!options.tmp) {
|
||
options.tmp = {
|
||
zIndex: head.css('z-index'),
|
||
overflow: styles.overflow
|
||
}
|
||
}
|
||
// remove message
|
||
if (options.html === '' && options.body === '' && options.buttons === '') {
|
||
removeLast()
|
||
} else {
|
||
options.msgIndex = query(where.box).find('.w2ui-message').length
|
||
if (options.msgIndex === 0 && typeof this.lock == 'function') {
|
||
query(where.box).css('overflow', 'hidden')
|
||
if (where.owner) { // where.praram is used in the panel
|
||
where.owner.lock(where.param)
|
||
} else {
|
||
this.lock(where.box)
|
||
}
|
||
}
|
||
// send back previous messages
|
||
query(where.box).find('.w2ui-message').css('z-index', 1390)
|
||
head.css('z-index', 1501)
|
||
// add message
|
||
let content = `
|
||
<div id="w2ui-message-${where.owner?.name}-${options.msgIndex}" class="w2ui-message" data-mousedown="stop"
|
||
style="z-index: 1500; left: ${((pWidth - options.width) / 2)}px; top: ${titleHeight}px;
|
||
width: ${options.width}px; height: ${options.height}px; transform: translateY(-${options.height}px)"
|
||
${options.hideOn.includes('click')
|
||
? where.param
|
||
? `data-click='["message", "${where.param}"]`
|
||
: 'data-click="message"'
|
||
: ''}>
|
||
<span name="hidden-first" tabindex="0" style="position: absolute; top: 0; outline: none"></span>
|
||
${options.html}
|
||
<span name="hidden-last" tabindex="0" style="position: absolute; top: 0; outline: none"></span>
|
||
</div>`
|
||
if (query(where.after).length > 0) {
|
||
query(where.box).find(where.after).after(content)
|
||
} else {
|
||
query(where.box).prepend(content)
|
||
}
|
||
options.box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`)[0]
|
||
w2utils.bindEvents(options.box, this)
|
||
query(options.box)
|
||
.addClass('animating')
|
||
// remember options and prev focus
|
||
options.box._msg_options = options
|
||
options.box._msg_prevFocus = document.activeElement
|
||
// timeout is needs so that callBacks are setup
|
||
setTimeout(() => {
|
||
// before event
|
||
edata = options.trigger('open', { target: this.name, box: options.box, self: options })
|
||
if (edata.isCancelled === true) {
|
||
query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`).remove()
|
||
if (options.msgIndex === 0) {
|
||
head.css('z-index', options.tmp.zIndex)
|
||
query(where.box).css('overflow', options.tmp.overflow)
|
||
}
|
||
return
|
||
}
|
||
// slide down
|
||
query(options.box).css({
|
||
transition: '0.3s',
|
||
transform: 'translateY(0px)'
|
||
})
|
||
}, 0)
|
||
// timeout is needed so that animation can finish
|
||
openTimer = setTimeout(() => {
|
||
// has to be on top of lock
|
||
query(where.box)
|
||
.find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`)
|
||
.removeClass('animating')
|
||
.css({ 'transition': '0s' })
|
||
// event after
|
||
edata.finish()
|
||
}, 300)
|
||
}
|
||
// action handler
|
||
options.action = (action, event) => {
|
||
let click = options.actions[action]
|
||
if (click instanceof Object && click.onClick) click = click.onClick
|
||
// event before
|
||
let edata = options.trigger('action', { target: this.name, action, self: options,
|
||
originalEvent: event, value: options.input ? options.input.value : null })
|
||
if (edata.isCancelled === true) return
|
||
// default actions
|
||
if (typeof click === 'function') click(edata)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
options.close = () => {
|
||
edata = options.trigger('close', { target: 'self', box: options.box, self: options })
|
||
if (edata.isCancelled === true) return
|
||
clearTimeout(openTimer)
|
||
if (query(options.box).hasClass('animating')) {
|
||
clearTimeout(closeTimer)
|
||
closeComplete(options)
|
||
return
|
||
}
|
||
// default behavior
|
||
query(options.box)
|
||
.addClass('w2ui-closing animating')
|
||
.css({
|
||
'transition': '0.15s',
|
||
'transform': 'translateY(-' + options.height + 'px)'
|
||
})
|
||
if (options.msgIndex !== 0) {
|
||
// previous message
|
||
query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex-1}`).css('z-index', 1499)
|
||
}
|
||
closeTimer = setTimeout(() => { closeComplete(options) }, 150)
|
||
}
|
||
options.setFocus = (focus) => {
|
||
// in message or popup
|
||
let cnt = query(where.box).find('.w2ui-message').length - 1
|
||
let box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${cnt}`)
|
||
let sel = 'input, button, select, textarea, [contentEditable], .w2ui-input'
|
||
if (focus != null) {
|
||
let el = isNaN(focus)
|
||
? box.find(sel).filter(focus).get(0)
|
||
: box.find(sel).get(focus)
|
||
el?.focus()
|
||
} else {
|
||
box.find('[name=hidden-first]').get(0)?.focus()
|
||
}
|
||
// clear focus if there are other messages
|
||
query(where.box)
|
||
.find('.w2ui-message')
|
||
.find(sel + ',[name=hidden-first],[name=hidden-last]')
|
||
.off('.keep-focus')
|
||
// keep focus/blur inside popup
|
||
query(box)
|
||
.find(sel + ',[name=hidden-first],[name=hidden-last]')
|
||
.on('blur.keep-focus', function (event) {
|
||
setTimeout(() => {
|
||
let focus = document.activeElement
|
||
let inside = query(box).find(sel).filter(focus).length > 0
|
||
let name = query(focus).attr('name')
|
||
if (!inside && focus && focus !== document.body) {
|
||
query(box).find(sel).get(0)?.focus()
|
||
}
|
||
if (name == 'hidden-last') {
|
||
query(box).find(sel).get(0)?.focus()
|
||
}
|
||
if (name == 'hidden-first') {
|
||
query(box).find(sel).get(-1)?.focus()
|
||
}
|
||
}, 1)
|
||
})
|
||
}
|
||
return prom
|
||
}
|
||
/**
|
||
* Shows small notification message at the bottom of the page, or containter that you specify
|
||
* in options.where (could be element or a selector)
|
||
*
|
||
* w2utils.notify('Document saved')
|
||
* w2utils.notify('Mesage sent ${udon}', { actions: { undo: function () {...} }})
|
||
*
|
||
* @param {String/Object} options can be {
|
||
* text: string, // message, can be html
|
||
* where: el/selector, // element or selector where to show, default is document.body
|
||
* timeout: int, // timeout when to hide, if 0 - indefinite
|
||
* error: boolean, // add error clases
|
||
* class: string, // additional class strings
|
||
* actions: object // object with action functions, it should correspot to templated text: '... ${action} ...'
|
||
* }
|
||
* @returns promise
|
||
*/
|
||
notify(text, options) {
|
||
return new Promise(resolve => {
|
||
if (typeof text == 'object') {
|
||
options = text
|
||
text = options.text
|
||
}
|
||
options = options || {}
|
||
options.where = options.where ?? document.body
|
||
options.timeout = options.timeout ?? 15_000 // 15 secodns or will be hidden on route change
|
||
if (typeof this.tmp.notify_resolve == 'function') {
|
||
this.tmp.notify_resolve()
|
||
query(this.tmp.notify_where).find('#w2ui-notify').remove()
|
||
}
|
||
this.tmp.notify_resolve = resolve
|
||
this.tmp.notify_where = options.where
|
||
clearTimeout(this.tmp.notify_timer)
|
||
if (text) {
|
||
if (typeof options.actions == 'object') {
|
||
let actions = {}
|
||
Object.keys(options.actions).forEach(action => {
|
||
actions[action] = `<a class="w2ui-notify-link" value="${action}">${action}</a>`
|
||
})
|
||
text = this.execTemplate(text, actions)
|
||
}
|
||
let html = `
|
||
<div id="w2ui-notify">
|
||
<div class="${options.class} ${options.error ? 'w2ui-notify-error' : ''}">
|
||
${text}
|
||
<span class="w2ui-notify-close w2ui-icon-cross"></span>
|
||
</div>
|
||
</div>`
|
||
query(options.where).append(html)
|
||
query(options.where).find('#w2ui-notify').find('.w2ui-notify-close')
|
||
.on('click', event => {
|
||
query(options.where).find('#w2ui-notify').remove()
|
||
resolve()
|
||
})
|
||
if (options.actions) {
|
||
query(options.where).find('#w2ui-notify .w2ui-notify-link')
|
||
.on('click', event => {
|
||
let value = query(event.target).attr('value')
|
||
options.actions[value]()
|
||
query(options.where).find('#w2ui-notify').remove()
|
||
resolve()
|
||
})
|
||
}
|
||
if (options.timeout > 0) {
|
||
this.tmp.notify_timer = setTimeout(() => {
|
||
query(options.where).find('#w2ui-notify').remove()
|
||
resolve()
|
||
}, options.timeout)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
confirm(where, options) {
|
||
if (typeof options == 'string') {
|
||
options = { text: options }
|
||
}
|
||
w2utils.normButtons(options, { yes: 'Yes', no: 'No' })
|
||
let prom = w2utils.message(where, options)
|
||
if (prom) {
|
||
prom.action(event => {
|
||
event.detail.self.close()
|
||
})
|
||
}
|
||
return prom
|
||
}
|
||
/**
|
||
* Normalizes yes, no buttons for confirmation dialog
|
||
*
|
||
* @param {*} options
|
||
* @returns options
|
||
*/
|
||
normButtons(options, btn) {
|
||
options.actions = options.actions ?? {}
|
||
let btns = Object.keys(btn)
|
||
btns.forEach(name => {
|
||
let action = options['btn_' + name]
|
||
if (action) {
|
||
btn[name] = {
|
||
text: w2utils.lang(action.text ?? ''),
|
||
class: action.class ?? '',
|
||
style: action.style ?? '',
|
||
attrs: action.attrs ?? ''
|
||
}
|
||
delete options['btn_' + name]
|
||
}
|
||
Array('text', 'class', 'style', 'attrs').forEach(suffix => {
|
||
if (options[name + '_' + suffix]) {
|
||
if (typeof btn[name] == 'string') {
|
||
btn[name] = { text: btn[name] }
|
||
}
|
||
btn[name][suffix] = options[name + '_' + suffix]
|
||
delete options[name + '_' + suffix]
|
||
}
|
||
})
|
||
})
|
||
if (btns.includes('yes') && btns.includes('no')) {
|
||
if (w2utils.settings.macButtonOrder) {
|
||
w2utils.extend(options.actions, { no: btn.no, yes: btn.yes })
|
||
} else {
|
||
w2utils.extend(options.actions, { yes: btn.yes, no: btn.no })
|
||
}
|
||
}
|
||
if (btns.includes('ok') && btns.includes('cancel')) {
|
||
if (w2utils.settings.macButtonOrder) {
|
||
w2utils.extend(options.actions, { cancel: btn.cancel, ok: btn.ok })
|
||
} else {
|
||
w2utils.extend(options.actions, { ok: btn.ok, cancel: btn.cancel })
|
||
}
|
||
}
|
||
return options
|
||
}
|
||
getSize(el, type) {
|
||
el = query(el) // for backward compatibility
|
||
let ret = 0
|
||
if (el.length > 0) {
|
||
el = el[0]
|
||
let styles = getComputedStyle(el)
|
||
switch (type) {
|
||
case 'width' :
|
||
ret = parseFloat(styles.width)
|
||
if (styles.width === 'auto') ret = 0
|
||
break
|
||
case 'height' :
|
||
ret = parseFloat(styles.height)
|
||
if (styles.height === 'auto') ret = 0
|
||
break
|
||
default:
|
||
ret = parseFloat(styles[type] ?? 0) || 0
|
||
break
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
getStrWidth(str, styles) {
|
||
query('body').append(`
|
||
<div id="_tmp_width" style="position: absolute; top: -9000px; ${styles || ''}">
|
||
${this.encodeTags(str)}
|
||
</div>`)
|
||
let width = query('#_tmp_width')[0].clientWidth
|
||
query('#_tmp_width').remove()
|
||
return width
|
||
}
|
||
execTemplate(str, replace_obj) {
|
||
if (typeof str !== 'string' || !replace_obj || typeof replace_obj !== 'object') {
|
||
return str
|
||
}
|
||
return str.replace(/\${([^}]+)?}/g, function($1, $2) { return replace_obj[$2]||$2 })
|
||
}
|
||
marker(el, items, options = { onlyFirst: false, wholeWord: false }) {
|
||
if (!Array.isArray(items)) {
|
||
if (items != null && items !== '') {
|
||
items = [items]
|
||
} else {
|
||
items = []
|
||
}
|
||
}
|
||
let ww = options.wholeWord
|
||
query(el).each(el => {
|
||
clearMerkers(el)
|
||
items.forEach(str => {
|
||
if (typeof str !== 'string') str = String(str)
|
||
let replaceValue = (matched) => { // mark new
|
||
return '<span class="w2ui-marker">' + matched + '</span>'
|
||
}
|
||
// escape regex special chars
|
||
str = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&').replace(/&/g, '&')
|
||
.replace(/</g, '>').replace(/>/g, '<')
|
||
let regex = new RegExp((ww ? '\\b' : '') + str + (ww ? '\\b' : '')+ '(?!([^<]+)?>)',
|
||
'i' + (!options.onlyFirst ? 'g' : '')) // only outside tags
|
||
el.innerHTML = el.innerHTML.replace(regex, replaceValue)
|
||
})
|
||
})
|
||
function clearMerkers(el) {
|
||
let markerRE = /\<span class=\"w2ui\-marker\"\>((.|\n|\r)*)\<\/span\>/ig
|
||
while (el.innerHTML.indexOf('<span class="w2ui-marker"') !== -1) {
|
||
el.innerHTML = el.innerHTML.replace(markerRE, '$1') // unmark
|
||
}
|
||
}
|
||
}
|
||
lang(phrase, params) {
|
||
if (!phrase || this.settings.phrases == null // if no phrases at all
|
||
|| typeof phrase !== 'string' || '<=>='.includes(phrase)) {
|
||
return this.execTemplate(phrase, params)
|
||
}
|
||
let translation = this.settings.phrases[phrase]
|
||
if (translation == null) {
|
||
translation = phrase
|
||
if (this.settings.warnNoPhrase) {
|
||
if (!this.settings.missing) {
|
||
this.settings.missing = {}
|
||
}
|
||
this.settings.missing[phrase] = '---' // collect phrases for translation, warn once
|
||
this.settings.phrases[phrase] = '---'
|
||
console.log(`Missing translation for "%c${phrase}%c", see %c w2utils.settings.phrases %c with value "---"`,
|
||
'color: orange', '',
|
||
'color: #999', '')
|
||
}
|
||
} else if (translation === '---' && !this.settings.warnNoPhrase) {
|
||
translation = phrase
|
||
}
|
||
if (translation === '---') {
|
||
translation = `<span ${this.tooltip(phrase)}>---</span>`
|
||
}
|
||
return this.execTemplate(translation, params)
|
||
}
|
||
locale(locale, keepPhrases, noMerge) {
|
||
return new Promise((resolve, reject) => {
|
||
// if locale is an array we call this function recursively and merge the results
|
||
if (Array.isArray(locale)) {
|
||
this.settings.phrases = {}
|
||
let proms = []
|
||
let files = {}
|
||
locale.forEach((file, ind) => {
|
||
if (file.length === 5) {
|
||
file = 'locale/'+ file.toLowerCase() +'.json'
|
||
locale[ind] = file
|
||
}
|
||
proms.push(this.locale(file, true, false))
|
||
})
|
||
Promise.allSettled(proms)
|
||
.then(res => {
|
||
// order of files is important to merge
|
||
res.forEach(r => { if (r.value) files[r.value.file] = r.value.data })
|
||
locale.forEach(file => {
|
||
this.settings = this.extend({}, this.settings, files[file])
|
||
})
|
||
resolve()
|
||
})
|
||
return
|
||
}
|
||
if (!locale) locale = 'en-us'
|
||
// if locale is an object, then merge it with w2utils.settings
|
||
if (locale instanceof Object) {
|
||
this.settings = this.extend({}, this.settings, w2locale, locale)
|
||
return
|
||
}
|
||
if (locale.length === 5) {
|
||
locale = 'locale/'+ locale.toLowerCase() +'.json'
|
||
}
|
||
// load from the file
|
||
fetch(locale, { method: 'GET' })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (noMerge !== true) {
|
||
if (keepPhrases) {
|
||
// keep phrases, useful for recursive calls
|
||
this.settings = this.extend({}, this.settings, data)
|
||
} else {
|
||
// clear phrases from language before merging
|
||
this.settings = this.extend({}, this.settings, w2locale, { phrases: {} }, data)
|
||
}
|
||
}
|
||
resolve({ file: locale, data })
|
||
})
|
||
.catch((err) => {
|
||
console.log('ERROR: Cannot load locale '+ locale)
|
||
reject(err)
|
||
})
|
||
})
|
||
}
|
||
scrollBarSize() {
|
||
if (this.tmp.scrollBarSize) return this.tmp.scrollBarSize
|
||
let html = `
|
||
<div id="_scrollbar_width" style="position: absolute; top: -300px; width: 100px; height: 100px; overflow-y: scroll;">
|
||
<div style="height: 120px">1</div>
|
||
</div>
|
||
`
|
||
query('body').append(html)
|
||
this.tmp.scrollBarSize = 100 - query('#_scrollbar_width > div')[0].clientWidth
|
||
query('#_scrollbar_width').remove()
|
||
return this.tmp.scrollBarSize
|
||
}
|
||
checkName(name) {
|
||
if (name == null) {
|
||
console.log('ERROR: Property "name" is required but not supplied.')
|
||
return false
|
||
}
|
||
if (w2ui[name] != null) {
|
||
console.log(`ERROR: Object named "${name}" is already registered as w2ui.${name}.`)
|
||
return false
|
||
}
|
||
if (!this.isAlphaNumeric(name)) {
|
||
console.log('ERROR: Property "name" has to be alpha-numeric (a-z, 0-9, dash and underscore).')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
checkUniqueId(id, items, desc, obj) {
|
||
if (!Array.isArray(items)) items = [items]
|
||
let isUnique = true
|
||
items.forEach(item => {
|
||
if (item.id === id) {
|
||
console.log(`ERROR: The item id="${id}" is not unique within the ${desc} "${obj}".`, items)
|
||
isUnique = false
|
||
}
|
||
})
|
||
return isUnique
|
||
}
|
||
/**
|
||
* Takes an object and encodes it into params string to be passed as a url
|
||
* { a: 1, b: 'str'} => "a=1&b=str"
|
||
* { a: 1, b: { c: 2 }} => "a=1&b[c]=2"
|
||
* { a: 1, b: {c: { k: 'dfdf' } } } => "a=1&b[c][k]=dfdf"
|
||
*/
|
||
encodeParams(obj, prefix = '') {
|
||
let str = ''
|
||
Object.keys(obj).forEach(key => {
|
||
if (str != '') str += '&'
|
||
if (typeof obj[key] == 'object') {
|
||
str += this.encodeParams(obj[key], prefix + key + (prefix ? ']' : '') + '[')
|
||
} else {
|
||
str += `${prefix}${key}${prefix ? ']' : ''}=${obj[key]}`
|
||
}
|
||
})
|
||
return str
|
||
}
|
||
parseRoute(route) {
|
||
let keys = []
|
||
let path = route
|
||
.replace(/\/\(/g, '(?:/')
|
||
.replace(/\+/g, '__plus__')
|
||
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash, format, key, capture, optional) => {
|
||
keys.push({ name: key, optional: !! optional })
|
||
slash = slash || ''
|
||
return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '')
|
||
})
|
||
.replace(/([\/.])/g, '\\$1')
|
||
.replace(/__plus__/g, '(.+)')
|
||
.replace(/\*/g, '(.*)')
|
||
return {
|
||
path : new RegExp('^' + path + '$', 'i'),
|
||
keys : keys
|
||
}
|
||
}
|
||
getCursorPosition(input) {
|
||
if (input == null) return null
|
||
let caretOffset = 0
|
||
let doc = input.ownerDocument || input.document
|
||
let win = doc.defaultView || doc.parentWindow
|
||
let sel
|
||
if (['INPUT', 'TEXTAREA'].includes(input.tagName)) {
|
||
caretOffset = input.selectionStart
|
||
} else {
|
||
if (win.getSelection) {
|
||
sel = win.getSelection()
|
||
if (sel.rangeCount > 0) {
|
||
let range = sel.getRangeAt(0)
|
||
let preCaretRange = range.cloneRange()
|
||
preCaretRange.selectNodeContents(input)
|
||
preCaretRange.setEnd(range.endContainer, range.endOffset)
|
||
caretOffset = preCaretRange.toString().length
|
||
}
|
||
} else if ( (sel = doc.selection) && sel.type !== 'Control') {
|
||
let textRange = sel.createRange()
|
||
let preCaretTextRange = doc.body.createTextRange()
|
||
preCaretTextRange.moveToElementText(input)
|
||
preCaretTextRange.setEndPoint('EndToEnd', textRange)
|
||
caretOffset = preCaretTextRange.text.length
|
||
}
|
||
}
|
||
return caretOffset
|
||
}
|
||
setCursorPosition(input, pos, posEnd) {
|
||
if (input == null) return
|
||
let range = document.createRange()
|
||
let el, sel = window.getSelection()
|
||
if (['INPUT', 'TEXTAREA'].includes(input.tagName)) {
|
||
input.setSelectionRange(pos, posEnd ?? pos)
|
||
} else {
|
||
for (let i = 0; i < input.childNodes.length; i++) {
|
||
let tmp = query(input.childNodes[i]).text()
|
||
if (input.childNodes[i].tagName) {
|
||
tmp = query(input.childNodes[i]).html()
|
||
tmp = tmp.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/ /g, ' ')
|
||
}
|
||
if (pos <= tmp.length) {
|
||
el = input.childNodes[i]
|
||
if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0]
|
||
if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0]
|
||
break
|
||
} else {
|
||
pos -= tmp.length
|
||
}
|
||
}
|
||
if (el == null) return
|
||
if (pos > el.length) pos = el.length
|
||
range.setStart(el, pos)
|
||
if (posEnd) {
|
||
range.setEnd(el, posEnd)
|
||
} else {
|
||
range.collapse(true)
|
||
}
|
||
sel.removeAllRanges()
|
||
sel.addRange(range)
|
||
}
|
||
}
|
||
parseColor(str) {
|
||
if (typeof str !== 'string') return null; else str = str.trim().toUpperCase()
|
||
if (str[0] === '#') str = str.substr(1)
|
||
let color = {}
|
||
if (str.length === 3) {
|
||
color = {
|
||
r: parseInt(str[0] + str[0], 16),
|
||
g: parseInt(str[1] + str[1], 16),
|
||
b: parseInt(str[2] + str[2], 16),
|
||
a: 1
|
||
}
|
||
} else if (str.length === 6) {
|
||
color = {
|
||
r: parseInt(str.substr(0, 2), 16),
|
||
g: parseInt(str.substr(2, 2), 16),
|
||
b: parseInt(str.substr(4, 2), 16),
|
||
a: 1
|
||
}
|
||
} else if (str.length === 8) {
|
||
color = {
|
||
r: parseInt(str.substr(0, 2), 16),
|
||
g: parseInt(str.substr(2, 2), 16),
|
||
b: parseInt(str.substr(4, 2), 16),
|
||
a: Math.round(parseInt(str.substr(6, 2), 16) / 255 * 100) / 100 // alpha channel 0-1
|
||
}
|
||
} else if (str.length > 4 && str.substr(0, 4) === 'RGB(') {
|
||
let tmp = str.replace('RGB', '').replace(/\(/g, '').replace(/\)/g, '').split(',')
|
||
color = {
|
||
r: parseInt(tmp[0], 10),
|
||
g: parseInt(tmp[1], 10),
|
||
b: parseInt(tmp[2], 10),
|
||
a: 1
|
||
}
|
||
} else if (str.length > 5 && str.substr(0, 5) === 'RGBA(') {
|
||
let tmp = str.replace('RGBA', '').replace(/\(/g, '').replace(/\)/g, '').split(',')
|
||
color = {
|
||
r: parseInt(tmp[0], 10),
|
||
g: parseInt(tmp[1], 10),
|
||
b: parseInt(tmp[2], 10),
|
||
a: parseFloat(tmp[3])
|
||
}
|
||
} else {
|
||
// word color
|
||
return null
|
||
}
|
||
return color
|
||
}
|
||
// h=0..360, s=0..100, v=0..100
|
||
hsv2rgb(h, s, v, a) {
|
||
let r, g, b, i, f, p, q, t
|
||
if (arguments.length === 1) {
|
||
s = h.s; v = h.v; a = h.a; h = h.h
|
||
}
|
||
h = h / 360
|
||
s = s / 100
|
||
v = v / 100
|
||
i = Math.floor(h * 6)
|
||
f = h * 6 - i
|
||
p = v * (1 - s)
|
||
q = v * (1 - f * s)
|
||
t = v * (1 - (1 - f) * s)
|
||
switch (i % 6) {
|
||
case 0: r = v, g = t, b = p; break
|
||
case 1: r = q, g = v, b = p; break
|
||
case 2: r = p, g = v, b = t; break
|
||
case 3: r = p, g = q, b = v; break
|
||
case 4: r = t, g = p, b = v; break
|
||
case 5: r = v, g = p, b = q; break
|
||
}
|
||
return {
|
||
r: Math.round(r * 255),
|
||
g: Math.round(g * 255),
|
||
b: Math.round(b * 255),
|
||
a: (a != null ? a : 1)
|
||
}
|
||
}
|
||
// r=0..255, g=0..255, b=0..255
|
||
rgb2hsv(r, g, b, a) {
|
||
if (arguments.length === 1) {
|
||
g = r.g; b = r.b; a = r.a; r = r.r
|
||
}
|
||
let max = Math.max(r, g, b), min = Math.min(r, g, b),
|
||
d = max - min,
|
||
h,
|
||
s = (max === 0 ? 0 : d / max),
|
||
v = max / 255
|
||
switch (max) {
|
||
case min: h = 0; break
|
||
case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break
|
||
case g: h = (b - r) + d * 2; h /= 6 * d; break
|
||
case b: h = (r - g) + d * 4; h /= 6 * d; break
|
||
}
|
||
return {
|
||
h: Math.round(h * 360),
|
||
s: Math.round(s * 100),
|
||
v: Math.round(v * 100),
|
||
a: (a != null ? a : 1)
|
||
}
|
||
}
|
||
tooltip(html, options) {
|
||
let actions
|
||
let showOn = 'mouseenter'
|
||
let hideOn = 'mouseleave'
|
||
if (typeof html == 'object') {
|
||
options = html
|
||
}
|
||
options = options || {}
|
||
if (typeof html == 'string') {
|
||
options.html = html
|
||
}
|
||
if (options.showOn) {
|
||
showOn = options.showOn
|
||
delete options.showOn
|
||
}
|
||
if (options.hideOn) {
|
||
hideOn = options.hideOn
|
||
delete options.hideOn
|
||
}
|
||
if (!options.name) options.name = 'no-name'
|
||
// base64 is needed to avoid '"<> and other special chars conflicts
|
||
actions = ` on${showOn}="w2tooltip.show(this, `
|
||
+ `JSON.parse(w2utils.base64decode('${this.base64encode(JSON.stringify(options))}')))" `
|
||
+ `on${hideOn}="w2tooltip.hide('${options.name}')"`
|
||
return actions
|
||
}
|
||
// determins if it is plain Object, not DOM element, nor a function, event, etc.
|
||
isPlainObject(value) {
|
||
if (value == null) { // null or undefined
|
||
return false
|
||
}
|
||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||
return false
|
||
}
|
||
if (value.constructor === undefined) {
|
||
return true
|
||
}
|
||
let proto = Object.getPrototypeOf(value)
|
||
return proto === null || proto === Object.prototype
|
||
}
|
||
/**
|
||
* Deep copy of an object or an array. Function, events and HTML elements will not be cloned,
|
||
* you can choose to include them or not, by default they are included.
|
||
* You can also exclude certain elements from final object if used with options: { exclude }
|
||
*/
|
||
clone(obj, options) {
|
||
let ret
|
||
options = Object.assign({ functions: true, elements: true, events: true, exclude: [] }, options ?? {})
|
||
if (Array.isArray(obj)) {
|
||
ret = Array.from(obj)
|
||
ret.forEach((value, ind) => {
|
||
ret[ind] = this.clone(value, options)
|
||
})
|
||
} else if (this.isPlainObject(obj)) {
|
||
ret = {}
|
||
Object.assign(ret, obj)
|
||
if (options.exclude) {
|
||
options.exclude.forEach(key => { delete ret[key] }) // delete excluded keys
|
||
}
|
||
Object.keys(ret).forEach(key => {
|
||
ret[key] = this.clone(ret[key], options)
|
||
if (ret[key] === undefined) delete ret[key] // do not include undefined elements
|
||
})
|
||
} else {
|
||
if ((obj instanceof Function && !options.functions)
|
||
|| (obj instanceof Node && !options.elements)
|
||
|| (obj instanceof Event && !options.events)
|
||
) {
|
||
// do not include these objects, otherwise include them uncloned
|
||
} else {
|
||
// primitive variable or function, event, dom element, etc, - all these are not cloned
|
||
ret = obj
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
/**
|
||
* Deep extend an object, if an array, it overwrrites it, cloning objects in the process
|
||
* target, source1, source2, ...
|
||
*/
|
||
extend(target, source) {
|
||
if (Array.isArray(target)) {
|
||
if (Array.isArray(source)) {
|
||
target.splice(0, target.length) // empty array but keep the reference
|
||
source.forEach(s => { target.push(this.clone(s)) })
|
||
} else {
|
||
throw new Error('Arrays can be extended with arrays only')
|
||
}
|
||
} else if (target instanceof Node || target instanceof Event) {
|
||
throw new Error('HTML elmenents and events cannot be extended')
|
||
} else if (target && typeof target == 'object' && source != null) {
|
||
if (typeof source != 'object') {
|
||
throw new Error('Object can be extended with other objects only.')
|
||
}
|
||
Object.keys(source).forEach(key => {
|
||
if (target[key] != null && typeof target[key] == 'object'
|
||
&& source[key] != null && typeof source[key] == 'object') {
|
||
let src = this.clone(source[key])
|
||
// do not extend HTML elements and events, but overwrite them
|
||
if (target[key] instanceof Node || target[key] instanceof Event) {
|
||
target[key] = src
|
||
} else {
|
||
// if an array needs to be extended with an object, then convert it to empty object
|
||
if (Array.isArray(target[key]) && this.isPlainObject(src)) {
|
||
target[key] = {}
|
||
}
|
||
this.extend(target[key], src)
|
||
}
|
||
} else {
|
||
target[key] = this.clone(source[key])
|
||
}
|
||
})
|
||
} else if (source != null) {
|
||
throw new Error('Object is not extendable, only {} or [] can be extended.')
|
||
}
|
||
// other arguments
|
||
if (arguments.length > 2) {
|
||
for (let i = 2; i < arguments.length; i++) {
|
||
this.extend(target, arguments[i])
|
||
}
|
||
}
|
||
return target
|
||
}
|
||
/*
|
||
* @author Lauri Rooden (https://github.com/litejs/natural-compare-lite)
|
||
* @license MIT License
|
||
*/
|
||
naturalCompare(a, b) {
|
||
let i, codeA
|
||
, codeB = 1
|
||
, posA = 0
|
||
, posB = 0
|
||
, alphabet = String.alphabet
|
||
function getCode(str, pos, code) {
|
||
if (code) {
|
||
for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i
|
||
return +str.slice(pos - 1, i)
|
||
}
|
||
code = alphabet && alphabet.indexOf(str.charAt(pos))
|
||
return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code
|
||
: code < 46 ? 65 // -
|
||
: code < 48 ? code - 1
|
||
: code < 58 ? code + 18 // 0-9
|
||
: code < 65 ? code - 11
|
||
: code < 91 ? code + 11 // A-Z
|
||
: code < 97 ? code - 37
|
||
: code < 123 ? code + 5 // a-z
|
||
: code - 63
|
||
}
|
||
|
||
if ((a+='') != (b+='')) for (;codeB;) {
|
||
codeA = getCode(a, posA++)
|
||
codeB = getCode(b, posB++)
|
||
if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) {
|
||
codeA = getCode(a, posA, posA)
|
||
codeB = getCode(b, posB, posA = i)
|
||
posB = i
|
||
}
|
||
if (codeA != codeB) return (codeA < codeB) ? -1 : 1
|
||
}
|
||
return 0
|
||
}
|
||
normMenu(menu, el) {
|
||
if (Array.isArray(menu)) {
|
||
menu.forEach((it, m) => {
|
||
if (typeof it === 'string' || typeof it === 'number') {
|
||
menu[m] = { id: it, text: String(it) }
|
||
} else if (it != null) {
|
||
if (it.caption != null && it.text == null) it.text = it.caption
|
||
if (it.text != null && it.id == null) it.id = it.text
|
||
if (it.text == null && it.id != null) it.text = it.id
|
||
} else {
|
||
menu[m] = { id: null, text: 'null' }
|
||
}
|
||
})
|
||
return menu
|
||
} else if (typeof menu === 'function') {
|
||
let newMenu = menu.call(this, menu, el)
|
||
return w2utils.normMenu.call(this, newMenu)
|
||
} else if (typeof menu === 'object') {
|
||
return Object.keys(menu).map(key => { return { id: key, text: menu[key] } })
|
||
}
|
||
}
|
||
bindEvents(selector, subject) {
|
||
// format is
|
||
// <div ... data-<event>='["<method>","param1","param2",...]'> -- should be valid JSON (no undefined)
|
||
// <div ... data-<event>="<method>|param1|param2">
|
||
// -- can have "event", "this", "stop", "stopPrevent", "alert" - as predefined objects
|
||
if (selector.length == 0) return
|
||
// for backward compatibility
|
||
if (selector?.[0] instanceof Node) {
|
||
selector = Array.isArray(selector) ? selector : selector.get()
|
||
}
|
||
query(selector).each((el) => {
|
||
let actions = query(el).data()
|
||
Object.keys(actions).forEach(name => {
|
||
let events = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mousemove', 'mouseup',
|
||
'contextmenu', 'focus', 'focusin', 'focusout', 'blur', 'input', 'change', 'keydown', 'keyup', 'keypress']
|
||
if (events.indexOf(String(name).toLowerCase()) == -1) {
|
||
return
|
||
}
|
||
let params = actions[name]
|
||
if (typeof params == 'string') {
|
||
params = params.split('|').map(key => {
|
||
if (key === 'true') key = true
|
||
if (key === 'false') key = false
|
||
if (key === 'undefined') key = undefined
|
||
if (key === 'null') key = null
|
||
if (parseFloat(key) == key) key = parseFloat(key)
|
||
let quotes = ['\'', '"', '`']
|
||
if (typeof key == 'string' && quotes.includes(key[0]) && quotes.includes(key[key.length-1])) {
|
||
key = key.substring(1, key.length-1)
|
||
}
|
||
return key
|
||
})
|
||
}
|
||
let method = params[0]
|
||
params = params.slice(1) // should be new array
|
||
query(el)
|
||
.off(name + '.w2utils-bind')
|
||
.on(name + '.w2utils-bind', function(event) {
|
||
switch (method) {
|
||
case 'alert':
|
||
alert(params[0]) // for testing purposes
|
||
break
|
||
case 'stop':
|
||
event.stopPropagation()
|
||
break
|
||
case 'prevent':
|
||
event.preventDefault()
|
||
break
|
||
case 'stopPrevent':
|
||
event.stopPropagation()
|
||
event.preventDefault()
|
||
return false
|
||
break
|
||
default:
|
||
if (subject[method] == null) {
|
||
throw new Error(`Cannot dispatch event as the method "${method}" does not exist.`)
|
||
}
|
||
subject[method].apply(subject, params.map((key, ind) => {
|
||
switch (String(key).toLowerCase()) {
|
||
case 'event':
|
||
return event
|
||
case 'this':
|
||
return this
|
||
default:
|
||
return key
|
||
}
|
||
}))
|
||
}
|
||
})
|
||
})
|
||
})
|
||
}
|
||
debounce(func, wait = 250) {
|
||
let timeout
|
||
return (...args) => {
|
||
clearTimeout(timeout)
|
||
timeout = setTimeout(() => { func(...args) }, wait)
|
||
}
|
||
}
|
||
}
|
||
var w2utils = new Utils() // eslint-disable-line -- needs to be functional/module scope variable
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
* - removed jQuery dependency
|
||
* - popup.open - returns promise like object
|
||
* - popup.confirm - refactored
|
||
* - popup.message - refactored
|
||
* - removed popup.options.mutliple
|
||
* - refactores w2alert, w2confirm, w2prompt
|
||
* - add w2popup.open().on('')
|
||
* - removed w2popup.restoreTemplate
|
||
* - deprecated onMsgOpen and onMsgClose
|
||
* - deprecated options.bgColor
|
||
* - rename focus -> setFocus
|
||
* - added center() // will auto center on window resize
|
||
* - close(immediate), also refactored if popup is closed when opening
|
||
*/
|
||
|
||
class Dialog extends w2base {
|
||
constructor() {
|
||
super()
|
||
this.defaults = {
|
||
title: '',
|
||
text: '', // just a text (will be centered)
|
||
body: '',
|
||
buttons: '',
|
||
width: 450,
|
||
height: 250,
|
||
focus: null, // brings focus to the element, can be a number or selector
|
||
actions: null, // actions object
|
||
style: '', // style of the message div
|
||
speed: 0.3,
|
||
modal: false,
|
||
maximized: false, // this is a flag to show the state - to open the popup maximized use openMaximized instead
|
||
keyboard: true, // will close popup on esc if not modal
|
||
showClose: true,
|
||
showMax: false,
|
||
transition: null,
|
||
openMaximized: false,
|
||
moved: false
|
||
}
|
||
this.name = 'popup'
|
||
this.status = 'closed' // string that describes current status
|
||
this.onOpen = null
|
||
this.onClose = null
|
||
this.onMax = null
|
||
this.onMin = null
|
||
this.onToggle = null
|
||
this.onKeydown = null
|
||
this.onAction = null
|
||
this.onMove = null
|
||
this.tmp = {}
|
||
// event handler for resize
|
||
this.handleResize = (event) => {
|
||
// if it was moved by the user, do not auto resize
|
||
if (!this.options.moved) {
|
||
this.center(undefined, undefined, true)
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Sample calls
|
||
* - w2popup.open('ddd').ok(() => { w2popup.close() })
|
||
* - w2popup.open('ddd', { height: 120 }).ok(() => { w2popup.close() })
|
||
* - w2popup.open({ body: 'text', title: 'caption', actions: ["Close"] }).close(() => { w2popup.close() })
|
||
* - w2popup.open({ body: 'text', title: 'caption', actions: { Close() { w2popup.close() }} })
|
||
*/
|
||
open(options) {
|
||
let self = this
|
||
if (this.status == 'closing' || query('#w2ui-popup').hasClass('animating')) {
|
||
// if called when previous is closing
|
||
this.close(true)
|
||
}
|
||
// get old options and merge them
|
||
let old_options = this.options
|
||
if (['string', 'number'].includes(typeof options)) {
|
||
options = w2utils.extend({
|
||
title: 'Notification',
|
||
body: `<div class="w2ui-centered">${options}</div>`,
|
||
actions: { Ok() { self.close() }},
|
||
cancelAction: 'ok'
|
||
}, arguments[1] ?? {})
|
||
}
|
||
if (options.text != null) options.body = `<div class="w2ui-centered w2ui-msg-text">${options.text}</div>`
|
||
options = Object.assign({}, this.defaults, old_options, { title: '', body : '' }, options, { maximized: false })
|
||
this.options = options
|
||
// if new - reset event handlers
|
||
if (query('#w2ui-popup').length === 0) {
|
||
this.off('*')
|
||
Object.keys(this).forEach(key => {
|
||
if (key.startsWith('on') && key != 'on') this[key] = null
|
||
})
|
||
}
|
||
// reassign events
|
||
Object.keys(options).forEach(key => {
|
||
if (key.startsWith('on') && key != 'on' && options[key]) {
|
||
this[key] = options[key]
|
||
}
|
||
})
|
||
options.width = parseInt(options.width)
|
||
options.height = parseInt(options.height)
|
||
let edata, msg, tmp
|
||
let { top, left } = this.center()
|
||
let prom = {
|
||
self: this,
|
||
action(callBack) {
|
||
self.on('action.prom', callBack)
|
||
return prom
|
||
},
|
||
close(callBack) {
|
||
self.on('close.prom', callBack)
|
||
return prom
|
||
},
|
||
then(callBack) {
|
||
self.on('open:after.prom', callBack)
|
||
return prom
|
||
}
|
||
}
|
||
// convert action arrays into buttons
|
||
if (options.actions != null && !options.buttons) {
|
||
options.buttons = ''
|
||
Object.keys(options.actions).forEach((action) => {
|
||
let handler = options.actions[action]
|
||
let btnAction = action
|
||
if (typeof handler == 'function') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction" data-click='["action","${action}","event"]'>${action}</button>`
|
||
}
|
||
if (typeof handler == 'object') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction ${handler.class || ''}" name="${action}" data-click='["action","${action}","event"]'
|
||
style="${handler.style}" ${handler.attrs}>${handler.text || action}</button>`
|
||
btnAction = Array.isArray(options.actions) ? handler.text : action
|
||
}
|
||
if (typeof handler == 'string') {
|
||
options.buttons += `<button class="w2ui-btn w2ui-eaction" data-click='["action","${handler}","event"]'>${handler}</button>`
|
||
btnAction = handler
|
||
}
|
||
if (typeof btnAction == 'string') {
|
||
btnAction = btnAction[0].toLowerCase() + btnAction.substr(1).replace(/\s+/g, '')
|
||
}
|
||
prom[btnAction] = function (callBack) {
|
||
self.on('action.buttons', (event) => {
|
||
let target = event.detail.action[0].toLowerCase() + event.detail.action.substr(1).replace(/\s+/g, '')
|
||
if (target == btnAction) callBack(event)
|
||
})
|
||
return prom
|
||
}
|
||
})
|
||
}
|
||
// check if message is already displayed
|
||
if (query('#w2ui-popup').length === 0) {
|
||
// trigger event
|
||
edata = this.trigger('open', { target: 'popup', present: false })
|
||
if (edata.isCancelled === true) return
|
||
this.status = 'opening'
|
||
// output message
|
||
w2utils.lock(document.body, {
|
||
opacity: 0.3,
|
||
onClick: options.modal ? null : () => { this.close() }
|
||
})
|
||
let btn = ''
|
||
if (options.showClose) {
|
||
btn += `<div class="w2ui-popup-button w2ui-popup-close">
|
||
<span class="w2ui-icon w2ui-icon-cross w2ui-eaction" data-mousedown="stop" data-click="close"></span>
|
||
</div>`
|
||
}
|
||
if (options.showMax) {
|
||
btn += `<div class="w2ui-popup-button w2ui-popup-max">
|
||
<span class="w2ui-icon w2ui-icon-box w2ui-eaction" data-mousedown="stop" data-click="toggle"></span>
|
||
</div>`
|
||
}
|
||
// first insert just body
|
||
let styles = `
|
||
left: ${left}px;
|
||
top: ${top}px;
|
||
width: ${parseInt(options.width)}px;
|
||
height: ${parseInt(options.height)}px;
|
||
transition: ${options.speed}s
|
||
`
|
||
msg = `<div id="w2ui-popup" class="w2ui-popup w2ui-anim-open animating" style="${w2utils.stripSpaces(styles)}"></div>`
|
||
query('body').append(msg)
|
||
query('#w2ui-popup')[0]._w2popup = {
|
||
self: this,
|
||
created: new Promise((resolve) => { this._promCreated = resolve }),
|
||
opened: new Promise((resolve) => { this._promOpened = resolve }),
|
||
closing: new Promise((resolve) => { this._promClosing = resolve }),
|
||
closed: new Promise((resolve) => { this._promClosed = resolve }),
|
||
}
|
||
// then content
|
||
styles = `${!options.title ? 'top: 0px !important;' : ''} ${!options.buttons ? 'bottom: 0px !important;' : ''}`
|
||
msg = `
|
||
<span name="hidden-first" tabindex="0" style="position: absolute; top: -100px"></span>
|
||
<div class="w2ui-popup-title-btns">${btn}</div>
|
||
<div class="w2ui-popup-title" style="${!options.title ? 'display: none' : ''}"></div>
|
||
<div class="w2ui-box" style="${styles}">
|
||
<div class="w2ui-popup-body ${!options.title || ' w2ui-popup-no-title'}
|
||
${!options.buttons || ' w2ui-popup-no-buttons'}" style="${options.style}">
|
||
</div>
|
||
</div>
|
||
<div class="w2ui-popup-buttons" style="${!options.buttons ? 'display: none' : ''}"></div>
|
||
<span name="hidden-last" tabindex="0" style="position: absolute; top: -100px"></span>
|
||
`
|
||
query('#w2ui-popup').html(msg)
|
||
if (options.title) query('#w2ui-popup .w2ui-popup-title').append(w2utils.lang(options.title))
|
||
if (options.buttons) query('#w2ui-popup .w2ui-popup-buttons').append(options.buttons)
|
||
if (options.body) query('#w2ui-popup .w2ui-popup-body').append(options.body)
|
||
// allow element to render
|
||
setTimeout(() => {
|
||
query('#w2ui-popup')
|
||
.css('transition', options.speed + 's')
|
||
.removeClass('w2ui-anim-open')
|
||
w2utils.bindEvents('#w2ui-popup .w2ui-eaction', this)
|
||
query('#w2ui-popup').find('.w2ui-popup-body').show()
|
||
this._promCreated()
|
||
}, 1)
|
||
// clean transform
|
||
clearTimeout(this._timer)
|
||
this._timer = setTimeout(() => {
|
||
this.status = 'open'
|
||
self.setFocus(options.focus)
|
||
// event after
|
||
edata.finish()
|
||
this._promOpened()
|
||
query('#w2ui-popup').removeClass('animating')
|
||
}, options.speed * 1000)
|
||
} else {
|
||
// trigger event
|
||
edata = this.trigger('open', { target: 'popup', present: true })
|
||
if (edata.isCancelled === true) return
|
||
// check if size changed
|
||
this.status = 'opening'
|
||
if (old_options != null) {
|
||
if (!old_options.maximized && (old_options.width != options.width || old_options.height != options.height)) {
|
||
this.resize(options.width, options.height)
|
||
}
|
||
options.prevSize = options.width + 'px:' + options.height + 'px'
|
||
options.maximized = old_options.maximized
|
||
}
|
||
// show new items
|
||
let cloned = query('#w2ui-popup .w2ui-box').get(0).cloneNode(true)
|
||
query(cloned).removeClass('w2ui-box').addClass('w2ui-box-temp').find('.w2ui-popup-body').empty().append(options.body)
|
||
query('#w2ui-popup .w2ui-box').after(cloned)
|
||
if (options.buttons) {
|
||
query('#w2ui-popup .w2ui-popup-buttons').show().html('').append(options.buttons)
|
||
query('#w2ui-popup .w2ui-popup-body').removeClass('w2ui-popup-no-buttons')
|
||
query('#w2ui-popup .w2ui-box, #w2ui-popup .w2ui-box-temp').css('bottom', '')
|
||
} else {
|
||
query('#w2ui-popup .w2ui-popup-buttons').hide().html('')
|
||
query('#w2ui-popup .w2ui-popup-body').addClass('w2ui-popup-no-buttons')
|
||
query('#w2ui-popup .w2ui-box, #w2ui-popup .w2ui-box-temp').css('bottom', '0px')
|
||
}
|
||
if (options.title) {
|
||
query('#w2ui-popup .w2ui-popup-title')
|
||
.show()
|
||
.html((options.showClose
|
||
? `<div class="w2ui-popup-button w2ui-popup-close">
|
||
<span class="w2ui-icon w2ui-icon-cross w2ui-eaction" data-mousedown="stop" data-click="close"></span>
|
||
</div>`
|
||
: '') +
|
||
(options.showMax
|
||
? `<div class="w2ui-popup-button w2ui-popup-max">
|
||
<span class="w2ui-icon w2ui-icon-box w2ui-eaction" data-mousedown="stop" data-click="toggle"></span>
|
||
</div>`
|
||
: ''))
|
||
.append(options.title)
|
||
query('#w2ui-popup .w2ui-popup-body').removeClass('w2ui-popup-no-title')
|
||
query('#w2ui-popup .w2ui-box, #w2ui-popup .w2ui-box-temp').css('top', '')
|
||
} else {
|
||
query('#w2ui-popup .w2ui-popup-title').hide().html('')
|
||
query('#w2ui-popup .w2ui-popup-body').addClass('w2ui-popup-no-title')
|
||
query('#w2ui-popup .w2ui-box, #w2ui-popup .w2ui-box-temp').css('top', '0px')
|
||
}
|
||
// transition
|
||
let div_old = query('#w2ui-popup .w2ui-box')[0]
|
||
let div_new = query('#w2ui-popup .w2ui-box-temp')[0]
|
||
query('#w2ui-popup').addClass('animating')
|
||
w2utils.transition(div_old, div_new, options.transition, () => {
|
||
// clean up
|
||
query(div_old).remove()
|
||
query(div_new).removeClass('w2ui-box-temp').addClass('w2ui-box')
|
||
let $body = query(div_new).find('.w2ui-popup-body')
|
||
if ($body.length == 1) {
|
||
$body[0].style.cssText = options.style
|
||
$body.show()
|
||
}
|
||
// focus on first button
|
||
self.setFocus(options.focus)
|
||
query('#w2ui-popup').removeClass('animating')
|
||
})
|
||
// call event onOpen
|
||
this.status = 'open'
|
||
edata.finish()
|
||
w2utils.bindEvents('#w2ui-popup .w2ui-eaction', this)
|
||
query('#w2ui-popup').find('.w2ui-popup-body').show()
|
||
}
|
||
if (options.openMaximized) {
|
||
this.max()
|
||
}
|
||
// save new options
|
||
options._last_focus = document.activeElement
|
||
// keyboard events
|
||
if (options.keyboard) {
|
||
query(document.body).on('keydown', (event) => {
|
||
this.keydown(event)
|
||
})
|
||
}
|
||
query(window).on('resize', this.handleResize)
|
||
// initialize move
|
||
tmp = {
|
||
resizing : false,
|
||
mvMove : mvMove,
|
||
mvStop : mvStop
|
||
}
|
||
query('#w2ui-popup .w2ui-popup-title').on('mousedown', function(event) {
|
||
if (!self.options.maximized) mvStart(event)
|
||
})
|
||
return prom
|
||
// handlers
|
||
function mvStart(evt) {
|
||
if (!evt) evt = window.event
|
||
self.status = 'moving'
|
||
let rect = query('#w2ui-popup').get(0).getBoundingClientRect()
|
||
Object.assign(tmp, {
|
||
resizing: true,
|
||
isLocked: query('#w2ui-popup > .w2ui-lock').length == 1 ? true : false,
|
||
x : evt.screenX,
|
||
y : evt.screenY,
|
||
pos_x : rect.x,
|
||
pos_y : rect.y,
|
||
})
|
||
if (!tmp.isLocked) self.lock({ opacity: 0 })
|
||
query(document.body)
|
||
.on('mousemove.w2ui-popup', tmp.mvMove)
|
||
.on('mouseup.w2ui-popup', tmp.mvStop)
|
||
if (evt.stopPropagation) evt.stopPropagation(); else evt.cancelBubble = true
|
||
if (evt.preventDefault) evt.preventDefault(); else return false
|
||
}
|
||
function mvMove(evt) {
|
||
if (tmp.resizing != true) return
|
||
if (!evt) evt = window.event
|
||
tmp.div_x = evt.screenX - tmp.x
|
||
tmp.div_y = evt.screenY - tmp.y
|
||
// trigger event
|
||
let edata = self.trigger('move', { target: 'popup', div_x: tmp.div_x, div_y: tmp.div_y, originalEvent: evt })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
query('#w2ui-popup').css({
|
||
'transition': 'none',
|
||
'transform' : 'translate3d('+ tmp.div_x +'px, '+ tmp.div_y +'px, 0px)'
|
||
})
|
||
self.options.moved = true
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
function mvStop(evt) {
|
||
if (tmp.resizing != true) return
|
||
if (!evt) evt = window.event
|
||
self.status = 'open'
|
||
tmp.div_x = (evt.screenX - tmp.x)
|
||
tmp.div_y = (evt.screenY - tmp.y)
|
||
query('#w2ui-popup')
|
||
.css({
|
||
'left': (tmp.pos_x + tmp.div_x) + 'px',
|
||
'top' : (tmp.pos_y + tmp.div_y) + 'px'
|
||
})
|
||
.css({
|
||
'transition': 'none',
|
||
'transform' : 'translate3d(0px, 0px, 0px)'
|
||
})
|
||
tmp.resizing = false
|
||
query(document.body).off('.w2ui-popup')
|
||
if (!tmp.isLocked) self.unlock()
|
||
}
|
||
}
|
||
load(options) {
|
||
return new Promise((resolve, reject) => {
|
||
if (typeof options == 'string') {
|
||
options = { url: options }
|
||
}
|
||
if (options.url == null) {
|
||
console.log('ERROR: The url is not defined.')
|
||
reject('The url is not defined')
|
||
return
|
||
}
|
||
this.status = 'loading'
|
||
let [url, selector] = String(options.url).split('#')
|
||
if (url) {
|
||
fetch(url).then(res => res.text()).then(html => {
|
||
resolve(this.template(html, selector, options))
|
||
})
|
||
}
|
||
})
|
||
}
|
||
template(data, id, options = {}) {
|
||
let html
|
||
try {
|
||
html = query(data)
|
||
} catch (e) {
|
||
html = query.html(data)
|
||
}
|
||
if (id) html = html.filter('#' + id)
|
||
Object.assign(options, {
|
||
width: parseInt(query(html).css('width')),
|
||
height: parseInt(query(html).css('height')),
|
||
title: query(html).find('[rel=title]').html(),
|
||
body: query(html).find('[rel=body]').html(),
|
||
buttons: query(html).find('[rel=buttons]').html(),
|
||
style: query(html).find('[rel=body]').get(0).style.cssText,
|
||
})
|
||
return this.open(options)
|
||
}
|
||
action(action, event) {
|
||
let click = this.options.actions[action]
|
||
if (click instanceof Object && click.onClick) click = click.onClick
|
||
// event before
|
||
let edata = this.trigger('action', { action, target: 'popup', self: this,
|
||
originalEvent: event, value: this.input ? this.input.value : null })
|
||
if (edata.isCancelled === true) return
|
||
// default actions
|
||
if (typeof click === 'function') click.call(this, event)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
keydown(event) {
|
||
if (this.options && !this.options.keyboard) return
|
||
// trigger event
|
||
let edata = this.trigger('keydown', { target: 'popup', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
switch (event.keyCode) {
|
||
case 27:
|
||
event.preventDefault()
|
||
if (query('#w2ui-popup .w2ui-message').length == 0) {
|
||
if (this.options.cancelAction) {
|
||
this.action(this.options.cancelAction)
|
||
} else {
|
||
this.close()
|
||
}
|
||
}
|
||
break
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
close(immediate) {
|
||
// trigger event
|
||
let edata = this.trigger('close', { target: 'popup' })
|
||
if (edata.isCancelled === true) return
|
||
let cleanUp = () => {
|
||
// return template
|
||
query('#w2ui-popup').remove()
|
||
// restore active
|
||
if (this.options._last_focus && this.options._last_focus.length > 0) this.options._last_focus.focus()
|
||
this.status = 'closed'
|
||
this.options = {}
|
||
// event after
|
||
edata.finish()
|
||
this._promClosed()
|
||
}
|
||
if (query('#w2ui-popup').length === 0 || this.status == 'closed') { // already closed
|
||
return
|
||
}
|
||
if (this.status == 'opening') { // if it is opening
|
||
immediate = true
|
||
}
|
||
if (this.status == 'closing' && immediate === true) {
|
||
cleanUp()
|
||
clearTimeout(this.tmp.closingTimer)
|
||
w2utils.unlock(document.body, 0)
|
||
return
|
||
}
|
||
// default behavior
|
||
this.status = 'closing'
|
||
query('#w2ui-popup')
|
||
.css('transition', this.options.speed + 's')
|
||
.addClass('w2ui-anim-close animating')
|
||
w2utils.unlock(document.body, 300)
|
||
this._promClosing()
|
||
if (immediate) {
|
||
cleanUp()
|
||
} else {
|
||
this.tmp.closingTimer = setTimeout(cleanUp, this.options.speed * 1000)
|
||
}
|
||
// remove keyboard events
|
||
if (this.options.keyboard) {
|
||
query(document.body).off('keydown', this.keydown)
|
||
}
|
||
query(window).off('resize', this.handleResize)
|
||
}
|
||
toggle() {
|
||
let edata = this.trigger('toggle', { target: 'popup' })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (this.options.maximized === true) this.min(); else this.max()
|
||
// event after
|
||
setTimeout(() => {
|
||
edata.finish()
|
||
}, (this.options.speed * 1000) + 50)
|
||
}
|
||
max() {
|
||
if (this.options.maximized === true) return
|
||
// trigger event
|
||
let edata = this.trigger('max', { target: 'popup' })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
this.status = 'resizing'
|
||
let rect = query('#w2ui-popup').get(0).getBoundingClientRect()
|
||
this.options.prevSize = rect.width + ':' + rect.height
|
||
// do resize
|
||
this.resize(10000, 10000, () => {
|
||
this.status = 'open'
|
||
this.options.maximized = true
|
||
edata.finish()
|
||
})
|
||
}
|
||
min() {
|
||
if (this.options.maximized !== true) return
|
||
let size = this.options.prevSize.split(':')
|
||
// trigger event
|
||
let edata = this.trigger('min', { target: 'popup' })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
this.status = 'resizing'
|
||
// do resize
|
||
this.options.maximized = false
|
||
this.resize(parseInt(size[0]), parseInt(size[1]), () => {
|
||
this.status = 'open'
|
||
this.options.prevSize = null
|
||
edata.finish()
|
||
})
|
||
}
|
||
clear() {
|
||
query('#w2ui-popup .w2ui-popup-title').html('')
|
||
query('#w2ui-popup .w2ui-popup-body').html('')
|
||
query('#w2ui-popup .w2ui-popup-buttons').html('')
|
||
}
|
||
reset() {
|
||
this.open(this.defaults)
|
||
}
|
||
message(options) {
|
||
return w2utils.message({
|
||
owner: this,
|
||
box : query('#w2ui-popup').get(0),
|
||
after: '.w2ui-popup-title'
|
||
}, options)
|
||
}
|
||
confirm(options) {
|
||
return w2utils.confirm({
|
||
owner: this,
|
||
box : query('#w2ui-popup'),
|
||
after: '.w2ui-popup-title'
|
||
}, options)
|
||
}
|
||
setFocus(focus) {
|
||
let box = query('#w2ui-popup')
|
||
let sel = 'input, button, select, textarea, [contentEditable], .w2ui-input'
|
||
if (focus != null) {
|
||
let el = isNaN(focus)
|
||
? box.find(sel).filter(focus).get(0)
|
||
: box.find(sel).get(focus)
|
||
el?.focus()
|
||
} else {
|
||
let el = box.find('[name=hidden-first]').get(0)
|
||
if (el) el.focus()
|
||
}
|
||
// keep focus/blur inside popup
|
||
query(box).find(sel + ',[name=hidden-first],[name=hidden-last]')
|
||
.off('.keep-focus')
|
||
.on('blur.keep-focus', function (event) {
|
||
setTimeout(() => {
|
||
let focus = document.activeElement
|
||
let inside = query(box).find(sel).filter(focus).length > 0
|
||
let name = query(focus).attr('name')
|
||
if (!inside && focus && focus !== document.body) {
|
||
query(box).find(sel).get(0)?.focus()
|
||
}
|
||
if (name == 'hidden-last') {
|
||
query(box).find(sel).get(0)?.focus()
|
||
}
|
||
if (name == 'hidden-first') {
|
||
query(box).find(sel).get(-1)?.focus()
|
||
}
|
||
}, 1)
|
||
})
|
||
}
|
||
lock(msg, showSpinner) {
|
||
let args = Array.from(arguments)
|
||
args.unshift(query('#w2ui-popup'))
|
||
w2utils.lock(...args)
|
||
}
|
||
unlock(speed) {
|
||
w2utils.unlock(query('#w2ui-popup'), speed)
|
||
}
|
||
center(width, height, force) {
|
||
let maxW, maxH
|
||
if (window.innerHeight == undefined) {
|
||
maxW = parseInt(document.documentElement.offsetWidth)
|
||
maxH = parseInt(document.documentElement.offsetHeight)
|
||
} else {
|
||
maxW = parseInt(window.innerWidth)
|
||
maxH = parseInt(window.innerHeight)
|
||
}
|
||
width = parseInt(width ?? this.options.width)
|
||
height = parseInt(height ?? this.options.height)
|
||
if (this.options.maximized === true) {
|
||
width = maxW
|
||
height = maxH
|
||
}
|
||
if (maxW - 10 < width) width = maxW - 10
|
||
if (maxH - 10 < height) height = maxH - 10
|
||
let top = (maxH - height) / 2
|
||
let left = (maxW - width) / 2
|
||
if (force) {
|
||
query('#w2ui-popup').css({
|
||
'transition': 'none',
|
||
'top' : top + 'px',
|
||
'left' : left + 'px',
|
||
'width' : width + 'px',
|
||
'height': height + 'px'
|
||
})
|
||
this.resizeMessages() // then messages resize nicely
|
||
}
|
||
return { top, left, width, height }
|
||
}
|
||
resize(newWidth, newHeight, callBack) {
|
||
let self = this
|
||
if (this.options.speed == null) this.options.speed = 0
|
||
// calculate new position
|
||
let { top, left, width, height } = this.center(newWidth, newHeight)
|
||
let speed = this.options.speed
|
||
query('#w2ui-popup').css({
|
||
'transition': `${speed}s width, ${speed}s height, ${speed}s left, ${speed}s top`,
|
||
'top' : top + 'px',
|
||
'left' : left + 'px',
|
||
'width' : width + 'px',
|
||
'height': height + 'px'
|
||
})
|
||
let tmp_int = setInterval(() => { self.resizeMessages() }, 10) // then messages resize nicely
|
||
setTimeout(() => {
|
||
clearInterval(tmp_int)
|
||
self.resizeMessages()
|
||
if (typeof callBack == 'function') callBack()
|
||
}, (this.options.speed * 1000) + 50) // give extra 50 ms
|
||
}
|
||
// internal function
|
||
resizeMessages() {
|
||
// see if there are messages and resize them
|
||
query('#w2ui-popup .w2ui-message').each(msg => {
|
||
let mopt = msg._msg_options
|
||
let popup = query('#w2ui-popup')
|
||
if (parseInt(mopt.width) < 10) mopt.width = 10
|
||
if (parseInt(mopt.height) < 10) mopt.height = 10
|
||
let rect = popup[0].getBoundingClientRect()
|
||
let titleHeight = parseInt(popup.find('.w2ui-popup-title')[0].clientHeight)
|
||
let pWidth = parseInt(rect.width)
|
||
let pHeight = parseInt(rect.height)
|
||
// re-calc width
|
||
mopt.width = mopt.originalWidth
|
||
if (mopt.width > pWidth - 10) {
|
||
mopt.width = pWidth - 10
|
||
}
|
||
// re-calc height
|
||
mopt.height = mopt.originalHeight
|
||
if (mopt.height > pHeight - titleHeight - 5) {
|
||
mopt.height = pHeight - titleHeight - 5
|
||
}
|
||
if (mopt.originalHeight < 0) mopt.height = pHeight + mopt.originalHeight - titleHeight
|
||
if (mopt.originalWidth < 0) mopt.width = pWidth + mopt.originalWidth * 2 // x 2 because there is left and right margin
|
||
query(msg).css({
|
||
left : ((pWidth - mopt.width) / 2) + 'px',
|
||
width : mopt.width + 'px',
|
||
height : mopt.height + 'px'
|
||
})
|
||
})
|
||
}
|
||
}
|
||
function w2alert(msg, title, callBack) {
|
||
let prom
|
||
let options = {
|
||
title: w2utils.lang(title ?? 'Notification'),
|
||
body: `<div class="w2ui-centered w2ui-msg-text">${msg}</div>`,
|
||
showClose: false,
|
||
actions: ['Ok'],
|
||
cancelAction: 'ok'
|
||
}
|
||
if (query('#w2ui-popup').length > 0 && w2popup.status != 'closing') {
|
||
prom = w2popup.message(options)
|
||
} else {
|
||
prom = w2popup.open(options)
|
||
}
|
||
prom.ok((event) => {
|
||
if (typeof event.detail.self?.close == 'function') {
|
||
event.detail.self.close()
|
||
}
|
||
if (typeof callBack == 'function') callBack()
|
||
})
|
||
return prom
|
||
}
|
||
function w2confirm(msg, title, callBack) {
|
||
let prom
|
||
let options = msg
|
||
if (['string', 'number'].includes(typeof options)) {
|
||
options = { msg: options }
|
||
}
|
||
if (options.msg) {
|
||
options.body = `<div class="w2ui-centered w2ui-msg-text">${options.msg}</div>`,
|
||
delete options.msg
|
||
}
|
||
w2utils.extend(options, {
|
||
title: w2utils.lang(title ?? 'Confirmation'),
|
||
showClose: false,
|
||
modal: true,
|
||
cancelAction: 'no'
|
||
})
|
||
w2utils.normButtons(options, { yes: 'Yes', no: 'No' })
|
||
if (query('#w2ui-popup').length > 0 && w2popup.status != 'closing') {
|
||
prom = w2popup.message(options)
|
||
} else {
|
||
prom = w2popup.open(options)
|
||
}
|
||
prom.self
|
||
.off('.confirm')
|
||
.on('action:after.confirm', (event) => {
|
||
if (typeof event.detail.self?.close == 'function') {
|
||
event.detail.self.close()
|
||
}
|
||
if (typeof callBack == 'function') callBack(event.detail.action)
|
||
})
|
||
return prom
|
||
}
|
||
function w2prompt(label, title, callBack) {
|
||
let prom
|
||
let options = label
|
||
if (['string', 'number'].includes(typeof options)) {
|
||
options = { label: options }
|
||
}
|
||
if (options.label) {
|
||
options.focus = 0
|
||
options.body = (options.textarea
|
||
? `<div class="w2ui-prompt textarea">
|
||
<div>${options.label}</div>
|
||
<textarea id="w2prompt" class="w2ui-input" ${options.attrs ?? ''}
|
||
data-keydown="keydown|event" data-keyup="change|event">${options.value??''}</textarea>
|
||
</div>`
|
||
: `<div class="w2ui-prompt w2ui-centered">
|
||
<label>${options.label}</label>
|
||
<input id="w2prompt" class="w2ui-input" ${options.attrs ?? ''}
|
||
data-keydown="keydown|event" data-keyup="change|event" value="${options.value??''}">
|
||
</div>`
|
||
)
|
||
}
|
||
w2utils.extend(options, {
|
||
title: w2utils.lang(title ?? 'Notification'),
|
||
showClose: false,
|
||
modal: true,
|
||
cancelAction: 'cancel'
|
||
})
|
||
w2utils.normButtons(options, { ok: 'Ok', cancel: 'Cancel' })
|
||
if (query('#w2ui-popup').length > 0 && w2popup.status != 'closing') {
|
||
prom = w2popup.message(options)
|
||
} else {
|
||
prom = w2popup.open(options)
|
||
}
|
||
if (prom.self.box) {
|
||
prom.self.input = query(prom.self.box).find('#w2prompt').get(0)
|
||
} else {
|
||
prom.self.input = query('#w2ui-popup .w2ui-popup-body #w2prompt').get(0)
|
||
}
|
||
if (options.value !== null) {
|
||
prom.self.input.select()
|
||
}
|
||
prom.change = function (callback) {
|
||
prom.self.on('change', callback)
|
||
return this
|
||
}
|
||
prom.self
|
||
.off('.prompt')
|
||
.on('open:after.prompt', (event) => {
|
||
let box = event.detail.box ? event.detail.box : query('#w2ui-popup .w2ui-popup-body').get(0)
|
||
w2utils.bindEvents(query(box).find('#w2prompt'), {
|
||
keydown(evt) {
|
||
if (evt.keyCode == 27) evt.stopPropagation()
|
||
},
|
||
change(evt) {
|
||
let edata = prom.self.trigger('change', { target: 'prompt', originalEvent: evt })
|
||
if (edata.isCancelled === true) return
|
||
if (evt.keyCode == 13 && evt.ctrlKey) {
|
||
prom.self.action('Ok', evt)
|
||
}
|
||
if (evt.keyCode == 27) {
|
||
prom.self.action('Cancel', evt)
|
||
}
|
||
edata.finish()
|
||
}
|
||
})
|
||
query(box).find('.w2ui-eaction').trigger('keyup')
|
||
})
|
||
.on('action:after.prompt', (event) => {
|
||
if (typeof event.detail.self?.close == 'function') {
|
||
event.detail.self.close()
|
||
}
|
||
if (typeof callBack == 'function') callBack(event.detail.action)
|
||
})
|
||
return prom
|
||
}
|
||
let w2popup = new Dialog()
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base
|
||
*
|
||
* 2.0 Changes
|
||
* - multiple tooltips to the same anchor
|
||
*
|
||
* TODO
|
||
* - load menu items from URL
|
||
*/
|
||
|
||
class Tooltip {
|
||
// no need to extend w2base, as each individual tooltip extends it
|
||
static active = {} // all defined tooltips
|
||
constructor() {
|
||
this.defaults = {
|
||
name : null, // name for the overlay, otherwise input id is used
|
||
html : '', // text or html
|
||
style : '', // additional style for the overlay
|
||
class : '', // add class for w2ui-tooltip-body
|
||
position : 'top|bottom', // can be left, right, top, bottom
|
||
align : '', // can be: both, both:XX left, right, both, top, bottom
|
||
anchor : null, // element it is attached to, if anchor is body, then it is context menu
|
||
anchorClass : '', // add class for anchor when tooltip is shown
|
||
anchorStyle : '', // add style for anchor when tooltip is shown
|
||
autoShow : false, // if autoShow true, then tooltip will show on mouseEnter and hide on mouseLeave
|
||
autoShowOn : null, // when options.autoShow = true, mouse event to show on
|
||
autoHideOn : null, // when options.autoShow = true, mouse event to hide on
|
||
arrowSize : 8, // size of the carret
|
||
margin : 0, // extra margin from the anchor
|
||
screenMargin : 2, // min margin from screen to tooltip
|
||
autoResize : true, // auto resize based on content size and available size
|
||
margin : 1, // distance from the anchor
|
||
offsetX : 0, // delta for left coordinate
|
||
offsetY : 0, // delta for top coordinate
|
||
maxWidth : null, // max width
|
||
maxHeight : null, // max height
|
||
watchScroll : null, // attach to onScroll event // TODO:
|
||
watchResize : null, // attach to onResize event // TODO:
|
||
hideOn : null, // events when to hide tooltip, ['click', 'change', 'key', 'focus', 'blur'],
|
||
onThen : null, // called when displayed
|
||
onShow : null, // callBack when shown
|
||
onHide : null, // callBack when hidden
|
||
onUpdate : null, // callback when tooltip gets updated
|
||
onMove : null // callback when tooltip is moved
|
||
}
|
||
}
|
||
static observeRemove = new MutationObserver((mutations) => {
|
||
let cnt = 0
|
||
Object.keys(Tooltip.active).forEach(name => {
|
||
let overlay = Tooltip.active[name]
|
||
if (overlay.displayed) {
|
||
if (!overlay.anchor || !overlay.anchor.isConnected) {
|
||
overlay.hide()
|
||
} else {
|
||
cnt++
|
||
}
|
||
}
|
||
})
|
||
// remove observer, as there is no active tooltips
|
||
if (cnt === 0) {
|
||
Tooltip.observeRemove.disconnect()
|
||
}
|
||
})
|
||
trigger(event, data) {
|
||
if (arguments.length == 2) {
|
||
let type = event
|
||
event = data
|
||
data.type = type
|
||
}
|
||
if (event.overlay) {
|
||
return event.overlay.trigger(event)
|
||
} else {
|
||
console.log('ERROR: cannot find overlay where to trigger events')
|
||
}
|
||
}
|
||
get(name) {
|
||
if (arguments.length == 0) {
|
||
return Object.keys(Tooltip.active)
|
||
} else if (name === true) {
|
||
return Tooltip.active
|
||
} else {
|
||
return Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
}
|
||
}
|
||
attach(anchor, text) {
|
||
let options, overlay
|
||
let self = this
|
||
if (arguments.length == 0) {
|
||
return
|
||
} else if (arguments.length == 1 && anchor.anchor) {
|
||
options = anchor
|
||
anchor = options.anchor
|
||
} else if (arguments.length === 2 && typeof text === 'string') {
|
||
options = { anchor, html: text }
|
||
text = options.html
|
||
} else if (arguments.length === 2 && text != null && typeof text === 'object') {
|
||
options = text
|
||
text = options.html
|
||
}
|
||
options = w2utils.extend({}, this.defaults, options || {})
|
||
if (!text && options.text) text = options.text
|
||
if (!text && options.html) text = options.html
|
||
// anchor is func var
|
||
delete options.anchor
|
||
// define tooltip
|
||
let name = (options.name ? options.name : anchor.id)
|
||
if (anchor == document || anchor == document.body) {
|
||
anchor = document.body
|
||
name = 'context-menu'
|
||
}
|
||
if (!name) {
|
||
name = 'noname-' + Object.keys(Tooltip.active).length
|
||
console.log('NOTICE: name property is not defined for tooltip, could lead to too many instances')
|
||
}
|
||
// clean name as it is used as id and css selector
|
||
name = name.replace(/[\s\.#]/g, '_')
|
||
if (Tooltip.active[name]) {
|
||
overlay = Tooltip.active[name]
|
||
overlay.prevOptions = overlay.options
|
||
overlay.options = options // do not merge or extend, otherwiser menu items get merged too
|
||
// overlay.options = w2utils.extend({}, overlay.options, options)
|
||
overlay.anchor = anchor // as HTML elements are not copied
|
||
if (overlay.prevOptions.html != overlay.options.html || overlay.prevOptions.class != overlay.options.class
|
||
|| overlay.prevOptions.style != overlay.options.style) {
|
||
overlay.needsUpdate = true
|
||
}
|
||
options = overlay.options // it was recreated
|
||
} else {
|
||
overlay = new w2base()
|
||
Object.assign(overlay, {
|
||
id: 'w2overlay-' + name, name, options, anchor,
|
||
displayed: false,
|
||
tmp: {
|
||
observeResize: new ResizeObserver(() => {
|
||
this.resize(overlay.name)
|
||
})
|
||
},
|
||
hide() {
|
||
self.hide(name)
|
||
}
|
||
})
|
||
Tooltip.active[name] = overlay
|
||
}
|
||
// move events on to overlay layer
|
||
Object.keys(overlay.options).forEach(key => {
|
||
let val = overlay.options[key]
|
||
if (key.startsWith('on') && typeof val == 'function') {
|
||
overlay[key] = val
|
||
delete overlay.options[key]
|
||
}
|
||
})
|
||
// add event for auto show/hide
|
||
if (options.autoShow === true) {
|
||
options.autoShowOn = options.autoShowOn ?? 'mouseenter'
|
||
options.autoHideOn = options.autoHideOn ?? 'mouseleave'
|
||
options.autoShow = false
|
||
}
|
||
if (options.autoShowOn) {
|
||
let scope = 'autoShow-' + overlay.name
|
||
query(anchor)
|
||
.off(`.${scope}`)
|
||
.on(`${options.autoShowOn}.${scope}`, event => {
|
||
self.show(overlay.name)
|
||
event.stopPropagation()
|
||
})
|
||
delete options.autoShowOn
|
||
}
|
||
if (options.autoHideOn) {
|
||
let scope = 'autoHide-' + overlay.name
|
||
query(anchor)
|
||
.off(`.${scope}`)
|
||
.on(`${options.autoHideOn}.${scope}`, event => {
|
||
self.hide(overlay.name)
|
||
event.stopPropagation()
|
||
})
|
||
delete options.autoHideOn
|
||
}
|
||
overlay.off('.attach')
|
||
let ret = {
|
||
overlay,
|
||
then: (callback) => {
|
||
overlay.on('show:after.attach', event => { callback(event) })
|
||
return ret
|
||
},
|
||
show: (callback) => {
|
||
overlay.on('show.attach', event => { callback(event) })
|
||
return ret
|
||
},
|
||
hide: (callback) => {
|
||
overlay.on('hide.attach', event => { callback(event) })
|
||
return ret
|
||
},
|
||
update: (callback) => {
|
||
overlay.on('update.attach', event => { callback(event) })
|
||
return ret
|
||
},
|
||
move: (callback) => {
|
||
overlay.on('move.attach', event => { callback(event) })
|
||
return ret
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
update(name, html) {
|
||
let overlay = Tooltip.active[name]
|
||
if (overlay) {
|
||
overlay.needsUpdate = true
|
||
overlay.options.html = html
|
||
this.show(name)
|
||
} else {
|
||
console.log(`Tooltip "${name}" is not displayed. Cannot update it.`)
|
||
}
|
||
}
|
||
show(name) {
|
||
if (name instanceof HTMLElement || name instanceof Object) {
|
||
let options = name
|
||
if (name instanceof HTMLElement) {
|
||
options = arguments[1] || {}
|
||
options.anchor = name
|
||
}
|
||
let ret = this.attach(options)
|
||
query(ret.overlay.anchor)
|
||
.off('.autoShow-' + ret.overlay.name)
|
||
.off('.autoHide-' + ret.overlay.name)
|
||
// need a timer, so that events would be preperty set
|
||
setTimeout(() => { this.show(ret.overlay.name) }, 1)
|
||
return ret
|
||
}
|
||
let edata
|
||
let self = this
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
if (!overlay) return
|
||
let options = overlay.options
|
||
if (!overlay || (overlay.displayed && !overlay.needsUpdate)) {
|
||
this.resize(overlay?.name)
|
||
return
|
||
}
|
||
let position = options.position.split('|')
|
||
let isVertical = ['top', 'bottom'].includes(position[0])
|
||
// enforce nowrap only when align=both and vertical
|
||
let overlayStyles = (options.align == 'both' && isVertical ? '' : 'white-space: nowrap;')
|
||
if (options.maxWidth && w2utils.getStrWidth(options.html, '') > options.maxWidth) {
|
||
overlayStyles = 'width: '+ options.maxWidth + 'px; white-space: inherit; overflow: auto;'
|
||
}
|
||
overlayStyles += ' max-height: '+ (options.maxHeight ? options.maxHeight : window.innerHeight - 40) + 'px;'
|
||
// if empty content - then hide it
|
||
if (options.html === '' || options.html == null) {
|
||
self.hide(name)
|
||
return
|
||
} else if (overlay.box) {
|
||
// if already present, update it
|
||
edata = this.trigger('update', { target: name, overlay })
|
||
if (edata.isCancelled === true) {
|
||
// restore previous options
|
||
if (overlay.prevOptions) {
|
||
overlay.options = overlay.prevOptions
|
||
delete overlay.prevOptions
|
||
}
|
||
return
|
||
}
|
||
query(overlay.box)
|
||
.find('.w2ui-overlay-body')
|
||
.attr('style', (options.style || '') + '; ' + overlayStyles)
|
||
.removeClass() // removes all classes
|
||
.addClass('w2ui-overlay-body ' + options.class)
|
||
.html(options.html)
|
||
this.resize(overlay.name)
|
||
} else {
|
||
// event before
|
||
edata = this.trigger('show', { target: name, overlay })
|
||
if (edata.isCancelled === true) return
|
||
// normal processing
|
||
query('body').append(
|
||
// pointer-events will be re-enabled leter
|
||
`<div id="${overlay.id}" name="${name}" style="display: none; pointer-events: none" class="w2ui-overlay"
|
||
data-click="stop" data-focusin="stop">
|
||
<style></style>
|
||
<div class="w2ui-overlay-body ${options.class}" style="${options.style || ''}; ${overlayStyles}">
|
||
${options.html}
|
||
</div>
|
||
</div>`)
|
||
overlay.box = query('#'+w2utils.escapeId(overlay.id))[0]
|
||
overlay.displayed = true
|
||
let names = query(overlay.anchor).data('tooltipName') ?? []
|
||
names.push(name)
|
||
query(overlay.anchor).data('tooltipName', names) // make available to element overlay attached to
|
||
w2utils.bindEvents(overlay.box, {})
|
||
// remember anchor's original styles
|
||
overlay.tmp.originalCSS = ''
|
||
if (query(overlay.anchor).length > 0) {
|
||
overlay.tmp.originalCSS = query(overlay.anchor)[0].style.cssText
|
||
}
|
||
this.resize(overlay.name)
|
||
}
|
||
if (options.anchorStyle) {
|
||
overlay.anchor.style.cssText += ';' + options.anchorStyle
|
||
}
|
||
if (options.anchorClass) {
|
||
// do not add w2ui-focus to body
|
||
if (!(options.anchorClass == 'w2ui-focus' && overlay.anchor == document.body)) {
|
||
query(overlay.anchor).addClass(options.anchorClass)
|
||
}
|
||
}
|
||
// add on hide events
|
||
if (typeof options.hideOn == 'string') options.hideOn = [options.hideOn]
|
||
if (!Array.isArray(options.hideOn)) options.hideOn = []
|
||
// initial scroll
|
||
Object.assign(overlay.tmp, {
|
||
scrollLeft: document.body.scrollLeft,
|
||
scrollTop: document.body.scrollTop
|
||
})
|
||
addHideEvents()
|
||
addWatchEvents(document.body)
|
||
// first show empty tooltip, so it will popup up in the right position
|
||
query(overlay.box).show()
|
||
overlay.tmp.observeResize.observe(overlay.box)
|
||
// observer element removal from DOM
|
||
Tooltip.observeRemove.observe(document.body, { subtree: true, childList: true })
|
||
// then insert html and it will adjust
|
||
query(overlay.box)
|
||
.css('opacity', 1)
|
||
.find('.w2ui-overlay-body')
|
||
.html(options.html)
|
||
/**
|
||
* pointer-events: none is needed to avoid cases when popup is shown right under the cursor
|
||
* or it will trigger onmouseout, onmouseleave and other events.
|
||
*/
|
||
setTimeout(() => { query(overlay.box).css({ 'pointer-events': 'auto' }).data('ready', 'yes') }, 100)
|
||
delete overlay.needsUpdate
|
||
// expose overlay to DOM element
|
||
overlay.box.overlay = overlay
|
||
// event after
|
||
if (edata) edata.finish()
|
||
return { overlay }
|
||
function addWatchEvents(el) {
|
||
let scope = 'tooltip-' + overlay.name
|
||
let queryEl = el
|
||
if (el.tagName == 'BODY') {
|
||
queryEl = el.ownerDocument
|
||
}
|
||
query(queryEl)
|
||
.off(`.${scope}`)
|
||
.on(`scroll.${scope}`, event => {
|
||
Object.assign(overlay.tmp, {
|
||
scrollLeft: el.scrollLeft,
|
||
scrollTop: el.scrollTop
|
||
})
|
||
self.resize(overlay.name)
|
||
})
|
||
}
|
||
function addHideEvents() {
|
||
let hide = (event) => { self.hide(overlay.name) }
|
||
let $anchor = query(overlay.anchor)
|
||
let scope = 'tooltip-' + overlay.name
|
||
// document click
|
||
query('body').off(`.${scope}`)
|
||
if (options.hideOn.includes('doc-click')) {
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
// otherwise hides on click to focus
|
||
$anchor
|
||
.off(`.${scope}-doc`)
|
||
.on(`click.${scope}-doc`, (event) => { event.stopPropagation() })
|
||
}
|
||
query('body').on(`click.${scope}`, hide)
|
||
}
|
||
if (options.hideOn.includes('focus-change')) {
|
||
query('body')
|
||
.on(`focusin.${scope}`, (e) => {
|
||
if (document.activeElement != overlay.anchor) {
|
||
self.hide(overlay.name)
|
||
}
|
||
})
|
||
}
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
$anchor.off(`.${scope}`)
|
||
options.hideOn.forEach(event => {
|
||
if (['doc-click', 'focus-change'].indexOf(event) == -1) {
|
||
$anchor.on(`${event}.${scope}`, { once: true }, hide)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
hide(name) {
|
||
let overlay
|
||
if (arguments.length == 0) {
|
||
// hide all tooltips
|
||
Object.keys(Tooltip.active).forEach(name => { this.hide(name) })
|
||
return
|
||
}
|
||
if (name instanceof HTMLElement) {
|
||
let names = query(name).data('tooltipName') ?? []
|
||
names.forEach(name => { this.hide(name) })
|
||
return
|
||
}
|
||
if (typeof name == 'string') {
|
||
name = name.replace(/[\s\.#]/g, '_')
|
||
overlay = Tooltip.active[name]
|
||
}
|
||
if (!overlay || !overlay.box) return
|
||
delete Tooltip.active[name]
|
||
// event before
|
||
let edata = this.trigger('hide', { target: name, overlay })
|
||
if (edata.isCancelled === true) return
|
||
let scope = 'tooltip-' + overlay.name
|
||
// normal processing
|
||
overlay.tmp.observeResize?.disconnect()
|
||
if (overlay.options.watchScroll) {
|
||
query(overlay.options.watchScroll)
|
||
.off('.w2scroll-' + overlay.name)
|
||
}
|
||
// if no active tooltip then disable observeRemove
|
||
let cnt = 0
|
||
Object.keys(Tooltip.active).forEach(key => {
|
||
let overlay = Tooltip.active[key]
|
||
if (overlay.displayed) {
|
||
cnt++
|
||
}
|
||
})
|
||
if (cnt == 0) {
|
||
Tooltip.observeRemove.disconnect()
|
||
}
|
||
query('body').off(`.${scope}`) // hide to click event here
|
||
query(document).off(`.${scope}`) // scroll event here
|
||
// remove element
|
||
overlay.box.remove()
|
||
overlay.box = null
|
||
overlay.displayed = false
|
||
// remove name from anchor properties
|
||
let names = query(overlay.anchor).data('tooltipName') ?? []
|
||
let ind = names.indexOf(overlay.name)
|
||
if (ind != -1) names.splice(names.indexOf(overlay.name), 1)
|
||
if (names.length == 0) {
|
||
query(overlay.anchor).removeData('tooltipName')
|
||
} else {
|
||
query(overlay.anchor).data('tooltipName', names)
|
||
}
|
||
// restore original CSS
|
||
overlay.anchor.style.cssText = overlay.tmp.originalCSS
|
||
query(overlay.anchor)
|
||
.off(`.${scope}`)
|
||
.removeClass(overlay.options.anchorClass)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
resize(name) {
|
||
if (arguments.length == 0) {
|
||
Object.keys(Tooltip.active).forEach(key => {
|
||
let overlay = Tooltip.active[key]
|
||
if (overlay.displayed) this.resize(overlay.name)
|
||
})
|
||
return
|
||
}
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
let pos = this.getPosition(overlay.name)
|
||
let newPos = pos.left + 'x' + pos.top
|
||
let edata
|
||
if (overlay.tmp.lastPos != newPos) {
|
||
edata = this.trigger('move', { target: name, overlay, pos })
|
||
}
|
||
query(overlay.box)
|
||
.css({
|
||
left: pos.left + 'px',
|
||
top : pos.top + 'px'
|
||
})
|
||
.then(query => {
|
||
if (pos.width != null) {
|
||
query.css('width', pos.width + 'px')
|
||
.find('.w2ui-overlay-body')
|
||
.css('width', '100%')
|
||
}
|
||
if (pos.height != null) {
|
||
query.css('height', pos.height + 'px')
|
||
.find('.w2ui-overlay-body')
|
||
.css('height', '100%')
|
||
}
|
||
})
|
||
.find('.w2ui-overlay-body')
|
||
.removeClass('w2ui-arrow-right w2ui-arrow-left w2ui-arrow-top w2ui-arrow-bottom')
|
||
.addClass(pos.arrow.class)
|
||
.closest('.w2ui-overlay')
|
||
.find('style')
|
||
.text(pos.arrow.style)
|
||
if (overlay.tmp.lastPos != newPos && edata) {
|
||
overlay.tmp.lastPos = newPos
|
||
edata.finish()
|
||
}
|
||
}
|
||
getPosition(name) {
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
if (!overlay || !overlay.box) {
|
||
return
|
||
}
|
||
let options = overlay.options
|
||
if (overlay.tmp.resizedY || overlay.tmp.resizedX) {
|
||
query(overlay.box).css({ width: '', height: '', scroll: 'auto' })
|
||
}
|
||
let scrollSize = w2utils.scrollBarSize()
|
||
let hasScrollBarX = !(document.body.scrollWidth == document.body.clientWidth)
|
||
let hasScrollBarY = !(document.body.scrollHeight == document.body.clientHeight)
|
||
let max = {
|
||
width: window.innerWidth - (hasScrollBarY ? scrollSize : 0),
|
||
height: window.innerHeight - (hasScrollBarX ? scrollSize : 0)
|
||
}
|
||
let position = options.position == 'auto' ? 'top|bottom|right|left'.split('|') : options.position.split('|')
|
||
let isVertical = ['top', 'bottom'].includes(position[0])
|
||
let content = overlay.box.getBoundingClientRect()
|
||
let anchor = overlay.anchor.getBoundingClientRect()
|
||
if (overlay.anchor == document.body) {
|
||
// context menu
|
||
let { x, y, width, height } = options.originalEvent
|
||
anchor = { left: x - 2, top: y - 4, width, height, arrow: 'none' }
|
||
}
|
||
let arrowSize = options.arrowSize
|
||
if (anchor.arrow == 'none') arrowSize = 0
|
||
// space available
|
||
let available = { // tipsize adjustment should be here, not in max.width/max.height
|
||
top: anchor.top,
|
||
bottom: max.height - (anchor.top + anchor.height) - + (hasScrollBarX ? scrollSize : 0),
|
||
left: anchor.left,
|
||
right: max.width - (anchor.left + anchor.width) + (hasScrollBarY ? scrollSize : 0),
|
||
}
|
||
// size of empty tooltip
|
||
if (content.width < 22) content.width = 22
|
||
if (content.height < 14) content.height = 14
|
||
let left, top, width, height // tooltip position
|
||
let found = ''
|
||
let arrow = {
|
||
offset: 0,
|
||
class: '',
|
||
style: `#${overlay.id} { --tip-size: ${arrowSize}px; }`
|
||
}
|
||
let adjust = { left: 0, top: 0 }
|
||
let bestFit = { posX: '', x: 0, posY: '', y: 0 }
|
||
// find best position
|
||
position.forEach(pos => {
|
||
if (['top', 'bottom'].includes(pos)) {
|
||
if (!found && (content.height + arrowSize/1.893) < available[pos]) { // 1.893 = 1 + sin(90)
|
||
found = pos
|
||
}
|
||
if (available[pos] > bestFit.y) {
|
||
Object.assign(bestFit, { posY: pos, y: available[pos] })
|
||
}
|
||
}
|
||
if (['left', 'right'].includes(pos)) {
|
||
if (!found && (content.width + arrowSize/1.893) < available[pos]) { // 1.893 = 1 + sin(90)
|
||
found = pos
|
||
}
|
||
if (available[pos] > bestFit.x) {
|
||
Object.assign(bestFit, { posX: pos, x: available[pos] })
|
||
}
|
||
}
|
||
})
|
||
// if not found, use best (greatest available space) position
|
||
if (!found) {
|
||
if (isVertical) {
|
||
found = bestFit.posY
|
||
} else {
|
||
found = bestFit.posX
|
||
}
|
||
}
|
||
if (options.autoResize) {
|
||
if (['top', 'bottom'].includes(found)) {
|
||
if (content.height > available[found]) {
|
||
height = available[found]
|
||
overlay.tmp.resizedY = true
|
||
} else {
|
||
overlay.tmp.resizedY = false
|
||
}
|
||
}
|
||
if (['left', 'right'].includes(found)) {
|
||
if (content.width > available[found]) {
|
||
width = available[found]
|
||
overlay.tmp.resizedX = true
|
||
} else {
|
||
overlay.tmp.resizedX = false
|
||
}
|
||
}
|
||
}
|
||
usePosition(found)
|
||
if (isVertical) anchorAlignment()
|
||
screenAdjust()
|
||
let extraTop = (found == 'top' ? -options.margin : (found == 'bottom' ? options.margin : 0))
|
||
let extraLeft = (found == 'left' ? -options.margin : (found == 'right' ? options.margin : 0))
|
||
// adjust for scrollbar
|
||
top = Math.floor((top + parseFloat(options.offsetY) + parseFloat(extraTop)) * 100) / 100
|
||
left = Math.floor((left + parseFloat(options.offsetX) + parseFloat(extraLeft)) * 100) / 100
|
||
return { left, top, arrow, adjust, width, height, pos: found }
|
||
function usePosition(pos) {
|
||
arrow.class = anchor.arrow ? anchor.arrow : `w2ui-arrow-${pos}`
|
||
switch (pos) {
|
||
case 'top': {
|
||
left = anchor.left + (anchor.width - (width ?? content.width)) / 2
|
||
top = anchor.top - (height ?? content.height) - arrowSize / 1.5 + 1
|
||
break
|
||
}
|
||
case 'bottom': {
|
||
left = anchor.left + (anchor.width - (width ?? content.width)) / 2
|
||
top = anchor.top + anchor.height + arrowSize / 1.25 + 1
|
||
break
|
||
}
|
||
case 'left': {
|
||
left = anchor.left - (width ?? content.width) - arrowSize / 1.2 - 1
|
||
top = anchor.top + (anchor.height - (height ?? content.height)) / 2
|
||
break
|
||
}
|
||
case 'right': {
|
||
left = anchor.left + anchor.width + arrowSize / 1.2 + 1
|
||
top = anchor.top + (anchor.height - (height ?? content.height)) / 2
|
||
break
|
||
}
|
||
}
|
||
}
|
||
function anchorAlignment() {
|
||
// top/bottom alignments
|
||
if (options.align == 'left') {
|
||
adjust.left = anchor.left - left
|
||
left = anchor.left
|
||
}
|
||
if (options.align == 'right') {
|
||
adjust.left = (anchor.left + anchor.width - (width ?? content.width)) - left
|
||
left = anchor.left + anchor.width - (width ?? content.width)
|
||
}
|
||
if (['top', 'bottom'].includes(found) && options.align.startsWith('both')) {
|
||
let minWidth = options.align.split(':')[1] ?? 50
|
||
if (anchor.width >= minWidth) {
|
||
left = anchor.left
|
||
width = anchor.width
|
||
}
|
||
}
|
||
// left/right alignments
|
||
if (options.align == 'top') {
|
||
adjust.top = anchor.top - top
|
||
top = anchor.top
|
||
}
|
||
if (options.align == 'bottom') {
|
||
adjust.top = (anchor.top + anchor.height - (height ?? content.height)) - top
|
||
top = anchor.top + anchor.height - (height ?? content.height)
|
||
}
|
||
if (['left', 'right'].includes(found) && options.align.startsWith('both')) {
|
||
let minHeight = options.align.split(':')[1] ?? 50
|
||
if (anchor.height >= minHeight) {
|
||
top = anchor.top
|
||
height = anchor.height
|
||
}
|
||
}
|
||
}
|
||
function screenAdjust() {
|
||
let adjustArrow
|
||
// adjust tip if needed after alignment
|
||
if ((['left', 'right'].includes(options.align) && anchor.width < (width ?? content.width))
|
||
|| (['top', 'bottom'].includes(options.align) && anchor.height < (height ?? content.height))
|
||
) {
|
||
adjustArrow = true
|
||
}
|
||
// if off screen then adjust
|
||
let minLeft = (found == 'right' ? arrowSize : options.screenMargin)
|
||
let minTop = (found == 'bottom' ? arrowSize : options.screenMargin)
|
||
let maxLeft = max.width - (width ?? content.width) - (found == 'left' ? arrowSize : options.screenMargin)
|
||
let maxTop = max.height - (height ?? content.height) - (found == 'top' ? arrowSize : options.screenMargin) + 3
|
||
// adjust X
|
||
if (['top', 'bottom'].includes(found) || options.autoResize) {
|
||
if (left < minLeft) {
|
||
adjustArrow = true
|
||
adjust.left -= left
|
||
left = minLeft
|
||
}
|
||
if (left > maxLeft) {
|
||
adjustArrow = true
|
||
adjust.left -= left - maxLeft
|
||
left += maxLeft - left
|
||
}
|
||
}
|
||
// adjust Y
|
||
if (['left', 'right'].includes(found) || options.autoResize) {
|
||
if (top < minTop) {
|
||
adjustArrow = true
|
||
adjust.top -= top
|
||
top = minTop
|
||
}
|
||
if (top > maxTop) {
|
||
adjustArrow = true
|
||
adjust.top -= top - maxTop
|
||
top += maxTop - top
|
||
}
|
||
}
|
||
// moves carret to adjust it with element width
|
||
if (adjustArrow) {
|
||
let aType = isVertical ? 'left' : 'top'
|
||
let sType = isVertical ? 'width' : 'height'
|
||
arrow.offset = -adjust[aType]
|
||
let maxOffset = content[sType] / 2 - arrowSize
|
||
if (Math.abs(arrow.offset) > maxOffset + arrowSize) {
|
||
arrow.class = '' // no arrow
|
||
}
|
||
if (Math.abs(arrow.offset) > maxOffset) {
|
||
arrow.offset = arrow.offset < 0 ? -maxOffset : maxOffset
|
||
}
|
||
arrow.style = w2utils.stripSpaces(`#${overlay.id} .w2ui-overlay-body:after,
|
||
#${overlay.id} .w2ui-overlay-body:before {
|
||
--tip-size: ${arrowSize}px;
|
||
margin-${aType}: ${arrow.offset}px;
|
||
}`)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
class ColorTooltip extends Tooltip {
|
||
constructor() {
|
||
super()
|
||
this.palette = [
|
||
['000000', '333333', '555555', '777777', '888888', '999999', 'AAAAAA', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'F7F7F7', 'FFFFFF'],
|
||
['FF011B', 'FF9838', 'FFC300', 'FFFD59', '86FF14', '14FF7A', '2EFFFC', '2693FF', '006CE7', '9B24F4', 'FF21F5', 'FF0099'],
|
||
['FFEAEA', 'FCEFE1', 'FCF4DC', 'FFFECF', 'EBFFD9', 'D9FFE9', 'E0FFFF', 'E8F4FF', 'ECF4FC', 'EAE6F4', 'FFF5FE', 'FCF0F7'],
|
||
['F4CCCC', 'FCE5CD', 'FFF1C2', 'FFFDA1', 'D5FCB1', 'B5F7D0', 'BFFFFF', 'D6ECFF', 'CFE2F3', 'D9D1E9', 'FFE3FD', 'FFD9F0'],
|
||
['EA9899', 'F9CB9C', 'FFE48C', 'F7F56F', 'B9F77E', '84F0B1', '83F7F7', 'B5DAFF', '9FC5E8', 'B4A7D6', 'FAB9F6', 'FFADDE'],
|
||
['E06666', 'F6B26B', 'DEB737', 'E0DE51', '8FDB48', '52D189', '4EDEDB', '76ACE3', '6FA8DC', '8E7CC3', 'E07EDA', 'F26DBD'],
|
||
['CC0814', 'E69138', 'AB8816', 'B5B20E', '6BAB30', '27A85F', '1BA8A6', '3C81C7', '3D85C6', '674EA7', 'A14F9D', 'BF4990'],
|
||
['99050C', 'B45F17', '80650E', '737103', '395E14', '10783D', '13615E', '094785', '0A5394', '351C75', '780172', '782C5A']
|
||
]
|
||
this.defaults = w2utils.extend({}, this.defaults, {
|
||
advanced : false,
|
||
transparent : true,
|
||
position : 'top|bottom',
|
||
class : 'w2ui-white',
|
||
color : '',
|
||
liveUpdate : true,
|
||
arrowSize : 12,
|
||
autoResize : false,
|
||
anchorClass : 'w2ui-focus',
|
||
autoShowOn : 'focus',
|
||
hideOn : ['doc-click', 'focus-change'],
|
||
onSelect : null,
|
||
onLiveUpdate: null
|
||
})
|
||
}
|
||
attach(anchor, text) {
|
||
let options
|
||
if (arguments.length == 1 && anchor.anchor) {
|
||
options = anchor
|
||
anchor = options.anchor
|
||
} else if (arguments.length === 2 && text != null && typeof text === 'object') {
|
||
options = text
|
||
options.anchor = anchor
|
||
}
|
||
let prevHideOn = options.hideOn
|
||
options = w2utils.extend({}, this.defaults, options || {})
|
||
if (prevHideOn) {
|
||
options.hideOn = prevHideOn
|
||
}
|
||
options.style += '; padding: 0;'
|
||
// add remove transparent color
|
||
if (options.transparent && this.palette[0][1] == '333333') {
|
||
this.palette[0].splice(1, 1)
|
||
this.palette[0].push('')
|
||
}
|
||
if (!options.transparent && this.palette[0][1] != '333333') {
|
||
this.palette[0].splice(1, 0, '333333')
|
||
this.palette[0].pop()
|
||
}
|
||
if (options.color) options.color = String(options.color).toUpperCase()
|
||
if (typeof options.color === 'string' && options.color.substr(0,1) === '#') options.color = options.color.substr(1)
|
||
// needed for keyboard navigation
|
||
this.index = [-1, -1]
|
||
let ret = super.attach(options)
|
||
let overlay = ret.overlay
|
||
overlay.options.html = this.getColorHTML(overlay.name, options)
|
||
overlay.on('show.attach', event => {
|
||
let overlay = event.detail.overlay
|
||
let anchor = overlay.anchor
|
||
let options = overlay.options
|
||
if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.color && anchor.value) {
|
||
overlay.tmp.initColor = anchor.value
|
||
}
|
||
delete overlay.newColor
|
||
})
|
||
overlay.on('show:after.attach', event => {
|
||
if (ret.overlay?.box) {
|
||
let actions = query(ret.overlay.box).find('.w2ui-eaction')
|
||
w2utils.bindEvents(actions, this)
|
||
this.initControls(ret.overlay)
|
||
}
|
||
})
|
||
overlay.on('update:after.attach', event => {
|
||
if (ret.overlay?.box) {
|
||
let actions = query(ret.overlay.box).find('.w2ui-eaction')
|
||
w2utils.bindEvents(actions, this)
|
||
this.initControls(ret.overlay)
|
||
}
|
||
})
|
||
overlay.on('hide.attach', event => {
|
||
let overlay = event.detail.overlay
|
||
let anchor = overlay.anchor
|
||
let color = overlay.newColor ?? overlay.options.color ?? ''
|
||
if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != color) {
|
||
anchor.value = color
|
||
}
|
||
let edata = this.trigger('select', { color, target: overlay.name, overlay })
|
||
if (edata.isCancelled === true) return
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
ret.liveUpdate = (callback) => {
|
||
overlay.on('liveUpdate.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
ret.select = (callback) => {
|
||
overlay.on('select.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
return ret
|
||
}
|
||
// regular panel handler, adds selection class
|
||
select(color, name) {
|
||
let target
|
||
this.index = [-1, -1]
|
||
if (typeof name != 'string') {
|
||
target = name.target
|
||
this.index = query(target).attr('index').split(':')
|
||
name = query(target).closest('.w2ui-overlay').attr('name')
|
||
}
|
||
let overlay = this.get(name)
|
||
// event before
|
||
let edata = this.trigger('liveUpdate', { color, target: name, overlay, param: arguments[1] })
|
||
if (edata.isCancelled === true) return
|
||
// if anchor is input - live update
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName) && overlay.options.liveUpdate) {
|
||
query(overlay.anchor).val(color)
|
||
}
|
||
overlay.newColor = color
|
||
query(overlay.box).find('.w2ui-selected').removeClass('w2ui-selected')
|
||
if (target) {
|
||
query(target).addClass('w2ui-selected')
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
// used for keyboard navigation, if any
|
||
nextColor(direction) { // TODO: check it
|
||
let pal = this.palette
|
||
switch (direction) {
|
||
case 'up':
|
||
this.index[0]--
|
||
break
|
||
case 'down':
|
||
this.index[0]++
|
||
break
|
||
case 'right':
|
||
this.index[1]++
|
||
break
|
||
case 'left':
|
||
this.index[1]--
|
||
break
|
||
}
|
||
if (this.index[0] < 0) this.index[0] = 0
|
||
if (this.index[0] > pal.length - 2) this.index[0] = pal.length - 2
|
||
if (this.index[1] < 0) this.index[1] = 0
|
||
if (this.index[1] > pal[0].length - 1) this.index[1] = pal[0].length - 1
|
||
return pal[this.index[0]][this.index[1]]
|
||
}
|
||
tabClick(index, name) {
|
||
if (typeof name != 'string') {
|
||
name = query(name.target).closest('.w2ui-overlay').attr('name')
|
||
}
|
||
let overlay = this.get(name)
|
||
let tab = query(overlay.box).find(`.w2ui-color-tab:nth-child(${index})`)
|
||
query(overlay.box).find('.w2ui-color-tab').removeClass('w2ui-selected')
|
||
query(tab).addClass('w2ui-selected')
|
||
query(overlay.box)
|
||
.find('.w2ui-tab-content')
|
||
.hide()
|
||
.closest('.w2ui-colors')
|
||
.find('.tab-'+ index)
|
||
.show()
|
||
}
|
||
// generate HTML with color pallent and controls
|
||
getColorHTML(name, options) {
|
||
let html = `
|
||
<div class="w2ui-colors">
|
||
<div class="w2ui-tab-content tab-1">`
|
||
for (let i = 0; i < this.palette.length; i++) {
|
||
html += '<div class="w2ui-color-row">'
|
||
for (let j = 0; j < this.palette[i].length; j++) {
|
||
let color = this.palette[i][j]
|
||
let border = ''
|
||
if (color === 'FFFFFF') border = '; border: 1px solid #efefef'
|
||
html += `
|
||
<div class="w2ui-color w2ui-eaction ${color === '' ? 'w2ui-no-color' : ''} ${options.color == color ? 'w2ui-selected' : ''}"
|
||
style="background-color: #${color + border};" name="${color}" index="${i}:${j}"
|
||
data-mousedown="select|'${color}'|event" data-mouseup="hide|${name}">
|
||
</div>`
|
||
}
|
||
html += '</div>'
|
||
if (i < 2) html += '<div style="height: 8px"></div>'
|
||
}
|
||
html += '</div>'
|
||
// advanced tab
|
||
html += `
|
||
<div class="w2ui-tab-content tab-2" style="display: none">
|
||
<div class="color-info">
|
||
<div class="color-preview-bg"><div class="color-preview"></div><div class="color-original"></div></div>
|
||
<div class="color-part">
|
||
<span>H</span> <input class="w2ui-input" name="h" maxlength="3" max="360" tabindex="101">
|
||
<span>R</span> <input class="w2ui-input" name="r" maxlength="3" max="255" tabindex="104">
|
||
</div>
|
||
<div class="color-part">
|
||
<span>S</span> <input class="w2ui-input" name="s" maxlength="3" max="100" tabindex="102">
|
||
<span>G</span> <input class="w2ui-input" name="g" maxlength="3" max="255" tabindex="105">
|
||
</div>
|
||
<div class="color-part">
|
||
<span>V</span> <input class="w2ui-input" name="v" maxlength="3" max="100" tabindex="103">
|
||
<span>B</span> <input class="w2ui-input" name="b" maxlength="3" max="255" tabindex="106">
|
||
</div>
|
||
<div class="color-part opacity">
|
||
<span>${w2utils.lang('Opacity')}</span>
|
||
<input class="w2ui-input" name="a" maxlength="5" max="1" tabindex="107">
|
||
</div>
|
||
</div>
|
||
<div class="palette" name="palette">
|
||
<div class="palette-bg"></div>
|
||
<div class="value1 move-x move-y"></div>
|
||
</div>
|
||
<div class="rainbow" name="rainbow">
|
||
<div class="value2 move-x"></div>
|
||
</div>
|
||
<div class="alpha" name="alpha">
|
||
<div class="alpha-bg"></div>
|
||
<div class="value2 move-x"></div>
|
||
</div>
|
||
</div>`
|
||
// color tabs on the bottom
|
||
html += `
|
||
<div class="w2ui-color-tabs">
|
||
<div class="w2ui-color-tab selected w2ui-eaction" data-click="tabClick|1|event|this"><span class="w2ui-icon w2ui-icon-colors"></span></div>
|
||
<div class="w2ui-color-tab w2ui-eaction" data-click="tabClick|2|event|this"><span class="w2ui-icon w2ui-icon-settings"></span></div>
|
||
<div style="padding: 5px; width: 100%; text-align: right;">
|
||
${(typeof options.html == 'string' ? options.html : '')}
|
||
</div>
|
||
</div>`
|
||
return html
|
||
}
|
||
// bind advanced tab controls
|
||
initControls(overlay) {
|
||
let initial // used for mouse events
|
||
let self = this
|
||
let options = overlay.options
|
||
let rgb = w2utils.parseColor(options.color || overlay.tmp.initColor)
|
||
if (rgb == null) {
|
||
rgb = { r: 140, g: 150, b: 160, a: 1 }
|
||
}
|
||
let hsv = w2utils.rgb2hsv(rgb)
|
||
if (options.advanced === true) {
|
||
this.tabClick(2, overlay.name)
|
||
}
|
||
setColor(hsv, true, true)
|
||
// even for rgb, hsv inputs
|
||
query(overlay.box).find('input')
|
||
.off('.w2color')
|
||
.on('change.w2color', (event) => {
|
||
let el = query(event.target)
|
||
let val = parseFloat(el.val())
|
||
let max = parseFloat(el.attr('max'))
|
||
if (isNaN(val)) {
|
||
val = 0
|
||
el.val(0)
|
||
}
|
||
if (max > 1) val = parseInt(val) // trancate fractions
|
||
if (max > 0 && val > max) {
|
||
el.val(max)
|
||
val = max
|
||
}
|
||
if (val < 0) {
|
||
el.val(0)
|
||
val = 0
|
||
}
|
||
let name = el.attr('name')
|
||
let color = {}
|
||
if (['r', 'g', 'b', 'a'].indexOf(name) !== -1) {
|
||
rgb[name] = val
|
||
hsv = w2utils.rgb2hsv(rgb)
|
||
} else if (['h', 's', 'v'].indexOf(name) !== -1) {
|
||
color[name] = val
|
||
}
|
||
setColor(color, true)
|
||
})
|
||
// click on original color resets it
|
||
query(overlay.box).find('.color-original')
|
||
.off('.w2color')
|
||
.on('click.w2color', (event) => {
|
||
let tmp = w2utils.parseColor(query(event.target).css('background-color'))
|
||
if (tmp != null) {
|
||
rgb = tmp
|
||
hsv = w2utils.rgb2hsv(rgb)
|
||
setColor(hsv, true)
|
||
}
|
||
})
|
||
// color sliders events
|
||
let mDown = `${!w2utils.isIOS ? 'mousedown' : 'touchstart'}.w2color`
|
||
let mUp = `${!w2utils.isIOS ? 'mouseup' : 'touchend'}.w2color`
|
||
let mMove = `${!w2utils.isIOS ? 'mousemove' : 'touchmove'}.w2color`
|
||
query(overlay.box).find('.palette, .rainbow, .alpha')
|
||
.off('.w2color')
|
||
.on(`${mDown}.w2color`, mouseDown)
|
||
return
|
||
function setColor(color, fullUpdate, initial) {
|
||
if (color.h != null) hsv.h = color.h
|
||
if (color.s != null) hsv.s = color.s
|
||
if (color.v != null) hsv.v = color.v
|
||
if (color.a != null) { rgb.a = color.a; hsv.a = color.a }
|
||
rgb = w2utils.hsv2rgb(hsv)
|
||
let newColor = 'rgba('+ rgb.r +','+ rgb.g +','+ rgb.b +','+ rgb.a +')'
|
||
let cl = [
|
||
Number(rgb.r).toString(16).toUpperCase(),
|
||
Number(rgb.g).toString(16).toUpperCase(),
|
||
Number(rgb.b).toString(16).toUpperCase(),
|
||
(Math.round(Number(rgb.a)*255)).toString(16).toUpperCase()
|
||
]
|
||
cl.forEach((item, ind) => { if (item.length === 1) cl[ind] = '0' + item })
|
||
newColor = cl[0] + cl[1] + cl[2] + cl[3]
|
||
if (rgb.a === 1) {
|
||
newColor = cl[0] + cl[1] + cl[2]
|
||
}
|
||
query(overlay.box).find('.color-preview').css('background-color', '#' + newColor)
|
||
query(overlay.box).find('input').each(el => {
|
||
if (el.name) {
|
||
if (rgb[el.name] != null) el.value = rgb[el.name]
|
||
if (hsv[el.name] != null) el.value = hsv[el.name]
|
||
if (el.name === 'a') el.value = rgb.a
|
||
}
|
||
})
|
||
// if it is in pallette
|
||
if (initial) {
|
||
let color = overlay.tmp?.initColor || newColor
|
||
query(overlay.box).find('.color-original')
|
||
.css('background-color', '#'+color)
|
||
query(overlay.box).find('.w2ui-colors .w2ui-selected')
|
||
.removeClass('w2ui-selected')
|
||
query(overlay.box).find(`.w2ui-colors [name="${color}"]`)
|
||
.addClass('w2ui-selected')
|
||
// if has transparent color, open advanced tab
|
||
if (newColor.length == 8) {
|
||
self.tabClick(2, overlay.name)
|
||
}
|
||
} else {
|
||
self.select(newColor, overlay.name)
|
||
}
|
||
if (fullUpdate) {
|
||
updateSliders()
|
||
refreshPalette()
|
||
}
|
||
}
|
||
function updateSliders() {
|
||
let el1 = query(overlay.box).find('.palette .value1')
|
||
let el2 = query(overlay.box).find('.rainbow .value2')
|
||
let el3 = query(overlay.box).find('.alpha .value2')
|
||
let offset1 = parseInt(el1[0].clientWidth) / 2
|
||
let offset2 = parseInt(el2[0].clientWidth) / 2
|
||
el1.css({
|
||
'left': (hsv.s * 150 / 100 - offset1) + 'px',
|
||
'top': ((100 - hsv.v) * 125 / 100 - offset1) + 'px'
|
||
})
|
||
el2.css('left', (hsv.h/(360/150) - offset2) + 'px')
|
||
el3.css('left', (rgb.a*150 - offset2) + 'px')
|
||
}
|
||
function refreshPalette() {
|
||
let cl = w2utils.hsv2rgb(hsv.h, 100, 100)
|
||
let rgb = `${cl.r},${cl.g},${cl.b}`
|
||
query(overlay.box).find('.palette')
|
||
.css('background-image', `linear-gradient(90deg, rgba(${rgb},0) 0%, rgba(${rgb},1) 100%)`)
|
||
}
|
||
function mouseDown(event) {
|
||
let el = query(this).find('.value1, .value2')
|
||
let offset = parseInt(el.prop('clientWidth')) / 2
|
||
if (el.hasClass('move-x')) el.css({ left: (event.offsetX - offset) + 'px' })
|
||
if (el.hasClass('move-y')) el.css({ top: (event.offsetY - offset) + 'px' })
|
||
initial = {
|
||
el : el,
|
||
x : event.pageX,
|
||
y : event.pageY,
|
||
width : el.prop('parentNode').clientWidth,
|
||
height : el.prop('parentNode').clientHeight,
|
||
left : parseInt(el.css('left')),
|
||
top : parseInt(el.css('top'))
|
||
}
|
||
mouseMove(event)
|
||
query('body')
|
||
.off('.w2color')
|
||
.on(mMove, mouseMove)
|
||
.on(mUp, mouseUp)
|
||
}
|
||
function mouseUp(event) {
|
||
query('body').off('.w2color')
|
||
}
|
||
function mouseMove(event) {
|
||
let el = initial.el
|
||
let divX = event.pageX - initial.x
|
||
let divY = event.pageY - initial.y
|
||
let newX = initial.left + divX
|
||
let newY = initial.top + divY
|
||
let offset = parseInt(el.prop('clientWidth')) / 2
|
||
if (newX < -offset) newX = -offset
|
||
if (newY < -offset) newY = -offset
|
||
if (newX > initial.width - offset) newX = initial.width - offset
|
||
if (newY > initial.height - offset) newY = initial.height - offset
|
||
if (el.hasClass('move-x')) el.css({ left : newX + 'px' })
|
||
if (el.hasClass('move-y')) el.css({ top : newY + 'px' })
|
||
// move
|
||
let name = query(el.get(0).parentNode).attr('name')
|
||
let x = parseInt(el.css('left')) + offset
|
||
let y = parseInt(el.css('top')) + offset
|
||
if (name === 'palette') {
|
||
setColor({
|
||
s: Math.round(x / initial.width * 100),
|
||
v: Math.round(100 - (y / initial.height * 100))
|
||
})
|
||
}
|
||
if (name === 'rainbow') {
|
||
let h = Math.round(360 / 150 * x)
|
||
setColor({ h: h })
|
||
refreshPalette()
|
||
}
|
||
if (name === 'alpha') {
|
||
setColor({ a: parseFloat(Number(x / 150).toFixed(2)) })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
class MenuTooltip extends Tooltip {
|
||
constructor() {
|
||
super()
|
||
// ITEM STRUCTURE
|
||
// item : {
|
||
// id : null,
|
||
// text : '',
|
||
// style : '',
|
||
// icon : '',
|
||
// count : '',
|
||
// tooltip : '',
|
||
// hotkey : '',
|
||
// remove : false,
|
||
// items : []
|
||
// indent : 0,
|
||
// type : null, // check/radio
|
||
// group : false, // groupping for checks
|
||
// expanded : false,
|
||
// hidden : false,
|
||
// checked : null,
|
||
// disabled : false
|
||
// ...
|
||
// }
|
||
this.defaults = w2utils.extend({}, this.defaults, {
|
||
type : 'normal', // can be normal, radio, check
|
||
items : [],
|
||
index : null, // current selected
|
||
render : null,
|
||
spinner : false,
|
||
msgNoItems : w2utils.lang('No items found'),
|
||
topHTML : '',
|
||
menuStyle : '',
|
||
filter : false,
|
||
markSearch : false,
|
||
match : 'contains', // is, begins, ends, contains
|
||
search : false, // top search TODO: Check
|
||
altRows : false,
|
||
arrowSize : 10,
|
||
align : 'left',
|
||
position : 'bottom|top',
|
||
class : 'w2ui-white',
|
||
anchorClass : 'w2ui-focus',
|
||
autoShowOn : 'focus',
|
||
hideOn : ['doc-click', 'focus-change', 'select'], // also can 'item-remove'
|
||
onSelect : null,
|
||
onSubMenu : null,
|
||
onRemove : null
|
||
})
|
||
}
|
||
attach(anchor, text) {
|
||
let options
|
||
if (arguments.length == 1 && anchor.anchor) {
|
||
options = anchor
|
||
anchor = options.anchor
|
||
} else if (arguments.length === 2 && text != null && typeof text === 'object') {
|
||
options = text
|
||
options.anchor = anchor
|
||
}
|
||
let prevHideOn = options.hideOn
|
||
options = w2utils.extend({}, this.defaults, options || {})
|
||
if (prevHideOn) {
|
||
options.hideOn = prevHideOn
|
||
}
|
||
options.style += '; padding: 0;'
|
||
if (options.items == null) {
|
||
options.items = []
|
||
}
|
||
options.html = this.getMenuHTML(options)
|
||
let ret = super.attach(options)
|
||
let overlay = ret.overlay
|
||
overlay.on('show:after.attach, update:after.attach', event => {
|
||
if (ret.overlay?.box) {
|
||
let search = ''
|
||
// reset selected and active chain
|
||
overlay.selected = null
|
||
overlay.options.items = w2utils.normMenu(overlay.options.items)
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
search = overlay.anchor.value
|
||
overlay.selected = overlay.anchor.dataset.selectedIndex
|
||
}
|
||
let actions = query(ret.overlay.box).find('.w2ui-eaction')
|
||
w2utils.bindEvents(actions, this)
|
||
let count = this.applyFilter(overlay.name, null, search)
|
||
overlay.tmp.searchCount = count
|
||
overlay.tmp.search = search
|
||
this.refreshSearch(overlay.name)
|
||
this.initControls(ret.overlay)
|
||
this.refreshIndex(overlay.name)
|
||
}
|
||
})
|
||
overlay.on('hide:after.attach', event => {
|
||
w2tooltip.hide(overlay.name + '-tooltip')
|
||
})
|
||
ret.select = (callback) => {
|
||
overlay.on('select.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
ret.remove = (callback) => {
|
||
overlay.on('remove.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
ret.subMenu = (callback) => {
|
||
overlay.on('subMenu.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
return ret
|
||
}
|
||
update(name, items) {
|
||
let overlay = Tooltip.active[name]
|
||
if (overlay) {
|
||
let options = overlay.options
|
||
if (options.items != items) {
|
||
options.items = items
|
||
}
|
||
let menuHTML = this.getMenuHTML(options)
|
||
if (options.html != menuHTML) {
|
||
options.html = menuHTML
|
||
overlay.needsUpdate = true
|
||
this.show(name)
|
||
}
|
||
} else {
|
||
console.log(`Tooltip "${name}" is not displayed. Cannot update it.`)
|
||
}
|
||
}
|
||
initControls(overlay) {
|
||
query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)')
|
||
.off('.w2menu')
|
||
.on('mouseDown.w2menu', { delegate: '.w2ui-menu-item' }, event => {
|
||
let dt = event.delegate.dataset
|
||
this.menuDown(overlay, event, dt.index, dt.parents)
|
||
})
|
||
.on((w2utils.isIOS ? 'touchStart' : 'click') + '.w2menu', { delegate: '.w2ui-menu-item' }, event => {
|
||
let dt = event.delegate.dataset
|
||
this.menuClick(overlay, event, parseInt(dt.index), dt.parents)
|
||
})
|
||
.find('.w2ui-menu-item')
|
||
.off('.w2menu')
|
||
.on('mouseEnter.w2menu', event => {
|
||
let dt = event.target.dataset
|
||
let tooltip = overlay.options.items[dt.index]?.tooltip
|
||
if (tooltip) {
|
||
w2tooltip.show({
|
||
name: overlay.name + '-tooltip',
|
||
anchor: event.target,
|
||
html: tooltip,
|
||
position: 'right|left',
|
||
hideOn: ['doc-click']
|
||
})
|
||
}
|
||
})
|
||
.on('mouseLeave.w2menu', event => {
|
||
w2tooltip.hide(overlay.name + '-tooltip')
|
||
})
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
query(overlay.anchor)
|
||
.off('.w2menu')
|
||
.on('input.w2menu', event => {
|
||
// if user types, clear selection
|
||
// let dt = event.target.dataset
|
||
// delete dt.selected
|
||
// delete dt.selectedIndex
|
||
})
|
||
.on('keyup.w2menu', event => {
|
||
event._searchType = 'filter'
|
||
this.keyUp(overlay, event)
|
||
})
|
||
}
|
||
if (overlay.options.search) {
|
||
query(overlay.box).find('#menu-search')
|
||
.off('.w2menu')
|
||
.on('keyup.w2menu', event => {
|
||
event._searchType = 'search'
|
||
this.keyUp(overlay, event)
|
||
})
|
||
}
|
||
}
|
||
getCurrent(name, id) {
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
let options = overlay.options
|
||
let selected = (id ? id : overlay.selected ?? '').split('-')
|
||
let last = selected.length-1
|
||
let index = selected[last]
|
||
let parents = selected.slice(0, selected.length-1).join('-')
|
||
index = w2utils.isInt(index) ? parseInt(index) : 0
|
||
// items
|
||
let items = options.items
|
||
selected.forEach((id, ind) => {
|
||
// do not go to the last one
|
||
if (ind < selected.length - 1) {
|
||
items = items[id].items
|
||
}
|
||
})
|
||
return { last, index, items, item: items[index], parents }
|
||
}
|
||
getMenuHTML(options, items, subMenu, parentIndex) {
|
||
if (options.spinner) {
|
||
return `
|
||
<div class="w2ui-menu">
|
||
<div class="w2ui-no-items">
|
||
<div class="w2ui-spinner"></div>
|
||
${w2utils.lang('Loading...')}
|
||
</div>
|
||
</div>`
|
||
}
|
||
if (!parentIndex) parentIndex = []
|
||
if (items == null) {
|
||
items = options.items
|
||
}
|
||
if (!Array.isArray(items)) items = []
|
||
let count = 0
|
||
let icon = null
|
||
let topHTML = ''
|
||
if (!subMenu && options.search) {
|
||
topHTML += `
|
||
<div class="w2ui-menu-search">
|
||
<span class="w2ui-icon w2ui-icon-search"></span>
|
||
<input id="menu-search" class="w2ui-input" type="text"/>
|
||
</div>`
|
||
items.forEach(item => item.hidden = false)
|
||
}
|
||
if (!subMenu && options.topHTML) {
|
||
topHTML += `<div class="w2ui-menu-top">${options.topHTML}</div>`
|
||
}
|
||
let menu_html = `
|
||
${topHTML}
|
||
<div class="w2ui-menu ${(subMenu ? 'w2ui-sub-menu' : '')}" ${!subMenu ? `style="${options.menuStyle}"` : ''}
|
||
data-parent="${parentIndex}">
|
||
`
|
||
items.forEach((mitem, f) => {
|
||
icon = mitem.icon
|
||
let index = (parentIndex.length > 0 ? parentIndex.join('-') + '-' : '') + f
|
||
if (icon == null) icon = null // icon might be undefined
|
||
if (['radio', 'check'].indexOf(options.type) != -1 && !Array.isArray(mitem.items) && mitem.group !== false) {
|
||
if (mitem.checked === true) icon = 'w2ui-icon-check'; else icon = 'w2ui-icon-empty'
|
||
}
|
||
if (mitem.hidden !== true) {
|
||
let txt = mitem.text
|
||
let icon_dsp = ''
|
||
let subMenu_dsp = ''
|
||
if (typeof options.render === 'function') txt = options.render(mitem, options)
|
||
if (typeof txt == 'function') txt = txt(mitem, options)
|
||
if (icon) {
|
||
if (String(icon).slice(0, 1) !== '<') {
|
||
icon = `<span class="w2ui-icon ${icon}"></span>`
|
||
}
|
||
icon_dsp = `<div class="menu-icon">${icon}</span></div>`
|
||
}
|
||
// render only if non-empty
|
||
if (mitem.type !== 'break' && txt != null && txt !== '' && String(txt).substr(0, 2) != '--') {
|
||
let classes = ['w2ui-menu-item']
|
||
if (options.altRows == true) {
|
||
classes.push(count % 2 === 0 ? 'w2ui-even' : 'w2ui-odd')
|
||
}
|
||
let colspan = 1
|
||
if (icon_dsp === '') colspan++
|
||
if (mitem.count == null && mitem.hotkey == null && mitem.remove !== true && mitem.items == null) colspan++
|
||
if (mitem.tooltip == null && mitem.hint != null) mitem.tooltip = mitem.hint // for backward compatibility
|
||
let count_dsp = ''
|
||
if (mitem.remove === true) {
|
||
count_dsp = '<span class="remove">x</span>'
|
||
} else if (mitem.items != null) {
|
||
let _items = []
|
||
if (typeof mitem.items == 'function') {
|
||
_items = mitem.items(mitem)
|
||
} else if (Array.isArray(mitem.items)) {
|
||
_items = mitem.items
|
||
}
|
||
count_dsp = '<span></span>'
|
||
subMenu_dsp = `
|
||
<div class="w2ui-sub-menu-box" style="${mitem.expanded ? '' : 'display: none'}">
|
||
${this.getMenuHTML(options, _items, true, parentIndex.concat(f))}
|
||
</div>`
|
||
} else {
|
||
if (mitem.count != null) count_dsp += '<span>' + mitem.count + '</span>'
|
||
if (mitem.hotkey != null) count_dsp += '<span class="hotkey">' + mitem.hotkey + '</span>'
|
||
}
|
||
if (mitem.disabled === true) classes.push('w2ui-disabled')
|
||
if (mitem._noSearchInside === true) classes.push('w2ui-no-search-inside')
|
||
if (subMenu_dsp !== '') {
|
||
classes.push('has-sub-menu')
|
||
if (mitem.expanded) {
|
||
classes.push('expanded')
|
||
} else {
|
||
classes.push('collapsed')
|
||
}
|
||
}
|
||
menu_html += `
|
||
<div index="${index}" class="${classes.join(' ')}" style="${mitem.style ? mitem.style : ''}"
|
||
data-index="${f}" data-parents="${parentIndex.join('-')}">
|
||
<div style="width: ${(subMenu ? 20 : 0) + parseInt(mitem.indent ?? 0)}px"></div>
|
||
${icon_dsp}
|
||
<div class="menu-text" colspan="${colspan}">${w2utils.lang(txt)}</div>
|
||
<div class="menu-extra">${count_dsp}</div>
|
||
</div>
|
||
${subMenu_dsp}`
|
||
count++
|
||
} else {
|
||
// horizontal line
|
||
let divText = (txt ?? '').replace(/^-+/g, '')
|
||
menu_html += `
|
||
<div index="${index}" class="w2ui-menu-divider ${divText != '' ? 'has-text' : ''}">
|
||
<div class="line"></div>
|
||
${divText ? `<div class="text">${divText}</div>` : ''}
|
||
</div>`
|
||
}
|
||
}
|
||
items[f] = mitem
|
||
})
|
||
if (count === 0 && options.msgNoItems) {
|
||
menu_html += `
|
||
<div class="w2ui-no-items">
|
||
${w2utils.lang(options.msgNoItems)}
|
||
</div>`
|
||
}
|
||
menu_html += '</div>'
|
||
return menu_html
|
||
}
|
||
// Refreshed only selected item highligh, used in keyboard navigation
|
||
refreshIndex(name) {
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
if (!overlay) return
|
||
if (!overlay.displayed) {
|
||
this.show(overlay.name)
|
||
}
|
||
let view = query(overlay.box).find('.w2ui-overlay-body').get(0)
|
||
let search = query(overlay.box).find('.w2ui-menu-search, .w2ui-menu-top').get(0)
|
||
query(overlay.box).find('.w2ui-menu-item.w2ui-selected')
|
||
.removeClass('w2ui-selected')
|
||
let el = query(overlay.box).find(`.w2ui-menu-item[index="${overlay.selected}"]`)
|
||
.addClass('w2ui-selected')
|
||
.get(0)
|
||
if (el) {
|
||
if (el.offsetTop + el.clientHeight > view.clientHeight + view.scrollTop) {
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' })
|
||
}
|
||
if (el.offsetTop < view.scrollTop + (search ? search.clientHeight : 0)) {
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'end' })
|
||
}
|
||
}
|
||
}
|
||
// show/hide searched items
|
||
refreshSearch(name) {
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
if (!overlay) return
|
||
if (!overlay.displayed) {
|
||
this.show(overlay.name)
|
||
}
|
||
query(overlay.box).find('.w2ui-no-items').hide()
|
||
query(overlay.box).find('.w2ui-menu-item, .w2ui-menu-divider').each(el => {
|
||
let cur = this.getCurrent(name, el.getAttribute('index'))
|
||
if (cur.item.hidden) {
|
||
query(el).hide()
|
||
} else {
|
||
let search = overlay.tmp?.search
|
||
if (search && overlay.options.markSearch) {
|
||
w2utils.marker(el, search, { onlyFirst: overlay.options.match == 'begins' })
|
||
}
|
||
query(el).show()
|
||
}
|
||
})
|
||
// hide empty menus
|
||
query(overlay.box).find('.w2ui-sub-menu').each(sub => {
|
||
let hasItems = query(sub).find('.w2ui-menu-item').get().some(el => {
|
||
return el.style.display != 'none' ? true : false
|
||
})
|
||
let parent = this.getCurrent(name, sub.dataset.parent)
|
||
// only if parent is expaneded
|
||
if (parent.item.expanded) {
|
||
if (!hasItems) {
|
||
query(sub).parent().hide()
|
||
} else {
|
||
query(sub).parent().show()
|
||
}
|
||
}
|
||
})
|
||
// show empty message
|
||
if (overlay.tmp.searchCount == 0 || overlay.options?.items?.length == 0) {
|
||
if (query(overlay.box).find('.w2ui-no-items').length == 0) {
|
||
query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)').append(`
|
||
<div class="w2ui-no-items">
|
||
${w2utils.lang(overlay.options.msgNoItems)}
|
||
</div>`)
|
||
}
|
||
query(overlay.box).find('.w2ui-no-items').show()
|
||
}
|
||
}
|
||
/**
|
||
* Loops through the items and markes item.hidden for those that need to be hidden.
|
||
* Return the number of visible items.
|
||
*/
|
||
applyFilter(name, items, search) {
|
||
let count = 0
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
let options = overlay.options
|
||
if (options.filter === false) {
|
||
return
|
||
}
|
||
if (items == null) items = overlay.options.items
|
||
if (search == null) {
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
search = overlay.anchor.value
|
||
} else {
|
||
search = ''
|
||
}
|
||
}
|
||
let selectedIds = []
|
||
if (options.selected) {
|
||
if (Array.isArray(options.selected)) {
|
||
selectedIds = options.selected.map(item => {
|
||
return item?.id ?? item
|
||
})
|
||
} else if (options.selected?.id) {
|
||
selectedIds = [options.selected.id]
|
||
}
|
||
}
|
||
items.forEach(item => {
|
||
let prefix = ''
|
||
let suffix = ''
|
||
if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
|
||
if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
|
||
try {
|
||
let re = new RegExp(prefix + search + suffix, 'i')
|
||
if (re.test(item.text) || item.text === '...') {
|
||
item.hidden = false
|
||
} else {
|
||
item.hidden = true
|
||
}
|
||
} catch (e) {}
|
||
// do not show selected items
|
||
if (options.hideSelected && selectedIds.includes(item.id)) {
|
||
item.hidden = true
|
||
}
|
||
// search nested items
|
||
if (Array.isArray(item.items) && item.items.length > 0) {
|
||
delete item._noSearchInside
|
||
let subCount = this.applyFilter(name, item.items, search)
|
||
if (subCount > 0) {
|
||
count += subCount
|
||
if (item.hidden) item._noSearchInside = true
|
||
// only expand items if search is not empty
|
||
if (search) item.expanded = true
|
||
item.hidden = false
|
||
}
|
||
}
|
||
if (item.hidden !== true) count++
|
||
})
|
||
overlay.tmp.activeChain = this.getActiveChain(name, items)
|
||
overlay.selected = null
|
||
return count
|
||
}
|
||
/**
|
||
* Builds an array of item ids that sequencial in navigation with up/down keys.
|
||
* Skips hidden and disabled items and goes into nested structures.
|
||
*/
|
||
getActiveChain(name, items, parents = [], res = [], noSave) {
|
||
let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
|
||
if (overlay.tmp.activeChain != null) {
|
||
return overlay.tmp.activeChain
|
||
}
|
||
if (items == null) items = overlay.options.items
|
||
items.forEach((item, ind) => {
|
||
if (!item.hidden && !item.disabled && !item?.text?.startsWith('--')) {
|
||
res.push(parents.concat([ind]).join('-'))
|
||
if (Array.isArray(item.items) && item.items.length > 0 && item.expanded) {
|
||
parents.push(ind)
|
||
this.getActiveChain(name, item.items, parents, res, true)
|
||
parents.pop()
|
||
}
|
||
}
|
||
})
|
||
if (noSave == null) {
|
||
overlay.tmp.activeChain = res
|
||
}
|
||
return res
|
||
}
|
||
menuDown(overlay, event, index, parentIndex) {
|
||
let options = overlay.options
|
||
let items = options.items
|
||
let icon = query(event.delegate).find('.w2ui-icon')
|
||
let menu = query(event.target).closest('.w2ui-menu:not(.w2ui-sub-menu)')
|
||
if (typeof parentIndex == 'string' && parentIndex !== '') {
|
||
let ids = parentIndex.split('-')
|
||
ids.forEach(id => {
|
||
items = items[id].items
|
||
})
|
||
}
|
||
let item = items[index]
|
||
if (item.disabled) {
|
||
return
|
||
}
|
||
let uncheck = (items, parent) => {
|
||
items.forEach((other, ind) => {
|
||
if (other.id == item.id) return
|
||
if (other.group === item.group && other.checked) {
|
||
menu
|
||
.find(`.w2ui-menu-item[index="${(parent ? parent + '-' : '') + ind}"] .w2ui-icon`)
|
||
.removeClass('w2ui-icon-check')
|
||
.addClass('w2ui-icon-empty')
|
||
items[ind].checked = false
|
||
}
|
||
if (Array.isArray(other.items)) {
|
||
uncheck(other.items, ind)
|
||
}
|
||
})
|
||
}
|
||
if ((options.type === 'check' || options.type === 'radio') && item.group !== false
|
||
&& !query(event.target).hasClass('remove')
|
||
&& !query(event.target).closest('.w2ui-menu-item').hasClass('has-sub-menu')) {
|
||
item.checked = options.type == 'radio' ? true : !item.checked
|
||
if (item.checked) {
|
||
if (options.type === 'radio') {
|
||
query(event.target).closest('.w2ui-menu').find('.w2ui-icon')
|
||
.removeClass('w2ui-icon-check')
|
||
.addClass('w2ui-icon-empty')
|
||
}
|
||
if (options.type === 'check' && item.group != null) {
|
||
uncheck(options.items)
|
||
}
|
||
icon.removeClass('w2ui-icon-empty').addClass('w2ui-icon-check')
|
||
} else if (options.type === 'check') {
|
||
icon.removeClass('w2ui-icon-check').addClass('w2ui-icon-empty')
|
||
}
|
||
}
|
||
// highlight record
|
||
if (!query(event.target).hasClass('remove')) {
|
||
menu.find('.w2ui-menu-item').removeClass('w2ui-selected')
|
||
query(event.delegate).addClass('w2ui-selected')
|
||
}
|
||
}
|
||
menuClick(overlay, event, index, parentIndex) {
|
||
let options = overlay.options
|
||
let items = options.items
|
||
let $item = query(event.delegate).closest('.w2ui-menu-item')
|
||
let keepOpen = options.hideOn.includes('select') ? false : true
|
||
if (event.shiftKey || event.metaKey || event.ctrlKey) {
|
||
keepOpen = true
|
||
}
|
||
if (typeof parentIndex == 'string' && parentIndex !== '') {
|
||
let ids = parentIndex.split('-')
|
||
ids.forEach(id => {
|
||
items = items[id].items
|
||
})
|
||
} else {
|
||
parentIndex = null
|
||
}
|
||
if (typeof items == 'function') {
|
||
items = items({ overlay, index, parentIndex, event })
|
||
}
|
||
let item = items[index]
|
||
if (item.disabled && !query(event.target).hasClass('remove')) {
|
||
return
|
||
}
|
||
let edata
|
||
if (query(event.target).hasClass('remove')) {
|
||
edata = this.trigger('remove', { originalEvent: event, target: overlay.name,
|
||
overlay, item, index, parentIndex, el: $item[0] })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
keepOpen = !options.hideOn.includes('item-remove')
|
||
$item.remove()
|
||
} else if ($item.hasClass('has-sub-menu')) {
|
||
edata = this.trigger('subMenu', { originalEvent: event, target: overlay.name,
|
||
overlay, item, index, parentIndex, el: $item[0] })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
keepOpen = true
|
||
if ($item.hasClass('expanded')) {
|
||
item.expanded = false
|
||
$item.removeClass('expanded').addClass('collapsed')
|
||
query($item.get(0).nextElementSibling).hide()
|
||
overlay.selected = parseInt($item.attr('index'))
|
||
} else {
|
||
item.expanded = true
|
||
$item.addClass('expanded').removeClass('collapsed')
|
||
query($item.get(0).nextElementSibling).show()
|
||
overlay.selected = parseInt($item.attr('index'))
|
||
}
|
||
} else {
|
||
// find items that are selected
|
||
let selected = this.findChecked(options.items)
|
||
overlay.selected = parseInt($item.attr('index'))
|
||
edata = this.trigger('select', { originalEvent: event, target: overlay.name,
|
||
overlay, item, index, parentIndex, selected, keepOpen, el: $item[0] })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
if (item.keepOpen != null) {
|
||
keepOpen = item.keepOpen
|
||
}
|
||
if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
overlay.anchor.dataset.selected = item.id
|
||
overlay.anchor.dataset.selectedIndex = overlay.selected
|
||
}
|
||
}
|
||
if (!keepOpen) {
|
||
this.hide(overlay.name)
|
||
}
|
||
// if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
|
||
// overlay.anchor.focus()
|
||
// }
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
findChecked(items) {
|
||
let found = []
|
||
items.forEach(item => {
|
||
if (item.checked) found.push(item)
|
||
if (Array.isArray(item.items)) {
|
||
found = found.concat(this.findChecked(item.items))
|
||
}
|
||
})
|
||
return found
|
||
}
|
||
keyUp(overlay, event) {
|
||
let options = overlay.options
|
||
let search = event.target.value
|
||
let key = event.keyCode
|
||
let filter = true
|
||
let refreshIndex = false
|
||
switch (key) {
|
||
case 8: { // delete
|
||
// if search empty and delete is clicked, do not filter nor show overlay
|
||
if (search === '' && !overlay.displayed) filter = false
|
||
break
|
||
}
|
||
case 13: { // enter
|
||
if (!overlay.displayed || !overlay.selected) return
|
||
let { index, parents } = this.getCurrent(overlay.name)
|
||
event.delegate = query(overlay.box).find('.w2ui-selected').get(0)
|
||
// reset active chain for folders
|
||
this.menuClick(overlay, event, parseInt(index), parents)
|
||
filter = false
|
||
break
|
||
}
|
||
case 27: { // escape
|
||
filter = false
|
||
if (overlay.displayed) {
|
||
this.hide(overlay.name)
|
||
} else {
|
||
// clear selected
|
||
let el = overlay.anchor
|
||
if (['INPUT', 'TEXTAREA'].includes(el.tagName)) {
|
||
el.value = ''
|
||
delete el.dataset.selected
|
||
delete el.dataset.selectedIndex
|
||
}
|
||
}
|
||
break
|
||
}
|
||
case 37: { // left
|
||
if (!overlay.displayed) return
|
||
let { item, index, parents } = this.getCurrent(overlay.name)
|
||
// collapse parent if any
|
||
if (parents) {
|
||
item = options.items[parents]
|
||
index = parseInt(parents)
|
||
parents = ''
|
||
refreshIndex = true
|
||
}
|
||
if (Array.isArray(item?.items) && item.items.length > 0 && item.expanded) {
|
||
event.delegate = query(overlay.box).find(`.w2ui-menu-item[index="${index}"]`).get(0)
|
||
overlay.selected = index
|
||
this.menuClick(overlay, event, parseInt(index), parents)
|
||
}
|
||
filter = false
|
||
break
|
||
}
|
||
case 39: { // right
|
||
if (!overlay.displayed) return
|
||
let { item, index, parents } = this.getCurrent(overlay.name)
|
||
if (Array.isArray(item?.items) && item.items.length > 0 && !item.expanded) {
|
||
event.delegate = query(overlay.box).find('.w2ui-selected').get(0)
|
||
this.menuClick(overlay, event, parseInt(index), parents)
|
||
}
|
||
filter = false
|
||
break
|
||
}
|
||
case 38: { // up
|
||
if (!overlay.displayed) {
|
||
break
|
||
}
|
||
let chain = this.getActiveChain(overlay.name)
|
||
if (overlay.selected == null || overlay.selected?.length == 0) {
|
||
overlay.selected = chain[chain.length-1]
|
||
} else {
|
||
let ind = chain.indexOf(overlay.selected)
|
||
// selected not in chain of items
|
||
if (ind == -1) {
|
||
overlay.selected = chain[chain.length-1]
|
||
}
|
||
// not first item
|
||
if (ind > 0) {
|
||
overlay.selected = chain[ind - 1]
|
||
}
|
||
}
|
||
filter = false
|
||
refreshIndex = true
|
||
event.preventDefault()
|
||
break
|
||
}
|
||
case 40: { // down
|
||
if (!overlay.displayed) {
|
||
break
|
||
}
|
||
let chain = this.getActiveChain(overlay.name)
|
||
if (overlay.selected == null || overlay.selected?.length == 0) {
|
||
overlay.selected = chain[0]
|
||
} else {
|
||
let ind = chain.indexOf(overlay.selected)
|
||
// selected not in chain of items
|
||
if (ind == -1) {
|
||
overlay.selected = chain[0]
|
||
}
|
||
// not the last item
|
||
if (ind < chain.length - 1) {
|
||
overlay.selected = chain[ind + 1]
|
||
}
|
||
}
|
||
filter = false
|
||
refreshIndex = true
|
||
event.preventDefault()
|
||
break
|
||
}
|
||
}
|
||
// filter
|
||
if (filter && overlay.displayed && ((options.filter && event._searchType == 'filter')
|
||
|| (options.search && event._searchType == 'search'))) {
|
||
let count = this.applyFilter(overlay.name, null, search)
|
||
overlay.tmp.searchCount = count
|
||
overlay.tmp.search = search
|
||
// if selected is not in searched items
|
||
if (count === 0 || !this.getActiveChain(overlay.name).includes(overlay.selected)) {
|
||
overlay.selected = null
|
||
}
|
||
this.refreshSearch(overlay.name)
|
||
}
|
||
if (refreshIndex) {
|
||
this.refreshIndex(overlay.name)
|
||
}
|
||
}
|
||
}
|
||
class DateTooltip extends Tooltip {
|
||
constructor() {
|
||
super()
|
||
let td = new Date()
|
||
this.daysCount = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||
this.today = td.getFullYear() + '/' + (Number(td.getMonth()) + 1) + '/' + td.getDate()
|
||
this.defaults = w2utils.extend({}, this.defaults, {
|
||
position : 'top|bottom',
|
||
class : 'w2ui-calendar',
|
||
type : 'date', // can be date/time/datetime
|
||
format : '',
|
||
value : '', // initial date (in w2utils.settings format)
|
||
start : null,
|
||
end : null,
|
||
blockDates : [], // array of blocked dates
|
||
blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
|
||
colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
|
||
arrowSize : 12,
|
||
autoResize : false,
|
||
anchorClass : 'w2ui-focus',
|
||
autoShowOn : 'focus',
|
||
hideOn : ['doc-click', 'focus-change'],
|
||
onSelect : null
|
||
})
|
||
}
|
||
attach(anchor, text) {
|
||
let options
|
||
if (arguments.length == 1 && anchor.anchor) {
|
||
options = anchor
|
||
anchor = options.anchor
|
||
} else if (arguments.length === 2 && text != null && typeof text === 'object') {
|
||
options = text
|
||
options.anchor = anchor
|
||
}
|
||
let prevHideOn = options.hideOn
|
||
options = w2utils.extend({}, this.defaults, options || {})
|
||
if (prevHideOn) {
|
||
options.hideOn = prevHideOn
|
||
}
|
||
if (!options.format) {
|
||
let df = w2utils.settings.dateFormat
|
||
let tf = w2utils.settings.timeFormat
|
||
if (options.type == 'date') {
|
||
options.format = df
|
||
} else if (options.type == 'time') {
|
||
options.format = tf
|
||
} else {
|
||
options.format = df + '|' + tf
|
||
}
|
||
}
|
||
let cal = options.type == 'time' ? this.getHourHTML(options) : this.getMonthHTML(options)
|
||
options.style += '; padding: 0;'
|
||
options.html = cal.html
|
||
let ret = super.attach(options)
|
||
let overlay = ret.overlay
|
||
Object.assign(overlay.tmp, cal)
|
||
overlay.on('show.attach', event => {
|
||
let overlay = event.detail.overlay
|
||
let anchor = overlay.anchor
|
||
let options = overlay.options
|
||
if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.value && anchor.value) {
|
||
overlay.tmp.initValue = anchor.value
|
||
}
|
||
delete overlay.newValue
|
||
delete overlay.newDate
|
||
})
|
||
overlay.on('show:after.attach', event => {
|
||
if (ret.overlay?.box) {
|
||
this.initControls(ret.overlay)
|
||
}
|
||
})
|
||
overlay.on('update:after.attach', event => {
|
||
if (ret.overlay?.box) {
|
||
this.initControls(ret.overlay)
|
||
}
|
||
})
|
||
overlay.on('hide.attach', event => {
|
||
let overlay = event.detail.overlay
|
||
let anchor = overlay.anchor
|
||
if (overlay.newValue != null) {
|
||
if (overlay.newDate) {
|
||
overlay.newValue = overlay.newDate + ' ' + overlay.newValue
|
||
}
|
||
if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != overlay.newValue) {
|
||
anchor.value = overlay.newValue
|
||
}
|
||
let edata = this.trigger('select', { date: overlay.newValue, target: overlay.name, overlay })
|
||
if (edata.isCancelled === true) return
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
})
|
||
ret.select = (callback) => {
|
||
overlay.on('select.attach', (event) => { callback(event) })
|
||
return ret
|
||
}
|
||
return ret
|
||
}
|
||
initControls(overlay) {
|
||
let options = overlay.options
|
||
let moveMonth = (inc) => {
|
||
let { month, year } = overlay.tmp
|
||
month += inc
|
||
if (month > 12) {
|
||
month = 1
|
||
year++
|
||
}
|
||
if (month < 1 ) {
|
||
month = 12
|
||
year--
|
||
}
|
||
let cal = this.getMonthHTML(options, month, year)
|
||
Object.assign(overlay.tmp, cal)
|
||
query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
|
||
this.initControls(overlay)
|
||
}
|
||
let checkJump = (event, dblclick) => {
|
||
query(event.target).parent().find('.w2ui-jump-month, .w2ui-jump-year')
|
||
.removeClass('w2ui-selected')
|
||
query(event.target).addClass('w2ui-selected')
|
||
let dt = new Date()
|
||
let { jumpMonth, jumpYear } = overlay.tmp
|
||
if (dblclick) {
|
||
if (jumpYear == null) jumpYear = dt.getFullYear()
|
||
if (jumpMonth == null) jumpMonth = dt.getMonth() + 1
|
||
}
|
||
if (jumpMonth && jumpYear) {
|
||
let cal = this.getMonthHTML(options, jumpMonth, jumpYear)
|
||
Object.assign(overlay.tmp, cal)
|
||
query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
|
||
overlay.tmp.jump = false
|
||
this.initControls(overlay)
|
||
}
|
||
}
|
||
// events for next/prev buttons and title
|
||
query(overlay.box).find('.w2ui-cal-title')
|
||
.off('.calendar')
|
||
// click on title
|
||
.on('click.calendar', event => {
|
||
Object.assign(overlay.tmp, { jumpYear: null, jumpMonth: null })
|
||
if (overlay.tmp.jump) {
|
||
let { month, year } = overlay.tmp
|
||
let cal = this.getMonthHTML(options, month, year)
|
||
query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
|
||
overlay.tmp.jump = false
|
||
} else {
|
||
query(overlay.box).find('.w2ui-overlay-body .w2ui-cal-days')
|
||
.replace(this.getYearHTML())
|
||
let el = query(overlay.box).find(`[name="${overlay.tmp.year}"]`).get(0)
|
||
if (el) el.scrollIntoView(true)
|
||
overlay.tmp.jump = true
|
||
}
|
||
this.initControls(overlay)
|
||
event.stopPropagation()
|
||
})
|
||
// prev button
|
||
.find('.w2ui-cal-previous')
|
||
.off('.calendar')
|
||
.on('click.calendar', event => {
|
||
moveMonth(-1)
|
||
event.stopPropagation()
|
||
})
|
||
.parent()
|
||
// next button
|
||
.find('.w2ui-cal-next')
|
||
.off('.calendar')
|
||
.on('click.calendar', event => {
|
||
moveMonth(1)
|
||
event.stopPropagation()
|
||
})
|
||
// now button
|
||
query(overlay.box).find('.w2ui-cal-now')
|
||
.off('.calendar')
|
||
.on('click.calendar', event => {
|
||
if (options.type == 'datetime') {
|
||
if (overlay.newDate) {
|
||
overlay.newValue = w2utils.formatTime(new Date(), options.format.split('|')[1])
|
||
} else {
|
||
overlay.newValue = w2utils.formatDateTime(new Date(), options.format)
|
||
}
|
||
} else if (options.type == 'date') {
|
||
overlay.newValue = w2utils.formatDate(new Date(), options.format)
|
||
} else if (options.type == 'time') {
|
||
overlay.newValue = w2utils.formatTime(new Date(), options.format)
|
||
}
|
||
this.hide(overlay.name)
|
||
})
|
||
// events for dates
|
||
query(overlay.box)
|
||
.off('.calendar')
|
||
.on('click.calendar', { delegate: '.w2ui-day.w2ui-date' }, event => {
|
||
if (options.type == 'datetime') {
|
||
overlay.newDate = query(event.target).attr('date')
|
||
query(overlay.box).find('.w2ui-overlay-body').html(this.getHourHTML(overlay.options).html)
|
||
this.initControls(overlay)
|
||
} else {
|
||
overlay.newValue = query(event.target).attr('date')
|
||
this.hide(overlay.name)
|
||
}
|
||
})
|
||
// click on month
|
||
.on('click.calendar', { delegate: '.w2ui-jump-month' }, event => {
|
||
overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name'))
|
||
checkJump(event)
|
||
})
|
||
// double click on month
|
||
.on('dblclick.calendar', { delegate: '.w2ui-jump-month' }, event => {
|
||
overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name'))
|
||
checkJump(event, true)
|
||
})
|
||
// click on year
|
||
.on('click.calendar', { delegate: '.w2ui-jump-year' }, event => {
|
||
overlay.tmp.jumpYear = parseInt(query(event.target).attr('name'))
|
||
checkJump(event)
|
||
})
|
||
// dbl click on year
|
||
.on('dblclick.calendar', { delegate: '.w2ui-jump-year' }, event => {
|
||
overlay.tmp.jumpYear = parseInt(query(event.target).attr('name'))
|
||
checkJump(event, true)
|
||
})
|
||
// click on hour
|
||
.on('click.calendar', { delegate: '.w2ui-time.hour' }, event => {
|
||
let hour = query(event.target).attr('hour')
|
||
let min = this.str2min(options.value) % 60
|
||
if (overlay.tmp.initValue && !options.value) {
|
||
min = this.str2min(overlay.tmp.initValue) % 60
|
||
}
|
||
if (options.noMinutes) {
|
||
overlay.newValue = this.min2str(hour * 60, options.format)
|
||
this.hide(overlay.name)
|
||
} else {
|
||
overlay.newValue = hour + ':' + min
|
||
let html = this.getMinHTML(hour, options).html
|
||
query(overlay.box).find('.w2ui-overlay-body').html(html)
|
||
this.initControls(overlay)
|
||
}
|
||
})
|
||
// click on minute
|
||
.on('click.calendar', { delegate: '.w2ui-time.min' }, event => {
|
||
let hour = Math.floor(this.str2min(overlay.newValue) / 60)
|
||
let time = (hour * 60) + parseInt(query(event.target).attr('min'))
|
||
overlay.newValue = this.min2str(time, options.format)
|
||
this.hide(overlay.name)
|
||
})
|
||
}
|
||
getMonthHTML(options, month, year) {
|
||
let days = w2utils.settings.fulldays.slice() // creates copy of the array
|
||
let sdays = w2utils.settings.shortdays.slice() // creates copy of the array
|
||
if (w2utils.settings.weekStarts !== 'M') {
|
||
days.unshift(days.pop())
|
||
sdays.unshift(sdays.pop())
|
||
}
|
||
let td = new Date()
|
||
let dayLengthMil = 1000 * 60 * 60 * 24
|
||
let selected = options.type === 'datetime'
|
||
? w2utils.isDateTime(options.value, options.format, true)
|
||
: w2utils.isDate(options.value, options.format, true)
|
||
let selected_dsp = w2utils.formatDate(selected)
|
||
// normalize date
|
||
if (month == null || year == null) {
|
||
year = selected ? selected.getFullYear() : td.getFullYear()
|
||
month = selected ? selected.getMonth() + 1 : td.getMonth() + 1
|
||
}
|
||
if (month > 12) { month -= 12; year++ }
|
||
if (month < 1 || month === 0) { month += 12; year-- }
|
||
if (year/4 == Math.floor(year/4)) { this.daysCount[1] = 29 } else { this.daysCount[1] = 28 }
|
||
options.current = month + '/' + year
|
||
// start with the required date
|
||
td = new Date(year, month-1, 1)
|
||
let weekDay = td.getDay()
|
||
let weekDays = ''
|
||
let st = w2utils.settings.weekStarts
|
||
for (let i = 0; i < sdays.length; i++) {
|
||
let isSat = (st == 'M' && i == 5) || (st != 'M' && i == 6) ? true : false
|
||
let isSun = (st == 'M' && i == 6) || (st != 'M' && i == 0) ? true : false
|
||
weekDays += `<div class="w2ui-day w2ui-weekday ${isSat ? 'w2ui-sunday' : ''} ${isSun ? 'w2ui-saturday' : ''}">${sdays[i]}</div>`
|
||
}
|
||
let html = `
|
||
<div class="w2ui-cal-title">
|
||
<div class="w2ui-cal-previous">
|
||
<div></div>
|
||
</div>
|
||
<div class="w2ui-cal-next">
|
||
<div></div>
|
||
</div>
|
||
${w2utils.settings.fullmonths[month-1]}, ${year}
|
||
<span class="arrow-down"></span>
|
||
</div>
|
||
<div class="w2ui-cal-days">
|
||
${weekDays}
|
||
`
|
||
let DT = new Date(`${year}/${month}/1`) // first of month
|
||
/**
|
||
* Move to noon, instead of midnight. If not, then the date when time saving happens
|
||
* will be duplicated in the calendar
|
||
*/
|
||
DT = new Date(DT.getTime() + dayLengthMil * 0.5)
|
||
let weekday = DT.getDay()
|
||
if (w2utils.settings.weekStarts == 'M') weekDay--
|
||
if (weekday > 0) {
|
||
DT = new Date(DT.getTime() - (weekDay * dayLengthMil))
|
||
}
|
||
for (let ci = 0; ci < 42; ci++) {
|
||
let className = []
|
||
let dt = `${DT.getFullYear()}/${DT.getMonth()+1}/${DT.getDate()}`
|
||
if (DT.getDay() === 6) className.push('w2ui-saturday')
|
||
if (DT.getDay() === 0) className.push('w2ui-sunday')
|
||
if (DT.getMonth() + 1 !== month) className.push('outside')
|
||
if (dt == this.today) className.push('w2ui-today')
|
||
let dspDay = DT.getDate()
|
||
let col = ''
|
||
let bgcol = ''
|
||
let tmp_dt, tmp_dt_fmt
|
||
if (options.type === 'datetime') {
|
||
tmp_dt = w2utils.formatDateTime(dt, options.format)
|
||
tmp_dt_fmt = w2utils.formatDate(dt, w2utils.settings.dateFormat)
|
||
} else {
|
||
tmp_dt = w2utils.formatDate(dt, options.format)
|
||
tmp_dt_fmt = tmp_dt
|
||
}
|
||
if (options.colored && options.colored[tmp_dt_fmt] !== undefined) { // if there is predefined colors for dates
|
||
let tmp = options.colored[tmp_dt_fmt].split('|')
|
||
bgcol = 'background-color: ' + tmp[0] + ';'
|
||
col = 'color: ' + tmp[1] + ';'
|
||
}
|
||
html += `<div class="w2ui-day ${this.inRange(tmp_dt, options, true)
|
||
? 'w2ui-date ' + (tmp_dt_fmt == selected_dsp ? 'w2ui-selected' : '')
|
||
: 'w2ui-blocked'
|
||
} ${className.join(' ')}"
|
||
style="${col + bgcol}" date="${tmp_dt_fmt}" data-date="${DT.getTime()}">
|
||
${dspDay}
|
||
</div>`
|
||
DT = new Date(DT.getTime() + dayLengthMil)
|
||
}
|
||
html += '</div>'
|
||
if (options.btnNow) {
|
||
let label = w2utils.lang('Today' + (options.type == 'datetime' ? ' & Now' : ''))
|
||
html += `<div class="w2ui-cal-now">${label}</div>`
|
||
}
|
||
return { html, month, year }
|
||
}
|
||
getYearHTML() {
|
||
let mhtml = ''
|
||
let yhtml = ''
|
||
for (let m = 0; m < w2utils.settings.fullmonths.length; m++) {
|
||
mhtml += `<div class="w2ui-jump-month" name="${m+1}">${w2utils.settings.shortmonths[m]}</div>`
|
||
}
|
||
for (let y = w2utils.settings.dateStartYear; y <= w2utils.settings.dateEndYear; y++) {
|
||
yhtml += `<div class="w2ui-jump-year" name="${y}">${y}</div>`
|
||
}
|
||
return `<div class="w2ui-cal-jump">
|
||
<div id="w2ui-jump-month">${mhtml}</div>
|
||
<div id="w2ui-jump-year">${yhtml}</div>
|
||
</div>`
|
||
}
|
||
getHourHTML(options) {
|
||
options = options ?? {}
|
||
if (!options.format) options.format = w2utils.settings.timeFormat
|
||
let h24 = (options.format.indexOf('h24') > -1)
|
||
let value = options.value ? options.value : (options.anchor ? options.anchor.value : '')
|
||
let tmp = []
|
||
for (let a = 0; a < 24; a++) {
|
||
let time = (a >= 12 && !h24 ? a - 12 : a) + ':00' + (!h24 ? (a < 12 ? ' am' : ' pm') : '')
|
||
if (a == 12 && !h24) time = '12:00 pm'
|
||
if (!tmp[Math.floor(a/8)]) tmp[Math.floor(a/8)] = ''
|
||
let tm1 = this.min2str(this.str2min(time))
|
||
let tm2 = this.min2str(this.str2min(time) + 59)
|
||
if (options.type === 'datetime') {
|
||
let dt = w2utils.isDateTime(value, options.format, true)
|
||
let fm = options.format.split('|')[0].trim()
|
||
tm1 = w2utils.formatDate(dt, fm) + ' ' + tm1
|
||
tm2 = w2utils.formatDate(dt, fm) + ' ' + tm2
|
||
}
|
||
let valid = this.inRange(tm1, options) || this.inRange(tm2, options)
|
||
tmp[Math.floor(a/8)] += `<span hour="${a}"
|
||
class="hour ${valid ? 'w2ui-time ' : 'w2ui-blocked'}">${time}</span>`
|
||
}
|
||
let html = `<div class="w2ui-calendar">
|
||
<div class="w2ui-time-title">${w2utils.lang('Select Hour')}</div>
|
||
<div class="w2ui-cal-time">
|
||
<div class="w2ui-cal-column">${tmp[0]}</div>
|
||
<div class="w2ui-cal-column">${tmp[1]}</div>
|
||
<div class="w2ui-cal-column">${tmp[2]}</div>
|
||
</div>
|
||
${options.btnNow ? `<div class="w2ui-cal-now">${w2utils.lang('Now')}</div>` : '' }
|
||
</div>`
|
||
return { html }
|
||
}
|
||
getMinHTML(hour, options) {
|
||
if (hour == null) hour = 0
|
||
options = options ?? {}
|
||
if (!options.format) options.format = w2utils.settings.timeFormat
|
||
let h24 = (options.format.indexOf('h24') > -1)
|
||
let value = options.value ? options.value : (options.anchor ? options.anchor.value : '')
|
||
let tmp = []
|
||
for (let a = 0; a < 60; a += 5) {
|
||
let time = (hour > 12 && !h24 ? hour - 12 : hour) + ':' + (a < 10 ? 0 : '') + a + ' ' + (!h24 ? (hour < 12 ? 'am' : 'pm') : '')
|
||
let tm = time
|
||
let ind = a < 20 ? 0 : (a < 40 ? 1 : 2)
|
||
if (!tmp[ind]) tmp[ind] = ''
|
||
if (options.type === 'datetime') {
|
||
let dt = w2utils.isDateTime(value, options.format, true)
|
||
let fm = options.format.split('|')[0].trim()
|
||
tm = w2utils.formatDate(dt, fm) + ' ' + tm
|
||
}
|
||
tmp[ind] += `<span min="${a}" class="min ${(this.inRange(tm, options) ? 'w2ui-time ' : 'w2ui-blocked')}">${time}</span>`
|
||
}
|
||
let html = `<div class="w2ui-calendar">
|
||
<div class="w2ui-time-title">${w2utils.lang('Select Minute')}</div>
|
||
<div class="w2ui-cal-time">
|
||
<div class="w2ui-cal-column">${tmp[0]}</div>
|
||
<div class="w2ui-cal-column">${tmp[1]}</div>
|
||
<div class="w2ui-cal-column">${tmp[2]}</div>
|
||
</div>
|
||
${options.btnNow ? `<div class="w2ui-cal-now">${w2utils.lang('Now')}</div>` : '' }
|
||
</div>`
|
||
return { html }
|
||
}
|
||
// checks if date is in range (loost at start, end, blockDates, blockWeekdays)
|
||
inRange(str, options, dateOnly) {
|
||
let inRange = false
|
||
if (options.type === 'date') {
|
||
let dt = w2utils.isDate(str, options.format, true)
|
||
if (dt) {
|
||
// enable range
|
||
if (options.start || options.end) {
|
||
let st = (typeof options.start === 'string' ? options.start : query(options.start).val())
|
||
let en = (typeof options.end === 'string' ? options.end : query(options.end).val())
|
||
let start = w2utils.isDate(st, options.format, true)
|
||
let end = w2utils.isDate(en, options.format, true)
|
||
let current = new Date(dt)
|
||
if (!start) start = current
|
||
if (!end) end = current
|
||
if (current >= start && current <= end) inRange = true
|
||
} else {
|
||
inRange = true
|
||
}
|
||
// block predefined dates
|
||
if (Array.isArray(options.blockDates) && options.blockDates.includes(str)) inRange = false
|
||
// block weekdays
|
||
if (Array.isArray(options.blockWeekdays) && options.blockWeekdays.includes(dt.getDay())) inRange = false
|
||
}
|
||
} else if (options.type === 'time') {
|
||
if (options.start || options.end) {
|
||
let tm = this.str2min(str)
|
||
let tm1 = this.str2min(options.start)
|
||
let tm2 = this.str2min(options.end)
|
||
if (!tm1) tm1 = tm
|
||
if (!tm2) tm2 = tm
|
||
if (tm >= tm1 && tm <= tm2) inRange = true
|
||
} else {
|
||
inRange = true
|
||
}
|
||
} else if (options.type === 'datetime') {
|
||
let dt = w2utils.isDateTime(str, options.format, true)
|
||
if (dt) {
|
||
let format = options.format.split('|').map(format => format.trim())
|
||
if (dateOnly) {
|
||
let date = w2utils.formatDate(dt, format[0])
|
||
let opts = w2utils.extend({}, options, { type: 'date', format: format[0] })
|
||
if (this.inRange(date, opts)) inRange = true
|
||
} else {
|
||
let time = w2utils.formatTime(dt, format[1])
|
||
let opts = { type: 'time', format: format[1], start: options.startTime, end: options.endTime }
|
||
if (this.inRange(time, opts)) inRange = true
|
||
}
|
||
}
|
||
}
|
||
return inRange
|
||
}
|
||
// converts time into number of minutes since midnight -- '11:50am' => 710
|
||
str2min(str) {
|
||
if (typeof str !== 'string') return null
|
||
let tmp = str.split(':')
|
||
if (tmp.length === 2) {
|
||
tmp[0] = parseInt(tmp[0])
|
||
tmp[1] = parseInt(tmp[1])
|
||
if (str.indexOf('pm') !== -1 && tmp[0] !== 12) tmp[0] += 12
|
||
if (str.includes('am') && tmp[0] == 12) tmp[0] = 0 // 12:00am - is midnight
|
||
} else {
|
||
return null
|
||
}
|
||
return tmp[0] * 60 + tmp[1]
|
||
}
|
||
// converts minutes since midnight into time str -- 710 => '11:50am'
|
||
min2str(time, format) {
|
||
let ret = ''
|
||
if (time >= 24 * 60) time = time % (24 * 60)
|
||
if (time < 0) time = 24 * 60 + time
|
||
let hour = Math.floor(time/60)
|
||
let min = ((time % 60) < 10 ? '0' : '') + (time % 60)
|
||
if (!format) { format = w2utils.settings.timeFormat}
|
||
if (format.indexOf('h24') !== -1) {
|
||
ret = hour + ':' + min
|
||
} else {
|
||
ret = (hour <= 12 ? hour : hour - 12) + ':' + min + ' ' + (hour >= 12 ? 'pm' : 'am')
|
||
}
|
||
return ret
|
||
}
|
||
}
|
||
let w2tooltip = new Tooltip()
|
||
let w2menu = new MenuTooltip()
|
||
let w2color = new ColorTooltip()
|
||
let w2date = new DateTooltip()
|
||
|
||
/*
|
||
// pull records from remote source for w2menu
|
||
clearCache() {
|
||
let options = this.options
|
||
options.items = []
|
||
this.tmp.xhr_loading = false
|
||
this.tmp.xhr_search = ''
|
||
this.tmp.xhr_total = -1
|
||
}
|
||
request(interval) {
|
||
let obj = this
|
||
let options = this.options
|
||
let search = $(obj.el).val() || ''
|
||
// if no url - do nothing
|
||
if (!options.url) return
|
||
// --
|
||
if (obj.type === 'enum') {
|
||
let tmp = $(obj.helpers.multi).find('input')
|
||
if (tmp.length === 0) search = ''; else search = tmp.val()
|
||
}
|
||
if (obj.type === 'list') {
|
||
let tmp = $(obj.helpers.focus).find('input')
|
||
if (tmp.length === 0) search = ''; else search = tmp.val()
|
||
}
|
||
if (options.minLength !== 0 && search.length < options.minLength) {
|
||
options.items = [] // need to empty the list
|
||
this.updateOverlay()
|
||
return
|
||
}
|
||
if (interval == null) interval = options.interval
|
||
if (obj.tmp.xhr_search == null) obj.tmp.xhr_search = ''
|
||
if (obj.tmp.xhr_total == null) obj.tmp.xhr_total = -1
|
||
// check if need to search
|
||
if (options.url && $(obj.el).prop('readonly') !== true && $(obj.el).prop('disabled') !== true && (
|
||
(options.items.length === 0 && obj.tmp.xhr_total !== 0) ||
|
||
(obj.tmp.xhr_total == options.cacheMax && search.length > obj.tmp.xhr_search.length) ||
|
||
(search.length >= obj.tmp.xhr_search.length && search.substr(0, obj.tmp.xhr_search.length) !== obj.tmp.xhr_search) ||
|
||
(search.length < obj.tmp.xhr_search.length)
|
||
)) {
|
||
// empty list
|
||
if (obj.tmp.xhr) try { obj.tmp.xhr.abort() } catch (e) {}
|
||
obj.tmp.xhr_loading = true
|
||
obj.search()
|
||
// timeout
|
||
clearTimeout(obj.tmp.timeout)
|
||
obj.tmp.timeout = setTimeout(() => {
|
||
// trigger event
|
||
let url = options.url
|
||
let postData = {
|
||
search : search,
|
||
max : options.cacheMax
|
||
}
|
||
$.extend(postData, options.postData)
|
||
let edata = obj.trigger({ phase: 'before', type: 'request', search: search, target: obj.el, url: url, postData: postData })
|
||
if (edata.isCancelled === true) return
|
||
url = edata.url
|
||
postData = edata.postData
|
||
let ajaxOptions = {
|
||
type : 'GET',
|
||
url : url,
|
||
data : postData,
|
||
dataType : 'JSON' // expected from server
|
||
}
|
||
if (options.method) ajaxOptions.type = options.method
|
||
if (w2utils.settings.dataType === 'JSON') {
|
||
ajaxOptions.type = 'POST'
|
||
ajaxOptions.data = JSON.stringify(ajaxOptions.data)
|
||
ajaxOptions.contentType = 'application/json'
|
||
}
|
||
if (w2utils.settings.dataType === 'HTTPJSON') {
|
||
ajaxOptions.data = { request: JSON.stringify(ajaxOptions.data) }
|
||
}
|
||
if (w2utils.settings.dataType === 'RESTFULLJSON') {
|
||
ajaxOptions.data = JSON.stringify(ajaxOptions.data)
|
||
ajaxOptions.contentType = 'application/json'
|
||
}
|
||
if (options.method != null) ajaxOptions.type = options.method
|
||
obj.tmp.xhr = $.ajax(ajaxOptions)
|
||
.done((data, status, xhr) => {
|
||
// trigger event
|
||
let edata2 = obj.trigger({ phase: 'before', type: 'load', target: obj.el, search: postData.search, data: data, xhr: xhr })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
data = edata2.data
|
||
if (typeof data === 'string') data = JSON.parse(data)
|
||
// if server just returns array
|
||
if (Array.isArray(data)) {
|
||
data = { records: data }
|
||
}
|
||
// needed for backward compatibility
|
||
if (data.records == null && data.items != null) {
|
||
data.records = data.items
|
||
delete data.items
|
||
}
|
||
// handles Golang marshal of empty arrays to null
|
||
if (data.status == 'success' && data.records == null) {
|
||
data.records = []
|
||
}
|
||
if (!Array.isArray(data.records)) {
|
||
console.error('ERROR: server did not return proper data structure', '\n',
|
||
' - it should return', { status: 'success', records: [{ id: 1, text: 'item' }] }, '\n',
|
||
' - or just an array ', [{ id: 1, text: 'item' }], '\n',
|
||
' - actual response', typeof data === 'object' ? data : xhr.responseText)
|
||
return
|
||
}
|
||
// remove all extra items if more then needed for cache
|
||
if (data.records.length > options.cacheMax) data.records.splice(options.cacheMax, 100000)
|
||
// map id and text
|
||
if (options.recId == null && options.recid != null) options.recId = options.recid // since lower-case recid is used in grid
|
||
if (options.recId || options.recText) {
|
||
data.records.forEach((item) => {
|
||
if (typeof options.recId === 'string') item.id = item[options.recId]
|
||
if (typeof options.recId === 'function') item.id = options.recId(item)
|
||
if (typeof options.recText === 'string') item.text = item[options.recText]
|
||
if (typeof options.recText === 'function') item.text = options.recText(item)
|
||
})
|
||
}
|
||
// remember stats
|
||
obj.tmp.xhr_loading = false
|
||
obj.tmp.xhr_search = search
|
||
obj.tmp.xhr_total = data.records.length
|
||
obj.tmp.lastError = ''
|
||
options.items = w2utils.normMenu(data.records)
|
||
if (search === '' && data.records.length === 0) obj.tmp.emptySet = true; else obj.tmp.emptySet = false
|
||
// preset item
|
||
let find_selected = $(obj.el).data('find_selected')
|
||
if (find_selected) {
|
||
let sel
|
||
if (Array.isArray(find_selected)) {
|
||
sel = []
|
||
find_selected.forEach((find) => {
|
||
let isFound = false
|
||
options.items.forEach((item) => {
|
||
if (item.id == find || (find && find.id == item.id)) {
|
||
sel.push($.extend(true, {}, item))
|
||
isFound = true
|
||
}
|
||
})
|
||
if (!isFound) sel.push(find)
|
||
})
|
||
} else {
|
||
sel = find_selected
|
||
options.items.forEach((item) => {
|
||
if (item.id == find_selected || (find_selected && find_selected.id == item.id)) {
|
||
sel = item
|
||
}
|
||
})
|
||
}
|
||
$(obj.el).data('selected', sel).removeData('find_selected').trigger('input').trigger('change')
|
||
}
|
||
obj.search()
|
||
// event after
|
||
obj.trigger($.extend(edata2, { phase: 'after' }))
|
||
})
|
||
.fail((xhr, status, error) => {
|
||
// trigger event
|
||
let errorObj = { status: status, error: error, rawResponseText: xhr.responseText }
|
||
let edata2 = obj.trigger({ phase: 'before', type: 'error', target: obj.el, search: search, error: errorObj, xhr: xhr })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
if (status !== 'abort') {
|
||
let data
|
||
try { data = JSON.parse(xhr.responseText) } catch (e) {}
|
||
console.error('ERROR: server did not return proper data structure', '\n',
|
||
' - it should return', { status: 'success', records: [{ id: 1, text: 'item' }] }, '\n',
|
||
' - or just an array ', [{ id: 1, text: 'item' }], '\n',
|
||
' - actual response', typeof data === 'object' ? data : xhr.responseText)
|
||
}
|
||
// reset stats
|
||
obj.tmp.xhr_loading = false
|
||
obj.tmp.xhr_search = search
|
||
obj.tmp.xhr_total = 0
|
||
obj.tmp.emptySet = true
|
||
obj.tmp.lastError = (edata2.error || 'Server communication failed')
|
||
options.items = []
|
||
obj.clearCache()
|
||
obj.search()
|
||
obj.updateOverlay(false)
|
||
// event after
|
||
obj.trigger($.extend(edata2, { phase: 'after' }))
|
||
})
|
||
// event after
|
||
obj.trigger($.extend(edata, { phase: 'after' }))
|
||
}, interval)
|
||
}
|
||
}
|
||
search() {
|
||
let obj = this
|
||
let options = this.options
|
||
let search = $(obj.el).val()
|
||
let target = obj.el
|
||
let ids = []
|
||
let selected = $(obj.el).data('selected')
|
||
if (obj.type === 'enum') {
|
||
target = $(obj.helpers.multi).find('input')
|
||
search = target.val()
|
||
for (let s in selected) { if (selected[s]) ids.push(selected[s].id) }
|
||
}
|
||
else if (obj.type === 'list') {
|
||
target = $(obj.helpers.focus).find('input')
|
||
search = target.val()
|
||
for (let s in selected) { if (selected[s]) ids.push(selected[s].id) }
|
||
}
|
||
let items = options.items
|
||
if (obj.tmp.xhr_loading !== true) {
|
||
let shown = 0
|
||
for (let i = 0; i < items.length; i++) {
|
||
let item = items[i]
|
||
if (options.compare != null) {
|
||
if (typeof options.compare === 'function') {
|
||
item.hidden = (options.compare.call(this, item, search) === false ? true : false)
|
||
}
|
||
} else {
|
||
let prefix = ''
|
||
let suffix = ''
|
||
if (['is', 'begins'].indexOf(options.match) !== -1) prefix = '^'
|
||
if (['is', 'ends'].indexOf(options.match) !== -1) suffix = '$'
|
||
try {
|
||
let re = new RegExp(prefix + search + suffix, 'i')
|
||
if (re.test(item.text) || item.text === '...') item.hidden = false; else item.hidden = true
|
||
} catch (e) {}
|
||
}
|
||
if (options.filter === false) item.hidden = false
|
||
// do not show selected items
|
||
if (obj.type === 'enum' && $.inArray(item.id, ids) !== -1) item.hidden = true
|
||
if (item.hidden !== true) { shown++; delete item.hidden }
|
||
}
|
||
// preselect first item
|
||
options.index = []
|
||
options.spinner = false
|
||
setTimeout(() => {
|
||
if (options.markSearch && $('#w2ui-overlay .no-matches').length == 0) { // do not highlight when no items
|
||
$('#w2ui-overlay').w2marker(search)
|
||
}
|
||
}, 1)
|
||
} else {
|
||
items.splice(0, options.cacheMax)
|
||
options.spinner = true
|
||
}
|
||
// only update overlay when it is displayed already
|
||
if ($('#w2ui-overlay').length > 0) {
|
||
obj.updateOverlay()
|
||
}
|
||
}
|
||
*/
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2color, w2menu
|
||
*
|
||
* == TODO ==
|
||
* - tab navigation (index state)
|
||
* - vertical toolbar
|
||
* - w2menu on second click of tb button should hide
|
||
* - button display groups for each show/hide, possibly add state: { single: t/f, multiple: t/f, type: 'font' }
|
||
* - item.count - should just support html, so a custom block can be created, such as a colored line
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
* - removed jQuery dependency
|
||
* - item.icon - can be class or <custom-icon-component> or <svg>
|
||
* - new w2tooltips and w2menu
|
||
* - scroll returns promise
|
||
* - added onMouseEntter, onMouseLeave, onMouseDown, onMouseUp events
|
||
* - add(..., skipRefresh), insert(..., skipRefresh)
|
||
*/
|
||
|
||
class w2toolbar extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.box = null // DOM Element that holds the element
|
||
this.name = null // unique name for w2ui
|
||
this.routeData = {} // data for dynamic routes
|
||
this.items = []
|
||
this.right = '' // HTML text on the right of toolbar
|
||
this.tooltip = 'top|left'// can be top, bottom, left, right
|
||
this.onClick = null
|
||
this.onMouseDown = null
|
||
this.onMouseUp = null
|
||
this.onMouseEnter = null // mouse enter the button event
|
||
this.onMouseLeave = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.item_template = {
|
||
id: null, // command to be sent to all event handlers
|
||
type: 'button', // button, check, radio, drop, menu, menu-radio, menu-check, break, html, spacer
|
||
text: null,
|
||
html: '',
|
||
tooltip: null, // w2toolbar.tooltip should be
|
||
count: null,
|
||
hidden: false,
|
||
disabled: false,
|
||
checked: false, // used for radio buttons
|
||
icon: null,
|
||
route: null, // if not null, it is route to go
|
||
arrow: null, // arrow down for drop/menu types
|
||
style: null, // extra css style for caption
|
||
group: null, // used for radio buttons
|
||
items: null, // for type menu* it is an array of items in the menu
|
||
selected: null, // used for menu-check, menu-radio
|
||
color: null, // color value - used in color pickers
|
||
overlay: { // additional options for overlay
|
||
anchorClass: ''
|
||
},
|
||
onClick: null,
|
||
onRefresh: null
|
||
}
|
||
this.last = {
|
||
badge: {}
|
||
}
|
||
// mix in options, w/o items
|
||
let items = options.items
|
||
delete options.items
|
||
Object.assign(this, options)
|
||
// add item via method to makes sure item_template is applied
|
||
if (Array.isArray(items)) this.add(items, true)
|
||
// need to reassign back to keep it in config
|
||
options.items = items
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
}
|
||
add(items, skipRefresh) {
|
||
this.insert(null, items, skipRefresh)
|
||
}
|
||
insert(id, items, skipRefresh) {
|
||
if (!Array.isArray(items)) items = [items]
|
||
items.forEach((item, idx, arr) => {
|
||
if (typeof item === 'string') {
|
||
item = arr[idx] = { id: item, text: item }
|
||
}
|
||
// checks
|
||
let valid = ['button', 'check', 'radio', 'drop', 'menu', 'menu-radio', 'menu-check', 'color', 'text-color', 'html',
|
||
'break', 'spacer', 'new-line']
|
||
if (!valid.includes(String(item.type))) {
|
||
console.log('ERROR: The parameter "type" should be one of the following:', valid, `, but ${item.type} is supplied.`, item)
|
||
return
|
||
}
|
||
if (item.id == null && !['break', 'spacer', 'new-line'].includes(item.type)) {
|
||
console.log('ERROR: The parameter "id" is required but not supplied.', item)
|
||
return
|
||
}
|
||
if (item.type == null) {
|
||
console.log('ERROR: The parameter "type" is required but not supplied.', item)
|
||
return
|
||
}
|
||
if (!w2utils.checkUniqueId(item.id, this.items, 'toolbar', this.name)) return
|
||
// add item
|
||
let newItem = w2utils.extend({}, this.item_template, item)
|
||
if (newItem.type == 'menu-check') {
|
||
if (!Array.isArray(newItem.selected)) newItem.selected = []
|
||
if (Array.isArray(newItem.items)) {
|
||
newItem.items.forEach(it => {
|
||
if (typeof it === 'string') {
|
||
it = arr[idx] = { id: it, text: it }
|
||
}
|
||
if (it.checked && !newItem.selected.includes(it.id)) newItem.selected.push(it.id)
|
||
if (!it.checked && newItem.selected.includes(it.id)) it.checked = true
|
||
if (it.checked == null) it.checked = false
|
||
})
|
||
}
|
||
} else if (newItem.type == 'menu-radio') {
|
||
if (Array.isArray(newItem.items)) {
|
||
newItem.items.forEach((it, idx, arr) => {
|
||
if (typeof it === 'string') {
|
||
it = arr[idx] = { id: it, text: it }
|
||
}
|
||
if (it.checked && newItem.selected == null) newItem.selected = it.id; else it.checked = false
|
||
if (!it.checked && newItem.selected == it.id) it.checked = true
|
||
if (it.checked == null) it.checked = false
|
||
})
|
||
}
|
||
}
|
||
if (id == null) {
|
||
this.items.push(newItem)
|
||
} else {
|
||
let middle = this.get(id, true)
|
||
this.items = this.items.slice(0, middle).concat([newItem], this.items.slice(middle))
|
||
}
|
||
newItem.line = newItem.line ?? 1
|
||
if (skipRefresh !== true) this.refresh(newItem.id)
|
||
})
|
||
if (skipRefresh !== true) this.resize()
|
||
}
|
||
remove() {
|
||
let effected = 0
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it || String(item).indexOf(':') != -1) return
|
||
effected++
|
||
// remove from screen
|
||
query(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id)).remove()
|
||
// remove from array
|
||
let ind = this.get(it.id, true)
|
||
if (ind != null) this.items.splice(ind, 1)
|
||
})
|
||
this.resize()
|
||
return effected
|
||
}
|
||
set(id, newOptions) {
|
||
let item = this.get(id)
|
||
if (item == null) return false
|
||
Object.assign(item, newOptions)
|
||
this.refresh(String(id).split(':')[0])
|
||
return true
|
||
}
|
||
get(id, returnIndex) {
|
||
if (arguments.length === 0) {
|
||
let all = []
|
||
for (let i1 = 0; i1 < this.items.length; i1++) if (this.items[i1].id != null) all.push(this.items[i1].id)
|
||
return all
|
||
}
|
||
let tmp = String(id).split(':')
|
||
for (let i2 = 0; i2 < this.items.length; i2++) {
|
||
let it = this.items[i2]
|
||
// find a menu item
|
||
if (['menu', 'menu-radio', 'menu-check'].includes(it.type) && tmp.length == 2 && it.id == tmp[0]) {
|
||
let subItems = it.items
|
||
if (typeof subItems == 'function') subItems = subItems(this)
|
||
for (let i = 0; i < subItems.length; i++) {
|
||
let item = subItems[i]
|
||
if (item.id == tmp[1] || (item.id == null && item.text == tmp[1])) {
|
||
if (returnIndex == true) return i; else return item
|
||
}
|
||
if (Array.isArray(item.items)) {
|
||
for (let j = 0; j < item.items.length; j++) {
|
||
if (item.items[j].id == tmp[1] || (item.items[j].id == null && item.items[j].text == tmp[1])) {
|
||
if (returnIndex == true) return i; else return item.items[j]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if (it.id == tmp[0]) {
|
||
if (returnIndex == true) return i2; else return it
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
setCount(id, count, className, style) {
|
||
let btn = query(this.box).find(`#tb_${this.name}_item_${w2utils.escapeId(id)} .w2ui-tb-count > span`)
|
||
if (btn.length > 0) {
|
||
btn.removeClass()
|
||
.addClass(className ?? '')
|
||
.text(count)
|
||
.get(0).style.cssText = style ?? ''
|
||
this.last.badge[id] = {
|
||
className: className ?? '',
|
||
style: style ?? ''
|
||
}
|
||
let item = this.get(id)
|
||
item.count = count
|
||
} else {
|
||
this.set(id, { count: count })
|
||
this.setCount(...arguments) // to update styles
|
||
}
|
||
}
|
||
show() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it) return
|
||
it.hidden = false
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it); this.resize() }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
hide() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it) return
|
||
it.hidden = true
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it); this.tooltipHide(it); this.resize() }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
enable() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it) return
|
||
it.disabled = false
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
disable() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it) return
|
||
it.disabled = true
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it); this.tooltipHide(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
check() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it || String(item).indexOf(':') != -1) return
|
||
it.checked = true
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
uncheck() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(item => {
|
||
let it = this.get(item)
|
||
if (!it || String(item).indexOf(':') != -1) return
|
||
// remove overlay
|
||
if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type) && it.checked) {
|
||
w2tooltip.hide(this.name + '-drop')
|
||
}
|
||
it.checked = false
|
||
effected.push(String(item).split(':')[0])
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
click(id, event) {
|
||
// click on menu items
|
||
let tmp = String(id).split(':')
|
||
let it = this.get(tmp[0])
|
||
let items = (it && it.items ? w2utils.normMenu.call(this, it.items, it) : [])
|
||
if (tmp.length > 1) {
|
||
let subItem = this.get(id)
|
||
if (subItem && !subItem.disabled) {
|
||
this.menuClick({ name: this.name, item: it, subItem: subItem, originalEvent: event })
|
||
}
|
||
return
|
||
}
|
||
if (it && !it.disabled) {
|
||
// event before
|
||
let edata = this.trigger('click', {
|
||
target: (id != null ? id : this.name),
|
||
item: it, object: it, originalEvent: event
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// read items again, they might have been changed in the click event handler
|
||
items = (it && it.items ? w2utils.normMenu.call(this, it.items, it) : [])
|
||
let btn = '#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id)
|
||
query(this.box).find(btn).removeClass('down') // need to re-query at the moment -- as well as elsewhere in this function
|
||
if (it.type == 'radio') {
|
||
for (let i = 0; i < this.items.length; i++) {
|
||
let itt = this.items[i]
|
||
if (itt == null || itt.id == it.id || itt.type !== 'radio') continue
|
||
if (itt.group == it.group && itt.checked) {
|
||
itt.checked = false
|
||
this.refresh(itt.id)
|
||
}
|
||
}
|
||
it.checked = true
|
||
query(this.box).find(btn).addClass('checked')
|
||
}
|
||
if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type)) {
|
||
this.tooltipHide(id)
|
||
if (it.checked) {
|
||
w2tooltip.hide(this.name + '-drop')
|
||
return
|
||
} else {
|
||
// timeout is needed to make sure previous overlay hides
|
||
setTimeout(() => {
|
||
let hideDrop = (id, btn) => {
|
||
// need a closure to capture id variable
|
||
let self = this
|
||
return function () {
|
||
self.set(id, { checked: false })
|
||
}
|
||
}
|
||
let el = query(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(it.id))
|
||
if (!w2utils.isPlainObject(it.overlay)) it.overlay = {}
|
||
if (it.type == 'drop') {
|
||
w2tooltip.show(w2utils.extend({
|
||
html: it.html,
|
||
class: 'w2ui-white',
|
||
hideOn: ['doc-click']
|
||
}, it.overlay, {
|
||
anchor: el[0],
|
||
name: this.name + '-drop',
|
||
data: { item: it, btn }
|
||
}))
|
||
.hide(hideDrop(it.id, btn))
|
||
}
|
||
if (['menu', 'menu-radio', 'menu-check'].includes(it.type)) {
|
||
let menuType = 'normal'
|
||
if (it.type == 'menu-radio') {
|
||
menuType = 'radio'
|
||
items.forEach((item) => {
|
||
if (it.selected == item.id) item.checked = true; else item.checked = false
|
||
})
|
||
}
|
||
if (it.type == 'menu-check') {
|
||
menuType = 'check'
|
||
items.forEach((item) => {
|
||
if (Array.isArray(it.selected) && it.selected.includes(item.id)) item.checked = true; else item.checked = false
|
||
})
|
||
}
|
||
w2menu.show(w2utils.extend({
|
||
items,
|
||
}, it.overlay, {
|
||
type: menuType,
|
||
name : this.name + '-drop',
|
||
anchor: el[0],
|
||
data: { item: it, btn }
|
||
}))
|
||
.hide(hideDrop(it.id, btn))
|
||
.remove(event => {
|
||
this.menuClick({ name: this.name, remove: true, item: it, subItem: event.detail.item,
|
||
originalEvent: event })
|
||
})
|
||
.select(event => {
|
||
this.menuClick({ name: this.name, item: it, subItem: event.detail.item,
|
||
originalEvent: event })
|
||
})
|
||
}
|
||
if (['color', 'text-color'].includes(it.type)) {
|
||
w2color.show(w2utils.extend({
|
||
color: it.color
|
||
}, it.overlay, {
|
||
anchor: el[0],
|
||
name: this.name + '-drop',
|
||
data: { item: it, btn }
|
||
}))
|
||
.hide(hideDrop(it.id, btn))
|
||
.select(event => {
|
||
if (event.detail.color != null) {
|
||
this.colorClick({ name: this.name, item: it, color: event.detail.color })
|
||
}
|
||
})
|
||
}
|
||
}, 0)
|
||
}
|
||
}
|
||
if (['check', 'menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(it.type)) {
|
||
it.checked = !it.checked
|
||
if (it.checked) {
|
||
query(this.box).find(btn).addClass('checked')
|
||
} else {
|
||
query(this.box).find(btn).removeClass('checked')
|
||
}
|
||
}
|
||
// route processing
|
||
if (it.route) {
|
||
let route = String('/'+ it.route).replace(/\/{2,}/g, '/')
|
||
let info = w2utils.parseRoute(route)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
setTimeout(() => { window.location.hash = route }, 1)
|
||
}
|
||
// need to refresh toolbar as it might be dynamic
|
||
this.tooltipShow(id)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
scroll(direction, line, instant) {
|
||
return new Promise((resolve, reject) => {
|
||
let scrollBox = query(this.box).find(`.w2ui-tb-line:nth-child(${line}) .w2ui-scroll-wrapper`)
|
||
let scrollLeft = scrollBox.get(0).scrollLeft
|
||
let right = scrollBox.find('.w2ui-tb-right').get(0)
|
||
let width1 = scrollBox.parent().get(0).getBoundingClientRect().width
|
||
let width2 = scrollLeft + parseInt(right.offsetLeft) + parseInt(right.clientWidth )
|
||
switch (direction) {
|
||
case 'left': {
|
||
scroll = scrollLeft - width1 + 50 // 35 is width of both button
|
||
if (scroll <= 0) scroll = 0
|
||
scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' })
|
||
break
|
||
}
|
||
case 'right': {
|
||
scroll = scrollLeft + width1 - 50 // 35 is width of both button
|
||
if (scroll >= width2 - width1) scroll = width2 - width1
|
||
scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' })
|
||
break
|
||
}
|
||
}
|
||
setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 500)
|
||
})
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// defaul action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find('.w2ui-scroll-wrapper .w2ui-tb-right').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-toolbar')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.box) return
|
||
if (!Array.isArray(this.right)) {
|
||
this.right = [this.right]
|
||
}
|
||
// render all buttons
|
||
let html = ''
|
||
let line = 0
|
||
for (let i = 0; i < this.items.length; i++) {
|
||
let it = this.items[i]
|
||
if (it == null) continue
|
||
if (it.id == null) it.id = 'item_' + i
|
||
if (it.caption != null) {
|
||
console.log('NOTICE: toolbar item.caption property is deprecated, please use item.text. Item -> ', it)
|
||
}
|
||
if (it.hint != null) {
|
||
console.log('NOTICE: toolbar item.hint property is deprecated, please use item.tooltip. Item -> ', it)
|
||
}
|
||
if (i === 0 || it.type == 'new-line') {
|
||
line++
|
||
html += `
|
||
<div class="w2ui-tb-line">
|
||
<div class="w2ui-scroll-wrapper w2ui-eaction" data-mousedown="resize">
|
||
<div class="w2ui-tb-right">${this.right[line-1] ?? ''}</div>
|
||
</div>
|
||
<div class="w2ui-scroll-left w2ui-eaction" data-click='["scroll", "left", "${line}"]'></div>
|
||
<div class="w2ui-scroll-right w2ui-eaction" data-click='["scroll", "right", "${line}"]'></div>
|
||
</div>
|
||
`
|
||
}
|
||
it.line = line
|
||
}
|
||
query(this.box)
|
||
.attr('name', this.name)
|
||
.addClass('w2ui-reset w2ui-toolbar')
|
||
.html(html)
|
||
if (query(this.box).length > 0) {
|
||
query(this.box)[0].style.cssText += this.style
|
||
}
|
||
w2utils.bindEvents(query(this.box).find('.w2ui-tb-line .w2ui-eaction'), this)
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
// refresh all
|
||
this.refresh()
|
||
this.resize()
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
refresh(id) {
|
||
let time = Date.now()
|
||
// event before
|
||
let edata = this.trigger('refresh', { target: (id != null ? id : this.name), item: this.get(id) })
|
||
if (edata.isCancelled === true) return
|
||
let edata2
|
||
// refresh all
|
||
if (id == null) {
|
||
for (let i = 0; i < this.items.length; i++) {
|
||
let it1 = this.items[i]
|
||
if (it1.id == null) it1.id = 'item_' + i
|
||
this.refresh(it1.id)
|
||
}
|
||
return
|
||
}
|
||
// create or refresh only one item
|
||
let it = this.get(id)
|
||
if (it == null) return false
|
||
if (typeof it.onRefresh == 'function') {
|
||
edata2 = this.trigger('refresh', { target: id, item: it, object: it })
|
||
if (edata2.isCancelled === true) return
|
||
}
|
||
let selector = `#tb_${this.name}_item_${w2utils.escapeId(it.id)}`
|
||
let btn = query(this.box).find(selector)
|
||
let html = this.getItemHTML(it)
|
||
// hide tooltip
|
||
this.tooltipHide(id)
|
||
// if there is a spacer, then right HTML is not 100%
|
||
if (it.type == 'spacer') {
|
||
query(this.box).find(`.w2ui-tb-line:nth-child(${it.line}`).find('.w2ui-tb-right').css('width', 'auto')
|
||
}
|
||
if (btn.length === 0) {
|
||
let next = parseInt(this.get(id, true)) + 1
|
||
let $next = query(this.box).find(`#tb_${this.name}_item_${w2utils.escapeId(this.items[next] ? this.items[next].id : '')}`)
|
||
if ($next.length == 0) {
|
||
$next = query(this.box).find(`.w2ui-tb-line:nth-child(${it.line}`).find('.w2ui-tb-right').before(html)
|
||
} else {
|
||
$next.after(html)
|
||
}
|
||
w2utils.bindEvents(query(this.box).find(selector), this)
|
||
} else {
|
||
// refresh
|
||
query(this.box).find(selector).replace(query.html(html))
|
||
let newBtn = query(this.box).find(selector).get(0)
|
||
w2utils.bindEvents(newBtn, this)
|
||
// update overlay's anchor if changed
|
||
let overlays = w2tooltip.get(true)
|
||
Object.keys(overlays).forEach(key => {
|
||
if (overlays[key].anchor == btn.get(0)) {
|
||
overlays[key].anchor = newBtn
|
||
}
|
||
})
|
||
}
|
||
if (['menu', 'menu-radio', 'menu-check'].includes(it.type) && it.checked) {
|
||
// check selected items
|
||
let selected = Array.isArray(it.selected) ? it.selected : [it.selected]
|
||
it.items.forEach((item) => {
|
||
if (selected.includes(item.id)) item.checked = true; else item.checked = false
|
||
})
|
||
w2menu.update(this.name + '-drop', it.items)
|
||
}
|
||
// event after
|
||
if (typeof it.onRefresh == 'function') {
|
||
edata2.finish()
|
||
}
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
resize() {
|
||
let time = Date.now()
|
||
// event before
|
||
let edata = this.trigger('resize', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
query(this.box).find('.w2ui-tb-line').each(el => {
|
||
// show hide overflow buttons
|
||
let box = query(el)
|
||
box.find('.w2ui-scroll-left, .w2ui-scroll-right').hide()
|
||
let scrollBox = box.find('.w2ui-scroll-wrapper').get(0)
|
||
let $right = box.find('.w2ui-tb-right')
|
||
let boxWidth = box.get(0).getBoundingClientRect().width
|
||
let itemsWidth = ($right.length > 0 ? $right[0].offsetLeft + $right[0].clientWidth : 0)
|
||
if (boxWidth < itemsWidth) {
|
||
// we have overflown content
|
||
if (scrollBox.scrollLeft > 0) {
|
||
box.find('.w2ui-scroll-left').show()
|
||
}
|
||
if (boxWidth < itemsWidth - scrollBox.scrollLeft) {
|
||
box.find('.w2ui-scroll-right').show()
|
||
}
|
||
}
|
||
})
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// clean up
|
||
if (query(this.box).find('.w2ui-scroll-wrapper .w2ui-tb-right').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-toolbar')
|
||
.html('')
|
||
}
|
||
query(this.box).html('')
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
// ========================================
|
||
// --- Internal Functions
|
||
getItemHTML(item) {
|
||
let html = ''
|
||
if (item.caption != null && item.text == null) item.text = item.caption // for backward compatibility
|
||
if (item.text == null) item.text = ''
|
||
if (item.tooltip == null && item.hint != null) item.tooltip = item.hint // for backward compatibility
|
||
if (item.tooltip == null) item.tooltip = ''
|
||
if (typeof item.get !== 'function' && (Array.isArray(item.items) || typeof item.items == 'function')) {
|
||
item.get = function get(id) { // need scope, cannot be arrow func
|
||
let tmp = item.items
|
||
if (typeof tmp == 'function') tmp = item.items(item)
|
||
return tmp.find(it => it.id == id ? true : false)
|
||
}
|
||
}
|
||
let icon = ''
|
||
let text = (typeof item.text == 'function' ? item.text.call(this, item) : item.text)
|
||
if (item.icon) {
|
||
icon = item.icon
|
||
if (typeof item.icon == 'function') {
|
||
icon = item.icon.call(this, item)
|
||
}
|
||
if (String(icon).slice(0, 1) !== '<') {
|
||
icon = `<span class="${icon}"></span>`
|
||
}
|
||
icon = `<div class="w2ui-tb-icon">${icon}</div>`
|
||
}
|
||
let classes = ['w2ui-tb-button']
|
||
if (item.checked) classes.push('checked')
|
||
if (item.disabled) classes.push('disabled')
|
||
if (item.hidden) classes.push('hidden')
|
||
if (!icon) classes.push('no-icon')
|
||
switch (item.type) {
|
||
case 'color':
|
||
case 'text-color':
|
||
if (typeof item.color == 'string') {
|
||
if (item.color.slice(0, 1) == '#') item.color = item.color.slice(1)
|
||
if ([3, 6, 8].includes(item.color.length)) item.color = '#' + item.color
|
||
}
|
||
if (item.type == 'color') {
|
||
text = `<span class="w2ui-tb-color-box" style="background-color: ${(item.color != null ? item.color : '#fff')}"></span>
|
||
${(item.text ? `<div style="margin-left: 17px;">${w2utils.lang(item.text)}</div>` : '')}`
|
||
}
|
||
if (item.type == 'text-color') {
|
||
text = '<span style="color: '+ (item.color != null ? item.color : '#444') +';">'+
|
||
(item.text ? w2utils.lang(item.text) : '<b>Aa</b>') +
|
||
'</span>'
|
||
}
|
||
case 'menu':
|
||
case 'menu-check':
|
||
case 'menu-radio':
|
||
case 'button':
|
||
case 'check':
|
||
case 'radio':
|
||
case 'drop': {
|
||
let arrow = (item.arrow === true
|
||
|| (item.arrow !== false && ['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(item.type)))
|
||
html = `
|
||
<div id="tb_${this.name}_item_${item.id}" style="${(item.hidden ? 'display: none' : '')}"
|
||
class="${classes.join(' ')} ${(item.class ? item.class : '')}"
|
||
${!item.disabled
|
||
? `data-click='["click","${item.id}"]'
|
||
data-mouseenter='["mouseAction", "event", "this", "Enter", "${item.id}"]'
|
||
data-mouseleave='["mouseAction", "event", "this", "Leave", "${item.id}"]'
|
||
data-mousedown='["mouseAction", "event", "this", "Down", "${item.id}"]'
|
||
data-mouseup='["mouseAction", "event", "this", "Up", "${item.id}"]'`
|
||
: ''}
|
||
>
|
||
${ icon }
|
||
${ text != ''
|
||
? `<div class="w2ui-tb-text" style="${(item.style ? item.style : '')}">
|
||
${ w2utils.lang(text) }
|
||
${ item.count != null
|
||
? w2utils.stripSpaces(`<span class="w2ui-tb-count">
|
||
<span class="${this.last.badge[item.id] ? this.last.badge[item.id].className ?? '' : ''}"
|
||
style="${this.last.badge[item.id] ? this.last.badge[item.id].style ?? '' : ''}"
|
||
>${item.count}</span>
|
||
</span>`)
|
||
: ''
|
||
}
|
||
${ arrow
|
||
? '<span class="w2ui-tb-down"><span></span></span>'
|
||
: ''
|
||
}
|
||
</div>`
|
||
: ''}
|
||
</div>
|
||
`
|
||
break
|
||
}
|
||
case 'break':
|
||
html = `<div id="tb_${this.name}_item_${item.id}" class="w2ui-tb-break"
|
||
style="${(item.hidden ? 'display: none' : '')}; ${(item.style ? item.style : '')}">
|
||
 
|
||
</div>`
|
||
break
|
||
case 'spacer':
|
||
html = `<div id="tb_${this.name}_item_${item.id}" class="w2ui-tb-spacer"
|
||
style="${(item.hidden ? 'display: none' : '')}; ${(item.style ? item.style : '')}">
|
||
</div>`
|
||
break
|
||
case 'html':
|
||
html = `<div id="tb_${this.name}_item_${item.id}" class="w2ui-tb-html ${classes.join(' ')}"
|
||
style="${(item.hidden ? 'display: none' : '')}; ${(item.style ? item.style : '')}">
|
||
${(typeof item.html == 'function' ? item.html.call(this, item) : item.html)}
|
||
</div>`
|
||
break
|
||
}
|
||
return html
|
||
}
|
||
tooltipShow(id) {
|
||
if (this.tooltip == null) return
|
||
let el = query(this.box).find('#tb_'+ this.name + '_item_'+ w2utils.escapeId(id)).get(0)
|
||
let item = this.get(id)
|
||
let pos = this.tooltip
|
||
let txt = item.tooltip
|
||
if (typeof txt == 'function') txt = txt.call(this, item)
|
||
// not for opened drop downs
|
||
if (['menu', 'menu-radio', 'menu-check', 'drop', 'color', 'text-color'].includes(item.type)
|
||
&& item.checked == true) {
|
||
return
|
||
}
|
||
w2tooltip.show({
|
||
anchor: el,
|
||
name: this.name + '-tooltip',
|
||
html: txt,
|
||
position: pos
|
||
})
|
||
return
|
||
}
|
||
tooltipHide(id) {
|
||
if (this.tooltip == null) return
|
||
w2tooltip.hide(this.name + '-tooltip')
|
||
}
|
||
menuClick(event) {
|
||
if (event.item && !event.item.disabled) {
|
||
// event before
|
||
let edata = this.trigger((event.remove !== true ? 'click' : 'remove'), {
|
||
target: event.item.id + ':' + event.subItem.id, item: event.item,
|
||
subItem: event.subItem, originalEvent: event.originalEvent
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// route processing
|
||
let it = event.subItem
|
||
let item = this.get(event.item.id)
|
||
let items = item.items
|
||
if (typeof items == 'function') items = item.items()
|
||
if (item.type == 'menu') {
|
||
item.selected = it.id
|
||
}
|
||
if (item.type == 'menu-radio') {
|
||
item.selected = it.id
|
||
if (Array.isArray(items)) {
|
||
items.forEach((item) => {
|
||
if (item.checked === true) delete item.checked
|
||
if (Array.isArray(item.items)) {
|
||
item.items.forEach((item) => {
|
||
if (item.checked === true) delete item.checked
|
||
})
|
||
}
|
||
})
|
||
}
|
||
it.checked = true
|
||
}
|
||
if (item.type == 'menu-check') {
|
||
if (!Array.isArray(item.selected)) item.selected = []
|
||
if (it.group == null) {
|
||
let ind = item.selected.indexOf(it.id)
|
||
if (ind == -1) {
|
||
item.selected.push(it.id)
|
||
it.checked = true
|
||
} else {
|
||
item.selected.splice(ind, 1)
|
||
it.checked = false
|
||
}
|
||
} else if (it.group === false) {
|
||
// if group is false, then it is not part of checkboxes
|
||
} else {
|
||
let unchecked = []
|
||
let ind = item.selected.indexOf(it.id)
|
||
let checkNested = (items) => {
|
||
items.forEach((sub) => {
|
||
if (sub.group === it.group) {
|
||
let ind = item.selected.indexOf(sub.id)
|
||
if (ind != -1) {
|
||
if (sub.id != it.id) unchecked.push(sub.id)
|
||
item.selected.splice(ind, 1)
|
||
}
|
||
}
|
||
if (Array.isArray(sub.items)) checkNested(sub.items)
|
||
})
|
||
}
|
||
checkNested(items)
|
||
if (ind == -1) {
|
||
item.selected.push(it.id)
|
||
it.checked = true
|
||
}
|
||
}
|
||
}
|
||
if (typeof it.route == 'string') {
|
||
let route = it.route !== '' ? String('/'+ it.route).replace(/\/{2,}/g, '/') : ''
|
||
let info = w2utils.parseRoute(route)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (this.routeData[info.keys[k].name] == null) continue
|
||
route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
setTimeout(() => { window.location.hash = route }, 1)
|
||
}
|
||
this.refresh(event.item.id)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
colorClick(event) {
|
||
let obj = this
|
||
if (event.item && !event.item.disabled) {
|
||
// event before
|
||
let edata = this.trigger('click', {
|
||
target: event.item.id, item: event.item,
|
||
color: event.color, final: event.final, originalEvent: event.originalEvent
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
event.item.color = event.color
|
||
obj.refresh(event.item.id)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
mouseAction(event, target, action, id) {
|
||
let btn = this.get(id)
|
||
let edata = this.trigger('mouse' + action, { target: id, item: btn, object: btn, originalEvent: event })
|
||
if (edata.isCancelled === true || btn.disabled || btn.hidden) return
|
||
switch (action) {
|
||
case 'Enter':
|
||
query(target).addClass('over')
|
||
this.tooltipShow(id)
|
||
break
|
||
case 'Leave':
|
||
query(target).removeClass('over down')
|
||
this.tooltipHide(id)
|
||
break
|
||
case 'Down':
|
||
query(target).addClass('down')
|
||
break
|
||
case 'Up':
|
||
query(target).removeClass('down')
|
||
break
|
||
}
|
||
edata.finish()
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2menu
|
||
*
|
||
* == TODO ==
|
||
* - dbl click should be like it is in grid (with timer not HTML dbl click event)
|
||
* - node.style is misleading - should be there to apply color for example
|
||
* - node.plus - is not working
|
||
*
|
||
* == 2.0 changes
|
||
* - remove jQuery dependency
|
||
* - deprecarted obj.img, node.img
|
||
* - CSP - fixed inline events
|
||
* - observeResize for the box
|
||
* - handleTooltip and handle.tooltip - text/function
|
||
* - added onMouseEntter, onMouseLeave events
|
||
*/
|
||
|
||
class w2sidebar extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.name = null
|
||
this.box = null
|
||
this.sidebar = null
|
||
this.parent = null
|
||
this.nodes = [] // Sidebar child nodes
|
||
this.menu = []
|
||
this.routeData = {} // data for dynamic routes
|
||
this.selected = null // current selected node (readonly)
|
||
this.icon = null
|
||
this.style = ''
|
||
this.topHTML = ''
|
||
this.bottomHTML = ''
|
||
this.flatButton = false
|
||
this.keyboard = true
|
||
this.flat = false
|
||
this.hasFocus = false
|
||
this.levelPadding = 12
|
||
this.skipRefresh = false
|
||
this.tabIndex = null // will only be set if > 0 and not null
|
||
this.handle = { size: 0, style: '', html: '', tooltip: '' },
|
||
this.onClick = null // Fire when user click on Node Text
|
||
this.onDblClick = null // Fire when user dbl clicks
|
||
this.onMouseEnter = null // mouse enter/leave over an item
|
||
this.onMouseLeave = null
|
||
this.onContextMenu = null
|
||
this.onMenuClick = null // when context menu item selected
|
||
this.onExpand = null // Fire when node expands
|
||
this.onCollapse = null // Fire when node collapses
|
||
this.onKeydown = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.onFocus = null
|
||
this.onBlur = null
|
||
this.onFlat = null
|
||
this.node_template = {
|
||
id: null,
|
||
text: '',
|
||
order: null,
|
||
count: null,
|
||
icon: null,
|
||
nodes: [],
|
||
style: '', // additional style for subitems
|
||
route: null,
|
||
selected: false,
|
||
expanded: false,
|
||
hidden: false,
|
||
disabled: false,
|
||
group: false, // if true, it will build as a group
|
||
groupShowHide: true,
|
||
collapsible: false,
|
||
plus: false, // if true, plus will be shown even if there is no sub nodes
|
||
// events
|
||
onClick: null,
|
||
onDblClick: null,
|
||
onContextMenu: null,
|
||
onExpand: null,
|
||
onCollapse: null,
|
||
// internal
|
||
parent: null, // node object
|
||
sidebar: null
|
||
}
|
||
this.last = {
|
||
badge: {}
|
||
}
|
||
let nodes = options.nodes
|
||
delete options.nodes
|
||
// mix in options
|
||
Object.assign(this, options)
|
||
// add item via method to makes sure item_template is applied
|
||
if (Array.isArray(nodes)) this.add(nodes)
|
||
// need to reassign back to keep it in config
|
||
options.nodes = nodes
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
}
|
||
add(parent, nodes) {
|
||
if (arguments.length == 1) {
|
||
// need to be in reverse order
|
||
nodes = arguments[0]
|
||
parent = this
|
||
}
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent == null || parent == '') parent = this
|
||
return this.insert(parent, null, nodes)
|
||
}
|
||
insert(parent, before, nodes) {
|
||
let txt, ind, tmp, node, nd
|
||
if (arguments.length == 2 && typeof parent == 'string') {
|
||
// need to be in reverse order
|
||
nodes = arguments[1]
|
||
before = arguments[0]
|
||
if (before != null) {
|
||
ind = this.get(before)
|
||
if (ind == null) {
|
||
if (!Array.isArray(nodes)) nodes = [nodes]
|
||
if (nodes[0].caption != null && nodes[0].text == null) {
|
||
console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text. Node -> ', nodes[0])
|
||
nodes[0].text = nodes[0].caption
|
||
}
|
||
txt = nodes[0].text
|
||
console.log('ERROR: Cannot insert node "'+ txt +'" because cannot find node "'+ before +'" to insert before.')
|
||
return null
|
||
}
|
||
parent = this.get(before).parent
|
||
} else {
|
||
parent = this
|
||
}
|
||
}
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent == null || parent == '') parent = this
|
||
if (!Array.isArray(nodes)) nodes = [nodes]
|
||
for (let o = 0; o < nodes.length; o++) {
|
||
node = nodes[o]
|
||
if (node.caption != null && node.text == null) {
|
||
console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text')
|
||
node.text = node.caption
|
||
}
|
||
if (typeof node.id == null) {
|
||
txt = node.text
|
||
console.log('ERROR: Cannot insert node "'+ txt +'" because it has no id.')
|
||
continue
|
||
}
|
||
if (this.get(this, node.id) != null) {
|
||
console.log('ERROR: Cannot insert node with id='+ node.id +' (text: '+ node.text + ') because another node with the same id already exists.')
|
||
continue
|
||
}
|
||
tmp = Object.assign({}, this.node_template, node)
|
||
tmp.sidebar = this
|
||
tmp.parent = parent
|
||
nd = tmp.nodes || []
|
||
tmp.nodes = [] // very important to re-init empty nodes array
|
||
if (before == null) { // append to the end
|
||
parent.nodes.push(tmp)
|
||
} else {
|
||
ind = this.get(parent, before, true)
|
||
if (ind == null) {
|
||
console.log('ERROR: Cannot insert node "'+ node.text +'" because cannot find node "'+ before +'" to insert before.')
|
||
return null
|
||
}
|
||
parent.nodes.splice(ind, 0, tmp)
|
||
}
|
||
if (nd.length > 0) {
|
||
this.insert(tmp, null, nd)
|
||
}
|
||
}
|
||
if (!this.skipRefresh) this.refresh(parent.id)
|
||
return tmp
|
||
}
|
||
remove() { // multiple arguments
|
||
let effected = 0
|
||
let node
|
||
Array.from(arguments).forEach(arg => {
|
||
node = this.get(arg)
|
||
if (node == null) return
|
||
if (this.selected != null && this.selected === node.id) {
|
||
this.selected = null
|
||
}
|
||
let ind = this.get(node.parent, arg, true)
|
||
if (ind == null) return
|
||
if (node.parent.nodes[ind].selected) node.sidebar.unselect(node.id)
|
||
node.parent.nodes.splice(ind, 1)
|
||
node.parent.collapsible = node.parent.nodes.length > 0
|
||
effected++
|
||
})
|
||
if (!this.skipRefresh) {
|
||
if (effected > 0 && arguments.length == 1) this.refresh(node.parent.id); else this.refresh()
|
||
}
|
||
return effected
|
||
}
|
||
set(parent, id, node) {
|
||
if (arguments.length == 2) {
|
||
// need to be in reverse order
|
||
node = id
|
||
id = parent
|
||
parent = this
|
||
}
|
||
// searches all nested nodes
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent.nodes == null) return null
|
||
for (let i = 0; i < parent.nodes.length; i++) {
|
||
if (parent.nodes[i].id === id) {
|
||
// see if quick update is possible
|
||
let res = this.update(id, node)
|
||
if (Object.keys(res).length != 0) {
|
||
// make sure nodes inserted correctly
|
||
let nodes = node.nodes
|
||
w2utils.extend(parent.nodes[i], node, { nodes: [] })
|
||
if (nodes != null) {
|
||
this.add(parent.nodes[i], nodes)
|
||
}
|
||
if (!this.skipRefresh) this.refresh(id)
|
||
}
|
||
return true
|
||
} else {
|
||
let rv = this.set(parent.nodes[i], id, node)
|
||
if (rv) return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
get(parent, id, returnIndex) { // can be just called get(id) or get(id, true)
|
||
if (arguments.length === 0) {
|
||
let all = []
|
||
let tmp = this.find({})
|
||
for (let t = 0; t < tmp.length; t++) {
|
||
if (tmp[t].id != null) all.push(tmp[t].id)
|
||
}
|
||
return all
|
||
} else {
|
||
if (arguments.length == 1 || (arguments.length == 2 && id === true) ) {
|
||
// need to be in reverse order
|
||
returnIndex = id
|
||
id = parent
|
||
parent = this
|
||
}
|
||
// searches all nested nodes
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent.nodes == null) return null
|
||
for (let i = 0; i < parent.nodes.length; i++) {
|
||
if (parent.nodes[i].id == id) {
|
||
if (returnIndex === true) return i; else return parent.nodes[i]
|
||
} else {
|
||
let rv = this.get(parent.nodes[i], id, returnIndex)
|
||
if (rv || rv === 0) return rv
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
}
|
||
setCount(id, count, className, style) {
|
||
let btn = query(this.box).find(`#node_${w2utils.escapeId(id)} .w2ui-node-count`)
|
||
if (btn.length > 0) {
|
||
btn.removeClass()
|
||
.addClass(`w2ui-node-count ${className || ''}`)
|
||
.text(count)
|
||
.get(0).style.cssText = style || ''
|
||
this.last.badge[id] = {
|
||
className: className || '',
|
||
style: style || ''
|
||
}
|
||
let item = this.get(id)
|
||
item.count = count
|
||
} else {
|
||
this.set(id, { count: count })
|
||
this.setCount(...arguments) // to update styles
|
||
}
|
||
}
|
||
find(parent, params, results) { // can be just called find({ selected: true })
|
||
// TODO: rewrite with this.each()
|
||
if (arguments.length == 1) {
|
||
// need to be in reverse order
|
||
params = parent
|
||
parent = this
|
||
}
|
||
if (!results) results = []
|
||
// searches all nested nodes
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent.nodes == null) return results
|
||
for (let i = 0; i < parent.nodes.length; i++) {
|
||
let match = true
|
||
for (let prop in params) { // params is an object
|
||
if (parent.nodes[i][prop] != params[prop]) match = false
|
||
}
|
||
if (match) results.push(parent.nodes[i])
|
||
if (parent.nodes[i].nodes.length > 0) results = this.find(parent.nodes[i], params, results)
|
||
}
|
||
return results
|
||
}
|
||
sort(options, nodes) {
|
||
// default options
|
||
if (!options || typeof options != 'object') options = {}
|
||
if (options.foldersFirst == null) options.foldersFirst = true
|
||
if (options.caseSensitive == null) options.caseSensitive = false
|
||
if (options.reverse == null) options.reverse = false
|
||
if (nodes == null) {
|
||
nodes = this.nodes
|
||
}
|
||
nodes.sort((a, b) => {
|
||
// folders first
|
||
let isAfolder = (a.nodes && a.nodes.length > 0)
|
||
let isBfolder = (b.nodes && b.nodes.length > 0)
|
||
// both folder or both not folders
|
||
if (options.foldersFirst === false || (!isAfolder && !isBfolder) || (isAfolder && isBfolder)) {
|
||
let aText = a.text
|
||
let bText = b.text
|
||
if (!options.caseSensitive) {
|
||
aText = aText.toLowerCase()
|
||
bText = bText.toLowerCase()
|
||
}
|
||
if (a.order != null) aText = a.order
|
||
if (b.order != null) bText = b.order
|
||
let cmp = w2utils.naturalCompare(aText, bText)
|
||
return (cmp === 1 || cmp === -1) & options.reverse ? -cmp : cmp
|
||
}
|
||
if (isAfolder && !isBfolder) {
|
||
return !options.reverse ? -1 : 1
|
||
}
|
||
if (!isAfolder && isBfolder) {
|
||
return !options.reverse ? 1 : -1
|
||
}
|
||
})
|
||
nodes.forEach(node => {
|
||
if (node.nodes && node.nodes.length > 0) {
|
||
this.sort(options, node.nodes)
|
||
}
|
||
})
|
||
}
|
||
each(fn, nodes) {
|
||
if (nodes == null) nodes = this.nodes
|
||
nodes.forEach((node) => {
|
||
fn.call(this, node)
|
||
if (node.nodes && node.nodes.length > 0) {
|
||
this.each(fn, node.nodes)
|
||
}
|
||
})
|
||
}
|
||
search(str) {
|
||
let count = 0
|
||
let str2 = str.toLowerCase()
|
||
this.each((node) => {
|
||
if (node.text.toLowerCase().indexOf(str2) === -1) {
|
||
node.hidden = true
|
||
} else {
|
||
count++
|
||
showParents(node)
|
||
node.hidden = false
|
||
}
|
||
})
|
||
this.refresh()
|
||
return count
|
||
function showParents(node) {
|
||
if (node.parent) {
|
||
node.parent.hidden = false
|
||
showParents(node.parent)
|
||
}
|
||
}
|
||
}
|
||
show() { // multiple arguments
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let node = this.get(it)
|
||
if (node == null || node.hidden === false) return
|
||
node.hidden = false
|
||
effected.push(node.id)
|
||
})
|
||
if (effected.length > 0) {
|
||
if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh()
|
||
}
|
||
return effected
|
||
}
|
||
hide() { // multiple arguments
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let node = this.get(it)
|
||
if (node == null || node.hidden === true) return
|
||
node.hidden = true
|
||
effected.push(node.id)
|
||
})
|
||
if (effected.length > 0) {
|
||
if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh()
|
||
}
|
||
return effected
|
||
}
|
||
enable() { // multiple arguments
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let node = this.get(it)
|
||
if (node == null || node.disabled === false) return
|
||
node.disabled = false
|
||
effected.push(node.id)
|
||
})
|
||
if (effected.length > 0) {
|
||
if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh()
|
||
}
|
||
return effected
|
||
}
|
||
disable() { // multiple arguments
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let node = this.get(it)
|
||
if (node == null || node.disabled === true) return
|
||
node.disabled = true
|
||
if (node.selected) this.unselect(node.id)
|
||
effected.push(node.id)
|
||
})
|
||
if (effected.length > 0) {
|
||
if (arguments.length == 1) this.refresh(arguments[0]); else this.refresh()
|
||
}
|
||
return effected
|
||
}
|
||
select(id) {
|
||
let new_node = this.get(id)
|
||
if (!new_node) return false
|
||
if (this.selected == id && new_node.selected) return false
|
||
this.unselect(this.selected)
|
||
let $el = query(this.box).find('#node_'+ w2utils.escapeId(id))
|
||
$el.addClass('w2ui-selected')
|
||
.find('.w2ui-icon')
|
||
.addClass('w2ui-icon-selected')
|
||
if ($el.length > 0) {
|
||
if (!this.inView(id)) this.scrollIntoView(id)
|
||
}
|
||
new_node.selected = true
|
||
this.selected = id
|
||
return true
|
||
}
|
||
unselect(id) {
|
||
// if no arguments provided, unselect selected node
|
||
if (arguments.length === 0) {
|
||
id = this.selected
|
||
}
|
||
let current = this.get(id)
|
||
if (!current) return false
|
||
current.selected = false
|
||
query(this.box).find('#node_'+ w2utils.escapeId(id))
|
||
.removeClass('w2ui-selected')
|
||
.find('.w2ui-icon').removeClass('w2ui-icon-selected')
|
||
if (this.selected == id) this.selected = null
|
||
return true
|
||
}
|
||
toggle(id) {
|
||
let nd = this.get(id)
|
||
if (nd == null) return false
|
||
if (nd.plus) {
|
||
this.set(id, { plus: false })
|
||
this.expand(id)
|
||
this.refresh(id)
|
||
return
|
||
}
|
||
if (nd.nodes.length === 0) return false
|
||
if (!nd.collapsible) return false
|
||
if (this.get(id).expanded) return this.collapse(id); else return this.expand(id)
|
||
}
|
||
collapse(id) {
|
||
let self = this
|
||
let nd = this.get(id)
|
||
if (nd == null) return false
|
||
// event before
|
||
let edata = this.trigger('collapse', { target: id, object: nd })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
query(this.box).find('#node_'+ w2utils.escapeId(id) +'_sub').hide()
|
||
query(this.box).find('#node_'+ w2utils.escapeId(id) +' .w2ui-expanded')
|
||
.removeClass('w2ui-expanded')
|
||
.addClass('w2ui-collapsed')
|
||
nd.expanded = false
|
||
// event after
|
||
edata.finish()
|
||
setTimeout(() => { self.refresh(id) }, 0)
|
||
return true
|
||
}
|
||
expand(id) {
|
||
let self = this
|
||
let nd = this.get(id)
|
||
// event before
|
||
let edata = this.trigger('expand', { target: id, object: nd })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
query(this.box).find('#node_'+ w2utils.escapeId(id) +'_sub')
|
||
.show()
|
||
query(this.box).find('#node_'+ w2utils.escapeId(id) +' .w2ui-collapsed')
|
||
.removeClass('w2ui-collapsed')
|
||
.addClass('w2ui-expanded')
|
||
nd.expanded = true
|
||
// event after
|
||
edata.finish()
|
||
self.refresh(id)
|
||
return true
|
||
}
|
||
collapseAll(parent) {
|
||
if (parent == null) parent = this
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent.nodes == null) return false
|
||
for (let i = 0; i < parent.nodes.length; i++) {
|
||
if (parent.nodes[i].expanded === true) parent.nodes[i].expanded = false
|
||
if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.collapseAll(parent.nodes[i])
|
||
}
|
||
this.refresh(parent.id)
|
||
return true
|
||
}
|
||
expandAll(parent) {
|
||
if (parent == null) parent = this
|
||
if (typeof parent == 'string') parent = this.get(parent)
|
||
if (parent.nodes == null) return false
|
||
for (let i = 0; i < parent.nodes.length; i++) {
|
||
if (parent.nodes[i].expanded === false) parent.nodes[i].expanded = true
|
||
if (parent.nodes[i].nodes && parent.nodes[i].nodes.length > 0) this.expandAll(parent.nodes[i])
|
||
}
|
||
this.refresh(parent.id)
|
||
}
|
||
expandParents(id) {
|
||
let node = this.get(id)
|
||
if (node == null) return false
|
||
if (node.parent) {
|
||
if (!node.parent.expanded) {
|
||
node.parent.expanded = true
|
||
this.refresh(node.parent.id)
|
||
}
|
||
this.expandParents(node.parent.id)
|
||
}
|
||
return true
|
||
}
|
||
click(id, event) {
|
||
let obj = this
|
||
let nd = this.get(id)
|
||
if (nd == null) return
|
||
if (nd.disabled || nd.group) return // should click event if already selected
|
||
// unselect all previously
|
||
query(obj.box).find('.w2ui-node.w2ui-selected').each(el => {
|
||
let oldID = query(el).attr('id').replace('node_', '')
|
||
let oldNode = obj.get(oldID)
|
||
if (oldNode != null) oldNode.selected = false
|
||
query(el).removeClass('w2ui-selected').find('.w2ui-icon').removeClass('w2ui-icon-selected')
|
||
})
|
||
// select new one
|
||
let newNode = query(obj.box).find('#node_'+ w2utils.escapeId(id))
|
||
let oldNode = query(obj.box).find('#node_'+ w2utils.escapeId(obj.selected))
|
||
newNode.addClass('w2ui-selected').find('.w2ui-icon').addClass('w2ui-icon-selected')
|
||
// need timeout to allow rendering
|
||
setTimeout(() => {
|
||
// event before
|
||
let edata = obj.trigger('click', { target: id, originalEvent: event, node: nd, object: nd })
|
||
if (edata.isCancelled === true) {
|
||
// restore selection
|
||
newNode.removeClass('w2ui-selected').find('.w2ui-icon').removeClass('w2ui-icon-selected')
|
||
oldNode.addClass('w2ui-selected').find('.w2ui-icon').addClass('w2ui-icon-selected')
|
||
return
|
||
}
|
||
// default action
|
||
if (oldNode != null) oldNode.selected = false
|
||
obj.get(id).selected = true
|
||
obj.selected = id
|
||
// route processing
|
||
if (typeof nd.route == 'string') {
|
||
let route = nd.route !== '' ? String('/'+ nd.route).replace(/\/{2,}/g, '/') : ''
|
||
let info = w2utils.parseRoute(route)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (obj.routeData[info.keys[k].name] == null) continue
|
||
route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), obj.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
setTimeout(() => { window.location.hash = route }, 1)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}, 1)
|
||
}
|
||
focus(event) {
|
||
let self = this
|
||
// event before
|
||
let edata = this.trigger('focus', { target: this.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return false
|
||
// default behaviour
|
||
this.hasFocus = true
|
||
query(this.box).find('.w2ui-sidebar-body').addClass('w2ui-focus')
|
||
setTimeout(() => {
|
||
let input = query(self.box).find('#sidebar_'+ self.name + '_focus').get(0)
|
||
if (document.activeElement != input) input.focus()
|
||
}, 10)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
blur(event) {
|
||
// event before
|
||
let edata = this.trigger('blur', { target: this.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return false
|
||
// default behaviour
|
||
this.hasFocus = false
|
||
query(this.box).find('.w2ui-sidebar-body').removeClass('w2ui-focus')
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
keydown(event) {
|
||
let obj = this
|
||
let nd = obj.get(obj.selected)
|
||
if (obj.keyboard !== true) return
|
||
if (!nd) nd = obj.nodes[0]
|
||
// trigger event
|
||
let edata = obj.trigger('keydown', { target: obj.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default behaviour
|
||
if (event.keyCode == 13 || event.keyCode == 32) { // enter or space
|
||
if (nd.nodes.length > 0) obj.toggle(obj.selected)
|
||
}
|
||
if (event.keyCode == 37) { // left
|
||
if (nd.nodes.length > 0 && nd.expanded) {
|
||
obj.collapse(obj.selected)
|
||
} else {
|
||
selectNode(nd.parent)
|
||
if (!nd.parent.group) obj.collapse(nd.parent.id)
|
||
}
|
||
}
|
||
if (event.keyCode == 39) { // right
|
||
if ((nd.nodes.length > 0 || nd.plus) && !nd.expanded) obj.expand(obj.selected)
|
||
}
|
||
if (event.keyCode == 38) { // up
|
||
if (obj.get(obj.selected) == null) {
|
||
selectNode(this.nodes[0] || null)
|
||
} else {
|
||
selectNode(neighbor(nd, prev))
|
||
}
|
||
}
|
||
if (event.keyCode == 40) { // down
|
||
if (obj.get(obj.selected) == null) {
|
||
selectNode(this.nodes[0] || null)
|
||
} else {
|
||
selectNode(neighbor(nd, next))
|
||
}
|
||
}
|
||
// cancel event if needed
|
||
if ([13, 32, 37, 38, 39, 40].includes(event.keyCode)) {
|
||
if (event.preventDefault) event.preventDefault()
|
||
if (event.stopPropagation) event.stopPropagation()
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
function selectNode(node, event) {
|
||
if (node != null && !node.hidden && !node.disabled && !node.group) {
|
||
obj.click(node.id, event)
|
||
if (!obj.inView(node.id)) obj.scrollIntoView(node.id)
|
||
}
|
||
}
|
||
function neighbor(node, neighborFunc) {
|
||
node = neighborFunc(node)
|
||
while (node != null && (node.hidden || node.disabled)) {
|
||
if (node.group) break; else node = neighborFunc(node)
|
||
}
|
||
return node
|
||
}
|
||
function next(node, noSubs) {
|
||
if (node == null) return null
|
||
let parent = node.parent
|
||
let ind = obj.get(node.id, true)
|
||
let nextNode = null
|
||
// jump inside
|
||
if (node.expanded && node.nodes.length > 0 && noSubs !== true) {
|
||
let t = node.nodes[0]
|
||
if (t.hidden || t.disabled || t.group) nextNode = next(t); else nextNode = t
|
||
} else {
|
||
if (parent && ind + 1 < parent.nodes.length) {
|
||
nextNode = parent.nodes[ind + 1]
|
||
} else {
|
||
nextNode = next(parent, true) // jump to the parent
|
||
}
|
||
}
|
||
if (nextNode != null && (nextNode.hidden || nextNode.disabled || nextNode.group)) nextNode = next(nextNode)
|
||
return nextNode
|
||
}
|
||
function prev(node) {
|
||
if (node == null) return null
|
||
let parent = node.parent
|
||
let ind = obj.get(node.id, true)
|
||
let prevNode = (ind > 0) ? lastChild(parent.nodes[ind - 1]) : parent
|
||
if (prevNode != null && (prevNode.hidden || prevNode.disabled || prevNode.group)) prevNode = prev(prevNode)
|
||
return prevNode
|
||
}
|
||
function lastChild(node) {
|
||
if (node.expanded && node.nodes.length > 0) {
|
||
let t = node.nodes[node.nodes.length - 1]
|
||
if (t.hidden || t.disabled || t.group) return prev(t); else return lastChild(t)
|
||
}
|
||
return node
|
||
}
|
||
}
|
||
inView(id) {
|
||
let item = query(this.box).find('#node_'+ w2utils.escapeId(id)).get(0)
|
||
if (!item) {
|
||
return false
|
||
}
|
||
let div = query(this.box).find('.w2ui-sidebar-body').get(0)
|
||
if (item.offsetTop < div.scrollTop || (item.offsetTop + item.clientHeight > div.clientHeight + div.scrollTop)) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
scrollIntoView(id, instant) {
|
||
return new Promise((resolve, reject) => {
|
||
if (id == null) id = this.selected
|
||
let nd = this.get(id)
|
||
if (nd == null) return
|
||
let item = query(this.box).find('#node_'+ w2utils.escapeId(id)).get(0)
|
||
item.scrollIntoView({ block: 'center', inline: 'center', behavior: instant ? 'atuo' : 'smooth' })
|
||
setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 500)
|
||
})
|
||
}
|
||
dblClick(id, event) {
|
||
let nd = this.get(id)
|
||
// event before
|
||
let edata = this.trigger('dblClick', { target: id, originalEvent: event, object: nd })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
this.toggle(id)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
contextMenu(id, event) {
|
||
let nd = this.get(id)
|
||
if (id != this.selected) this.click(id)
|
||
// event before
|
||
let edata = this.trigger('contextMenu', { target: id, originalEvent: event, object: nd, allowOnDisabled: false })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (nd.disabled && !edata.allowOnDisabled) return
|
||
if (this.menu.length > 0) {
|
||
w2menu.show({
|
||
name: this.name + '_menu',
|
||
anchor: document.body,
|
||
items: this.menu,
|
||
originalEvent: event
|
||
})
|
||
.select(evt => {
|
||
this.menuClick(id, parseInt(evt.detail.index), event)
|
||
})
|
||
}
|
||
// prevent default context menu
|
||
if (event.preventDefault) event.preventDefault()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
menuClick(itemId, index, event) {
|
||
// event before
|
||
let edata = this.trigger('menuClick', { target: itemId, originalEvent: event, menuIndex: index, menuItem: this.menu[index] })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
// -- empty
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
goFlat() {
|
||
// event before
|
||
let edata = this.trigger('flat', { goFlat: !this.flat })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
this.flat = !this.flat
|
||
this.refresh()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
let obj = this
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find('.w2ui-sidebar-body').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-sidebar')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.box) return
|
||
query(this.box)
|
||
.attr('name', this.name)
|
||
.addClass('w2ui-reset w2ui-sidebar')
|
||
.html(`<div>
|
||
<div class="w2ui-sidebar-top"></div>
|
||
<input id="sidebar_${this.name}_focus" ${(this.tabIndex ? 'tabindex="' + this.tabIndex + '"' : '')}
|
||
style="position: absolute; top: 0; right: 0; width: 1px; z-index: -1; opacity: 0"
|
||
${(w2utils.isIOS ? 'readonly' : '')}/>
|
||
<div class="w2ui-sidebar-body"></div>
|
||
<div class="w2ui-sidebar-bottom"></div>
|
||
</div>`)
|
||
let rect = query(this.box).get(0).getBoundingClientRect()
|
||
query(this.box).find(':scope > div').css({
|
||
width : rect.width + 'px',
|
||
height : rect.height + 'px'
|
||
})
|
||
query(this.box).get(0).style.cssText += this.style
|
||
// focus
|
||
let kbd_timer
|
||
query(this.box).find('#sidebar_'+ this.name + '_focus')
|
||
.on('focus', function(event) {
|
||
clearTimeout(kbd_timer)
|
||
if (!obj.hasFocus) obj.focus(event)
|
||
})
|
||
.on('blur', function(event) {
|
||
kbd_timer = setTimeout(() => {
|
||
if (obj.hasFocus) { obj.blur(event) }
|
||
}, 100)
|
||
})
|
||
.on('keydown', function(event) {
|
||
if (event.keyCode != 9) { // not tab
|
||
w2ui[obj.name].keydown.call(w2ui[obj.name], event)
|
||
}
|
||
})
|
||
query(this.box).off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
// set focus to grid
|
||
setTimeout(() => {
|
||
// if input then do not focus
|
||
if (['INPUT', 'TEXTAREA', 'SELECT'].indexOf(event.target.tagName.toUpperCase()) == -1) {
|
||
let $input = query(obj.box).find('#sidebar_'+ obj.name + '_focus')
|
||
if (document.activeElement != $input.get(0)) {
|
||
$input.get(0).focus()
|
||
}
|
||
}
|
||
}, 1)
|
||
})
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
// event after
|
||
edata.finish()
|
||
// ---
|
||
this.refresh()
|
||
return Date.now() - time
|
||
}
|
||
update(id, options) {
|
||
// quick function to refresh just this item (not sub nodes)
|
||
// - icon, class, style, text, count
|
||
let nd = this.get(id)
|
||
let level
|
||
if (nd) {
|
||
let $el = query(this.box).find('#node_'+ w2utils.escapeId(nd.id))
|
||
if (nd.group) {
|
||
if (options.text) {
|
||
nd.text = options.text
|
||
$el.find('.w2ui-group-text').replace(typeof nd.text == 'function'
|
||
? nd.text.call(this, nd)
|
||
: '<span class="w2ui-group-text">'+ nd.text +'</span>')
|
||
delete options.text
|
||
}
|
||
if (options.class) {
|
||
nd.class = options.class
|
||
level = $el.data('level')
|
||
$el.get(0).className = 'w2ui-node-group w2ui-level-'+ level +(nd.class ? ' ' + nd.class : '')
|
||
delete options.class
|
||
}
|
||
if (options.style) {
|
||
nd.style = options.style
|
||
$el.get(0).nextElementSibling.style = nd.style +';'+ (!nd.hidden && nd.expanded ? '' : 'display: none;')
|
||
delete options.style
|
||
}
|
||
} else {
|
||
if (options.icon) {
|
||
let $icon = $el.find('.w2ui-node-image > span')
|
||
if ($icon.length > 0) {
|
||
nd.icon = options.icon
|
||
$icon[0].className = (typeof nd.icon == 'function' ? nd.icon.call(this, nd) : nd.icon)
|
||
delete options.icon
|
||
}
|
||
}
|
||
if (options.count) {
|
||
nd.count = options.count
|
||
$el.find('.w2ui-node-count').html(nd.count)
|
||
if ($el.find('.w2ui-node-count').length > 0) delete options.count
|
||
}
|
||
if (options.class && $el.length > 0) {
|
||
nd.class = options.class
|
||
level = $el.data('level')
|
||
$el[0].className = 'w2ui-node w2ui-level-'+ level + (nd.selected ? ' w2ui-selected' : '') + (nd.disabled ? ' w2ui-disabled' : '') + (nd.class ? ' ' + nd.class : '')
|
||
delete options.class
|
||
}
|
||
if (options.text) {
|
||
nd.text = options.text
|
||
$el.find('.w2ui-node-text').html(typeof nd.text == 'function' ? nd.text.call(this, nd) : nd.text)
|
||
delete options.text
|
||
}
|
||
if (options.style && $el.length > 0) {
|
||
let $txt = $el.find('.w2ui-node-text')
|
||
nd.style = options.style
|
||
$txt[0].style = nd.style
|
||
delete options.style
|
||
}
|
||
}
|
||
}
|
||
// return what was not set
|
||
return options
|
||
}
|
||
refresh(id, noBinding) {
|
||
if (this.box == null) return
|
||
let time = Date.now()
|
||
// event before
|
||
let edata = this.trigger('refresh', {
|
||
target: (id != null ? id : this.name),
|
||
nodeId: (id != null ? id : null),
|
||
fullRefresh: (id != null ? false : true)
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// adjust top and bottom
|
||
let flatHTML = ''
|
||
if (this.flatButton == true) {
|
||
flatHTML = `<div class="w2ui-flat w2ui-flat-${(this.flat ? 'right' : 'left')}"></div>`
|
||
}
|
||
if (id == null && (this.topHTML !== '' || flatHTML !== '')) {
|
||
query(this.box).find('.w2ui-sidebar-top').html(this.topHTML + flatHTML)
|
||
query(this.box).find('.w2ui-sidebar-body')
|
||
.css('top', query(this.box).find('.w2ui-sidebar-top').get(0)?.clientHeight + 'px')
|
||
query(this.box).find('.w2ui-flat')
|
||
.off('click')
|
||
.on('click', event => { this.goFlat() })
|
||
}
|
||
if (id != null && this.bottomHTML !== '') {
|
||
query(this.box).find('.w2ui-sidebar-bottom').html(this.bottomHTML)
|
||
query(this.box).find('.w2ui-sidebar-body')
|
||
.css('bottom', query(this.box).find('.w2ui-sidebar-bottom').get(0)?.clientHeight + 'px')
|
||
}
|
||
// default action
|
||
query(this.box).find(':scope > div').removeClass('w2ui-sidebar-flat').addClass(this.flat ? 'w2ui-sidebar-flat' : '').css({
|
||
width : query(this.box).get(0)?.clientWidth + 'px',
|
||
height: query(this.box).get(0)?.clientHeight + 'px'
|
||
})
|
||
// if no parent - reset nodes
|
||
if (this.nodes.length > 0 && this.nodes[0].parent == null) {
|
||
let tmp = this.nodes
|
||
this.nodes = []
|
||
this.add(this, tmp)
|
||
}
|
||
let obj = this
|
||
let node
|
||
let nodeSubId
|
||
if (id == null) {
|
||
node = this
|
||
nodeSubId = '.w2ui-sidebar-body'
|
||
} else {
|
||
node = this.get(id)
|
||
if (node == null) return
|
||
nodeSubId = '#node_'+ w2utils.escapeId(node.id) + '_sub'
|
||
}
|
||
let nodeId = '#node_'+ w2utils.escapeId(node.id)
|
||
let nodeHTML
|
||
if (node !== this) {
|
||
nodeHTML = getNodeHTML(node)
|
||
query(this.box).find(nodeId).before('<div id="sidebar_'+ this.name + '_tmp"></div>')
|
||
query(this.box).find(nodeId).remove()
|
||
query(this.box).find(nodeSubId).remove()
|
||
query(this.box).find('#sidebar_'+ this.name + '_tmp').before(nodeHTML)
|
||
query(this.box).find('#sidebar_'+ this.name + '_tmp').remove()
|
||
}
|
||
// remember scroll position
|
||
let div = query(this.box).find(':scope > div').get(0)
|
||
let scroll = {
|
||
top: div?.scrollTop,
|
||
left: div?.scrollLeft
|
||
}
|
||
// refresh sub nodes
|
||
query(this.box).find(nodeSubId).html('')
|
||
for (let i = 0; i < node.nodes.length; i++) {
|
||
let subNode = node.nodes[i]
|
||
nodeHTML = getNodeHTML(subNode)
|
||
query(this.box).find(nodeSubId).append(nodeHTML)
|
||
if (subNode.nodes.length !== 0) {
|
||
this.refresh(subNode.id, true)
|
||
} else {
|
||
// trigger event
|
||
let edata2 = this.trigger('refresh', { target: subNode.id })
|
||
if (edata2.isCancelled === true) return
|
||
// event after
|
||
edata2.finish()
|
||
}
|
||
}
|
||
// reset scroll
|
||
if (div) {
|
||
div.scrollTop = scroll.top
|
||
div.scrollLeft = scroll.left
|
||
}
|
||
// bind events
|
||
if (!noBinding) {
|
||
let els = query(this.box).find(`${nodeId}.w2ui-eaction, ${nodeSubId} .w2ui-eaction`)
|
||
w2utils.bindEvents(els, this)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
function getNodeHTML(nd) {
|
||
let html = ''
|
||
let icon = nd.icon
|
||
if (icon == null) icon = obj.icon
|
||
// -- find out level
|
||
let tmp = nd.parent
|
||
let level = 0
|
||
while (tmp && tmp.parent != null) {
|
||
// if (tmp.group) level--;
|
||
tmp = tmp.parent
|
||
level++
|
||
}
|
||
if (nd.caption != null && nd.text == null) nd.text = nd.caption
|
||
if (nd.caption != null) {
|
||
console.log('NOTICE: sidebar node.caption property is deprecated, please use node.text. Node -> ', nd)
|
||
nd.text = nd.caption
|
||
}
|
||
if (Array.isArray(nd.nodes) && nd.nodes.length > 0) nd.collapsible = true
|
||
if (nd.group) {
|
||
let text = w2utils.lang(typeof nd.text == 'function' ? nd.text.call(obj, nd) : nd.text)
|
||
if (String(text).substr(0, 5) != '<span') {
|
||
text = `<span class="w2ui-group-text">${text}</span>`
|
||
}
|
||
html = `
|
||
<div id="node_${nd.id}" data-level="${level}" style="${nd.hidden ? 'display: none' : ''}"
|
||
class="w2ui-node-group w2ui-level-${level} ${nd.class ? nd.class : ''} w2ui-eaction"
|
||
data-click="toggle|${nd.id}"
|
||
data-contextmenu="contextMenu|${nd.id}|event"
|
||
data-mouseenter="showPlus|this|inherit"
|
||
data-mouseleave="showPlus|this|transparent">
|
||
${nd.groupShowHide && nd.collapsible
|
||
? `<span>${!nd.hidden && nd.expanded ? w2utils.lang('Hide') : w2utils.lang('Show')}</span>`
|
||
: '<span></span>'
|
||
} ${text}
|
||
</div>
|
||
<div class="w2ui-node-sub" id="node_${nd.id}_sub" style="${nd.style}; ${!nd.hidden && nd.expanded ? '' : 'display: none;'}">
|
||
</div>`
|
||
if (obj.flat) {
|
||
html = `
|
||
<div class="w2ui-node-group" id="node_${nd.id}"><span> </span></div>
|
||
<div id="node_${nd.id}_sub" style="${nd.style}; ${!nd.hidden && nd.expanded ? '' : 'display: none;'}"></div>`
|
||
}
|
||
} else {
|
||
if (nd.selected && !nd.disabled) obj.selected = nd.id
|
||
tmp = ''
|
||
if (icon) {
|
||
tmp = `
|
||
<div class="w2ui-node-image">
|
||
<span class="${typeof icon == 'function' ? icon.call(obj, nd) : icon}"></span>
|
||
</div>`
|
||
}
|
||
let expand = ''
|
||
let counts = (nd.count != null
|
||
? `<div class="w2ui-node-count ${obj.last.badge[nd.id] ? obj.last.badge[nd.id].className || '' : ''}"
|
||
style="${obj.last.badge[nd.id] ? obj.last.badge[nd.id].style || '' : ''}">
|
||
${nd.count}
|
||
</div>`
|
||
: '')
|
||
if (nd.collapsible === true) {
|
||
expand = `<div class="w2ui-${nd.expanded ? 'expanded' : 'collapsed'}"><span></span></div>`
|
||
}
|
||
let text = w2utils.lang(typeof nd.text == 'function' ? nd.text.call(obj, nd) : nd.text)
|
||
// array with classes
|
||
let classes = ['w2ui-node', `w2ui-level-${level}`, 'w2ui-eaction']
|
||
if (nd.selected) classes.push('w2ui-selected')
|
||
if (nd.disabled) classes.push('w2ui-disabled')
|
||
if (nd.class) classes.push(nd.class)
|
||
html = `
|
||
<div id="node_${nd.id}" class="${classes.join(' ')}" data-level="${level}"
|
||
style="position: relative; ${nd.hidden ? 'display: none;' : ''}"
|
||
data-click="click|${nd.id}|event"
|
||
data-dblclick="dblClick|${nd.id}|event"
|
||
data-contextmenu="contextMenu|${nd.id}|event"
|
||
data-mouseEnter="mouseAction|Enter|this|${nd.id}|event"
|
||
data-mouseLeave="mouseAction|Leave|this|${nd.id}|event"
|
||
>
|
||
${obj.handle.html
|
||
? `<div class="w2ui-node-handle w2ui-eaction" style="width: ${obj.handle.size}px; ${obj.handle.style}"
|
||
data-mouseEnter="mouseAction|Enter|this|${nd.id}|event|handle"
|
||
data-mouseLeave="mouseAction|Leave|this|${nd.id}|event|handle"
|
||
>
|
||
${typeof obj.handle.html == 'function' ? obj.handle.html.call(obj, nd) : obj.handle.html}
|
||
</div>`
|
||
: ''
|
||
}
|
||
<div class="w2ui-node-data" style="margin-left: ${level * obj.levelPadding + obj.handle.size}px">
|
||
${expand} ${tmp} ${counts}
|
||
<div class="w2ui-node-text w2ui-node-caption" style="${nd.style || ''}">${text}</div>
|
||
</div>
|
||
</div>
|
||
<div class="w2ui-node-sub" id="node_${nd.id}_sub" style="${nd.style}; ${!nd.hidden && nd.expanded ? '' : 'display: none;'}"></div>`
|
||
if (obj.flat) {
|
||
html = `
|
||
<div id="node_${nd.id}" class="${classes.join(' ')}" style="${nd.hidden ? 'display: none;' : ''}"
|
||
data-click="click|${nd.id}|event"
|
||
data-dblclick="dblClick|${nd.id}|event"
|
||
data-contextmenu="contextMenu|${nd.id}|event"
|
||
data-mouseEnter="mouseAction|Enter|this|${nd.id}|event|tooltip"
|
||
data-mouseLeave="mouseAction|Leave|this|${nd.id}|event|tooltip"
|
||
>
|
||
<div class="w2ui-node-data w2ui-node-flat">${tmp}</div>
|
||
</div>
|
||
<div class="w2ui-node-sub" id="node_${nd.id}_sub" style="${nd.style}; ${!nd.hidden && nd.expanded ? '' : 'display: none;'}"></div>`
|
||
}
|
||
}
|
||
return html
|
||
}
|
||
}
|
||
mouseAction(action, el, id, event, type) {
|
||
let node = this.get(id)
|
||
let text = w2utils.lang(typeof node.text == 'function' ? node.text.call(this, node) : node.text)
|
||
let tooltip = text + (node.count || node.count === 0 ? ' - <span class="w2ui-node-count">'+ node.count +'</span>' : '')
|
||
let edata = this.trigger('mouse' + action, { target: id, node, tooltip, originalEvent: event })
|
||
if (type == 'tooltip') {
|
||
this.tooltip(el, tooltip, id)
|
||
}
|
||
if (type == 'handle') {
|
||
this.handleTooltip(el, id)
|
||
}
|
||
edata.finish()
|
||
}
|
||
tooltip(el, text, id) {
|
||
let $el = query(el).find('.w2ui-node-data')
|
||
if (text !== '') {
|
||
w2tooltip.show({
|
||
anchor: $el.get(0),
|
||
name: this.name + '_tooltip',
|
||
html: text,
|
||
position: 'right|left'
|
||
})
|
||
} else {
|
||
w2tooltip.hide(this.name + '_tooltip')
|
||
}
|
||
}
|
||
handleTooltip(anchor, id) {
|
||
let text = this.handle.tooltip
|
||
if (typeof text == 'function') {
|
||
text = text(id)
|
||
}
|
||
if (text !== '' && id != null) {
|
||
w2tooltip.show({
|
||
anchor: anchor,
|
||
name: this.name + '_tooltip',
|
||
html: text,
|
||
position: 'top|bottom'
|
||
})
|
||
} else {
|
||
w2tooltip.hide(this.name + '_tooltip')
|
||
}
|
||
}
|
||
showPlus(el, color) {
|
||
query(el).find('span:nth-child(1)').css('color', color)
|
||
}
|
||
resize() {
|
||
let time = Date.now()
|
||
// event before
|
||
let edata = this.trigger('resize', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
let rect = query(this.box).get(0).getBoundingClientRect()
|
||
query(this.box).css('overflow', 'hidden') // container should have no overflow
|
||
query(this.box).find(':scope > div').css({
|
||
width : rect.width + 'px',
|
||
height : rect.height + 'px'
|
||
})
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// clean up
|
||
if (query(this.box).find('.w2ui-sidebar-body').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-sidebar')
|
||
.html('')
|
||
}
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
lock(msg, showSpinner) {
|
||
let args = Array.from(arguments)
|
||
args.unshift(this.box)
|
||
w2utils.lock(...args)
|
||
}
|
||
unlock(speed) {
|
||
w2utils.unlock(this.box, speed)
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tooltip
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
* - removed jQuery dependency
|
||
* - observeResize for the box
|
||
* - refactored w2events
|
||
* - scrollIntoView - removed callback
|
||
* - scroll, scrollIntoView return promise
|
||
* - animateInsert, animateClose - returns a promise
|
||
* - add, insert return a promise
|
||
* - onMouseEnter, onMouseLeave, onMouseDown, onMouseUp
|
||
*/
|
||
|
||
class w2tabs extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.box = null // DOM Element that holds the element
|
||
this.name = null // unique name for w2ui
|
||
this.active = null
|
||
this.reorder = false
|
||
this.flow = 'down' // can be down or up
|
||
this.tooltip = 'top|left' // can be top, bottom, left, right
|
||
this.tabs = []
|
||
this.routeData = {} // data for dynamic routes
|
||
this.last = {} // placeholder for internal variables
|
||
this.right = ''
|
||
this.style = ''
|
||
this.onClick = null
|
||
this.onMouseEnter = null // mouse enter and lease
|
||
this.onMouseLeave = null
|
||
this.onMouseDown = null
|
||
this.onMouseUp = null
|
||
this.onClose = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.tab_template = {
|
||
id: null,
|
||
text: null,
|
||
route: null,
|
||
hidden: false,
|
||
disabled: false,
|
||
closable: false,
|
||
tooltip: null,
|
||
style: '',
|
||
onClick: null,
|
||
onRefresh: null,
|
||
onClose: null
|
||
}
|
||
let tabs = options.tabs
|
||
delete options.tabs
|
||
// mix in options
|
||
Object.assign(this, options)
|
||
// add item via method to makes sure item_template is applied
|
||
if (Array.isArray(tabs)) this.add(tabs)
|
||
// need to reassign back to keep it in config
|
||
options.tabs = tabs
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
}
|
||
add(tab) {
|
||
return this.insert(null, tab)
|
||
}
|
||
insert(id, tabs) {
|
||
if (!Array.isArray(tabs)) tabs = [tabs]
|
||
// assume it is array
|
||
let proms = []
|
||
tabs.forEach(tab => {
|
||
// checks
|
||
if (tab.id == null) {
|
||
console.log(`ERROR: The parameter "id" is required but not supplied. (obj: ${this.name})`)
|
||
return
|
||
}
|
||
if (!w2utils.checkUniqueId(tab.id, this.tabs, 'tabs', this.name)) return
|
||
// add tab
|
||
let it = Object.assign({}, this.tab_template, tab)
|
||
if (id == null) {
|
||
this.tabs.push(it)
|
||
proms.push(this.animateInsert(null, it))
|
||
} else {
|
||
let middle = this.get(id, true)
|
||
let before = this.tabs[middle].id
|
||
this.tabs.splice(middle, 0, it)
|
||
proms.push(this.animateInsert(before, it))
|
||
}
|
||
})
|
||
return Promise.all(proms)
|
||
}
|
||
remove() {
|
||
let effected = 0
|
||
Array.from(arguments).forEach(it => {
|
||
let tab = this.get(it)
|
||
if (!tab) return
|
||
effected++
|
||
// remove from array
|
||
this.tabs.splice(this.get(tab.id, true), 1)
|
||
// remove from screen
|
||
query(this.box).find(`#tabs_${this.name}_tab_${w2utils.escapeId(tab.id)}`).remove()
|
||
})
|
||
this.resize()
|
||
return effected
|
||
}
|
||
select(id) {
|
||
if (this.active == id || this.get(id) == null) return false
|
||
this.active = id
|
||
this.refresh()
|
||
return true
|
||
}
|
||
set(id, tab) {
|
||
let index = this.get(id, true)
|
||
if (index == null) return false
|
||
w2utils.extend(this.tabs[index], tab)
|
||
this.refresh(id)
|
||
return true
|
||
}
|
||
get(id, returnIndex) {
|
||
if (arguments.length === 0) {
|
||
let all = []
|
||
for (let i1 = 0; i1 < this.tabs.length; i1++) {
|
||
if (this.tabs[i1].id != null) {
|
||
all.push(this.tabs[i1].id)
|
||
}
|
||
}
|
||
return all
|
||
} else {
|
||
for (let i2 = 0; i2 < this.tabs.length; i2++) {
|
||
if (this.tabs[i2].id == id) { // need to be == since id can be numeric
|
||
return (returnIndex === true ? i2 : this.tabs[i2])
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
show() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let tab = this.get(it)
|
||
if (!tab || tab.hidden === false) return
|
||
tab.hidden = false
|
||
effected.push(tab.id)
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it); this.resize() }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
hide() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let tab = this.get(it)
|
||
if (!tab || tab.hidden === true) return
|
||
tab.hidden = true
|
||
effected.push(tab.id)
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it); this.resize() }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
enable() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let tab = this.get(it)
|
||
if (!tab || tab.disabled === false) return
|
||
tab.disabled = false
|
||
effected.push(tab.id)
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
disable() {
|
||
let effected = []
|
||
Array.from(arguments).forEach(it => {
|
||
let tab = this.get(it)
|
||
if (!tab || tab.disabled === true) return
|
||
tab.disabled = true
|
||
effected.push(tab.id)
|
||
})
|
||
setTimeout(() => { effected.forEach(it => { this.refresh(it) }) }, 15) // needs timeout
|
||
return effected
|
||
}
|
||
dragMove(event) {
|
||
if (!this.last.reordering) return
|
||
let self = this
|
||
let info = this.last.moving
|
||
let tab = this.tabs[info.index]
|
||
let next = _find(info.index, 1)
|
||
let prev = _find(info.index, -1)
|
||
let $el = query(this.box).find('#tabs_'+ this.name + '_tab_'+ w2utils.escapeId(tab.id))
|
||
if (info.divX > 0 && next) {
|
||
let $nextEl = query(this.box).find('#tabs_'+ this.name + '_tab_'+ w2utils.escapeId(next.id))
|
||
let width1 = parseInt($el.get(0).clientWidth)
|
||
let width2 = parseInt($nextEl.get(0).clientWidth)
|
||
if (width1 < width2) {
|
||
width1 = Math.floor(width1 / 3)
|
||
width2 = width2 - width1
|
||
} else {
|
||
width1 = Math.floor(width2 / 3)
|
||
width2 = width2 - width1
|
||
}
|
||
if (info.divX > width2) {
|
||
let index = this.tabs.indexOf(next)
|
||
this.tabs.splice(info.index, 0, this.tabs.splice(index, 1)[0]) // reorder in the array
|
||
info.$tab.before($nextEl.get(0))
|
||
info.$tab.css('opacity', 0)
|
||
Object.assign(this.last.moving, {
|
||
index: index,
|
||
divX: -width1,
|
||
x: event.pageX + width1,
|
||
left: info.left + info.divX + width1
|
||
})
|
||
return
|
||
}
|
||
}
|
||
if (info.divX < 0 && prev) {
|
||
let $prevEl = query(this.box).find('#tabs_'+ this.name + '_tab_'+ w2utils.escapeId(prev.id))
|
||
let width1 = parseInt($el.get(0).clientWidth)
|
||
let width2 = parseInt($prevEl.get(0).clientWidth)
|
||
if (width1 < width2) {
|
||
width1 = Math.floor(width1 / 3)
|
||
width2 = width2 - width1
|
||
} else {
|
||
width1 = Math.floor(width2 / 3)
|
||
width2 = width2 - width1
|
||
}
|
||
if (Math.abs(info.divX) > width2) {
|
||
let index = this.tabs.indexOf(prev)
|
||
this.tabs.splice(info.index, 0, this.tabs.splice(index, 1)[0]) // reorder in the array
|
||
$prevEl.before(info.$tab)
|
||
info.$tab.css('opacity', 0)
|
||
Object.assign(info, {
|
||
index: index,
|
||
divX: width1,
|
||
x: event.pageX - width1,
|
||
left: info.left + info.divX - width1
|
||
})
|
||
return
|
||
}
|
||
}
|
||
function _find(ind, inc) {
|
||
ind += inc
|
||
let tab = self.tabs[ind]
|
||
if (tab && tab.hidden) {
|
||
tab = _find(ind, inc)
|
||
}
|
||
return tab
|
||
}
|
||
}
|
||
mouseAction(action, id, event) {
|
||
let tab = this.get(id)
|
||
let edata = this.trigger('mouse' + action, { target: id, tab, object: tab, originalEvent: event })
|
||
if (edata.isCancelled === true || tab.disabled || tab.hidden) return
|
||
switch (action) {
|
||
case 'Enter':
|
||
this.tooltipShow(id)
|
||
break
|
||
case 'Leave':
|
||
this.tooltipHide(id)
|
||
break
|
||
case 'Down':
|
||
this.initReorder(id, event)
|
||
break
|
||
case 'Up':
|
||
break
|
||
}
|
||
edata.finish()
|
||
}
|
||
tooltipShow(id) {
|
||
let item = this.get(id)
|
||
let el = query(this.box).find('#tabs_'+ this.name + '_tab_'+ w2utils.escapeId(id)).get(0)
|
||
if (this.tooltip == null || item.disabled || this.last.reordering) {
|
||
return
|
||
}
|
||
let pos = this.tooltip
|
||
let txt = item.tooltip
|
||
if (typeof txt == 'function') txt = txt.call(this, item)
|
||
w2tooltip.show({
|
||
anchor: el,
|
||
name: this.name + '_tooltip',
|
||
html: txt,
|
||
position: pos
|
||
})
|
||
}
|
||
tooltipHide(id) {
|
||
if (this.tooltip == null) return
|
||
w2tooltip.hide(this.name + '_tooltip')
|
||
}
|
||
getTabHTML(id) {
|
||
let index = this.get(id, true)
|
||
let tab = this.tabs[index]
|
||
if (tab == null) return false
|
||
if (tab.text == null && tab.caption != null) tab.text = tab.caption
|
||
if (tab.tooltip == null && tab.hint != null) tab.tooltip = tab.hint // for backward compatibility
|
||
if (tab.caption != null) {
|
||
console.log('NOTICE: tabs tab.caption property is deprecated, please use tab.text. Tab -> ', tab)
|
||
}
|
||
if (tab.hint != null) {
|
||
console.log('NOTICE: tabs tab.hint property is deprecated, please use tab.tooltip. Tab -> ', tab)
|
||
}
|
||
let text = tab.text
|
||
if (typeof text == 'function') text = text.call(this, tab)
|
||
if (text == null) text = ''
|
||
let closable = ''
|
||
let addStyle = ''
|
||
if (tab.hidden) { addStyle += 'display: none;' }
|
||
if (tab.disabled) { addStyle += 'opacity: 0.2;' }
|
||
if (tab.closable && !tab.disabled) {
|
||
closable = `<div class="w2ui-tab-close w2ui-eaction ${this.active === tab.id ? 'active' : ''}"
|
||
data-mousedown="stop" data-mouseup="clickClose|${tab.id}|event">
|
||
</div>`
|
||
}
|
||
return `
|
||
<div id="tabs_${this.name}_tab_${tab.id}" style="${addStyle} ${tab.style}"
|
||
class="w2ui-tab w2ui-eaction ${this.active === tab.id ? 'active' : ''} ${tab.closable ? 'closable' : ''} ${tab.class ? tab.class : ''}"
|
||
data-mouseenter="mouseAction|Enter|${tab.id}|event]"
|
||
data-mouseleave="mouseAction|Leave|${tab.id}|event]"
|
||
data-mousedown="mouseAction|Down|${tab.id}|event"
|
||
data-mouseup="mouseAction|Up|${tab.id}|event"
|
||
data-click="click|${tab.id}|event"
|
||
>
|
||
${w2utils.lang(text) + closable}
|
||
</div>`
|
||
}
|
||
refresh(id) {
|
||
let time = Date.now()
|
||
if (this.flow == 'up') {
|
||
query(this.box).addClass('w2ui-tabs-up')
|
||
} else {
|
||
query(this.box).removeClass('w2ui-tabs-up')
|
||
}
|
||
// event before
|
||
let edata = this.trigger('refresh', { target: (id != null ? id : this.name), object: this.get(id) })
|
||
if (edata.isCancelled === true) return
|
||
if (id == null) {
|
||
// refresh all
|
||
for (let i = 0; i < this.tabs.length; i++) {
|
||
this.refresh(this.tabs[i].id)
|
||
}
|
||
} else {
|
||
// create or refresh only one item
|
||
let selector = '#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(id)
|
||
let $tab = query(this.box).find(selector)
|
||
let tabHTML = this.getTabHTML(id)
|
||
if ($tab.length === 0) {
|
||
query(this.box).find('#tabs_'+ this.name +'_right').before(tabHTML)
|
||
} else {
|
||
if (query(this.box).find('.tab-animate-insert').length == 0) {
|
||
$tab.replace(tabHTML)
|
||
}
|
||
}
|
||
w2utils.bindEvents(query(this.box).find(`${selector}, ${selector} .w2ui-eaction`), this)
|
||
}
|
||
// right html
|
||
query(this.box).find('#tabs_'+ this.name +'_right').html(this.right)
|
||
// event after
|
||
edata.finish()
|
||
// this.resize();
|
||
return Date.now() - time
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find('#tabs_'+ this.name + '_right').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-tabs')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.box) return false
|
||
// render all buttons
|
||
let html =`
|
||
<div class="w2ui-tabs-line"></div>
|
||
<div class="w2ui-scroll-wrapper w2ui-eaction" data-mousedown="resize">
|
||
<div id="tabs_${this.name}_right" class="w2ui-tabs-right">${this.right}</div>
|
||
</div>
|
||
<div class="w2ui-scroll-left w2ui-eaction" data-click='["scroll","left"]'></div>
|
||
<div class="w2ui-scroll-right w2ui-eaction" data-click='["scroll","right"]'></div>`
|
||
query(this.box)
|
||
.attr('name', this.name)
|
||
.addClass('w2ui-reset w2ui-tabs')
|
||
.html(html)
|
||
if (query(this.box).length > 0) {
|
||
query(this.box)[0].style.cssText += this.style
|
||
}
|
||
w2utils.bindEvents(query(this.box).find('.w2ui-eaction'), this)
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
// event after
|
||
edata.finish()
|
||
this.refresh()
|
||
this.resize()
|
||
return Date.now() - time
|
||
}
|
||
initReorder(id, event) {
|
||
if (!this.reorder) return
|
||
let self = this
|
||
let $tab = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id))
|
||
let tabIndex = this.get(id, true)
|
||
let $ghost = query($tab.get(0).cloneNode(true))
|
||
let edata
|
||
$ghost.attr('id', '#tabs_' + this.name + '_tab_ghost')
|
||
this.last.moving = {
|
||
index: tabIndex,
|
||
indexFrom: tabIndex,
|
||
$tab: $tab,
|
||
$ghost: $ghost,
|
||
divX: 0,
|
||
left: $tab.get(0).getBoundingClientRect().left,
|
||
parentX: query(this.box).get(0).getBoundingClientRect().left,
|
||
x: event.pageX,
|
||
opacity: $tab.css('opacity')
|
||
}
|
||
query(document)
|
||
.off('.w2uiTabReorder')
|
||
.on('mousemove.w2uiTabReorder', function (event) {
|
||
if (!self.last.reordering) {
|
||
// event before
|
||
edata = self.trigger('reorder', { target: self.tabs[tabIndex].id, indexFrom: tabIndex, tab: self.tabs[tabIndex] })
|
||
if (edata.isCancelled === true) return
|
||
w2tooltip.hide(this.name + '_tooltip')
|
||
self.last.reordering = true
|
||
$ghost.addClass('moving')
|
||
$ghost.css({
|
||
'pointer-events': 'none',
|
||
'position': 'absolute',
|
||
'left': $tab.get(0).getBoundingClientRect().left
|
||
})
|
||
$tab.css('opacity', 0)
|
||
query(self.box).find('.w2ui-scroll-wrapper').append($ghost.get(0))
|
||
query(self.box).find('.w2ui-tab-close').hide()
|
||
}
|
||
self.last.moving.divX = event.pageX - self.last.moving.x
|
||
$ghost.css('left', (self.last.moving.left - self.last.moving.parentX + self.last.moving.divX) + 'px')
|
||
self.dragMove(event)
|
||
})
|
||
.on('mouseup.w2uiTabReorder', function () {
|
||
query(document).off('.w2uiTabReorder')
|
||
$ghost.css({
|
||
'transition': '0.1s',
|
||
'left': self.last.moving.$tab.get(0).getBoundingClientRect().left - self.last.moving.parentX
|
||
})
|
||
query(self.box).find('.w2ui-tab-close').show()
|
||
setTimeout(() => {
|
||
$ghost.remove()
|
||
$tab.css({ opacity: self.last.moving.opacity })
|
||
// self.render()
|
||
if (self.last.reordering) {
|
||
edata.finish({ indexTo: self.last.moving.index })
|
||
}
|
||
self.last.reordering = false
|
||
}, 100)
|
||
})
|
||
}
|
||
scroll(direction, instant) {
|
||
return new Promise((resolve, reject) => {
|
||
let scrollBox = query(this.box).find('.w2ui-scroll-wrapper')
|
||
let scrollLeft = scrollBox.get(0).scrollLeft
|
||
let right = scrollBox.find('.w2ui-tabs-right').get(0)
|
||
let width1 = scrollBox.parent().get(0).getBoundingClientRect().width
|
||
let width2 = scrollLeft + parseInt(right.offsetLeft) + parseInt(right.clientWidth )
|
||
switch (direction) {
|
||
case 'left': {
|
||
let scroll = scrollLeft - width1 + 50 // 35 is width of both button
|
||
if (scroll <= 0) scroll = 0
|
||
scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' })
|
||
break
|
||
}
|
||
case 'right': {
|
||
let scroll = scrollLeft + width1 - 50 // 35 is width of both button
|
||
if (scroll >= width2 - width1) scroll = width2 - width1
|
||
scrollBox.get(0).scrollTo({ top: 0, left: scroll, behavior: instant ? 'atuo' : 'smooth' })
|
||
break
|
||
}
|
||
}
|
||
setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 350)
|
||
})
|
||
}
|
||
scrollIntoView(id, instant) {
|
||
return new Promise((resolve, reject) => {
|
||
if (id == null) id = this.active
|
||
let tab = this.get(id)
|
||
if (tab == null) return
|
||
let tabEl = query(this.box).find('#tabs_' + this.name + '_tab_' + w2utils.escapeId(id)).get(0)
|
||
tabEl.scrollIntoView({ block: 'start', inline: 'center', behavior: instant ? 'atuo' : 'smooth' })
|
||
setTimeout(() => { this.resize(); resolve() }, instant ? 0 : 500)
|
||
})
|
||
}
|
||
resize() {
|
||
let time = Date.now()
|
||
if (this.box == null) return
|
||
// event before
|
||
let edata = this.trigger('resize', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// show hide overflow buttons
|
||
let box = query(this.box)
|
||
box.find('.w2ui-scroll-left, .w2ui-scroll-right').hide()
|
||
let scrollBox = box.find('.w2ui-scroll-wrapper').get(0)
|
||
let $right = box.find('.w2ui-tabs-right')
|
||
let boxWidth = box.get(0).getBoundingClientRect().width
|
||
let itemsWidth = ($right.length > 0 ? $right[0].offsetLeft + $right[0].clientWidth : 0)
|
||
if (boxWidth < itemsWidth) {
|
||
// we have overflown content
|
||
if (scrollBox.scrollLeft > 0) {
|
||
box.find('.w2ui-scroll-left').show()
|
||
}
|
||
if (boxWidth < itemsWidth - scrollBox.scrollLeft) {
|
||
box.find('.w2ui-scroll-right').show()
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// clean up
|
||
if (query(this.box).find('#tabs_'+ this.name + '_right').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-tabs')
|
||
.html('')
|
||
}
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
// ===================================================
|
||
// -- Internal Event Handlers
|
||
click(id, event) {
|
||
let tab = this.get(id)
|
||
if (tab == null || tab.disabled || this.last.reordering) return false
|
||
// event before
|
||
let edata = this.trigger('click', { target: id, tab: tab, object: tab, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
query(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.active)).removeClass('active')
|
||
this.active = tab.id
|
||
query(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(this.active)).addClass('active')
|
||
// route processing
|
||
if (typeof tab.route == 'string') {
|
||
let route = tab.route !== '' ? String('/'+ tab.route).replace(/\/{2,}/g, '/') : ''
|
||
let info = w2utils.parseRoute(route)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (this.routeData[info.keys[k].name] == null) continue
|
||
route = route.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
setTimeout(() => { window.location.hash = route }, 1)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
clickClose(id, event) {
|
||
let tab = this.get(id)
|
||
if (tab == null || tab.disabled) return false
|
||
// event before
|
||
let edata = this.trigger('close', { target: id, object: tab, tab, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
this.animateClose(id).then(() => {
|
||
this.remove(id)
|
||
edata.finish()
|
||
this.refresh()
|
||
})
|
||
if (event) event.stopPropagation()
|
||
}
|
||
animateClose(id) {
|
||
return new Promise((resolve, reject) => {
|
||
let $tab = query(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(id))
|
||
let width = parseInt($tab.get(0).clientWidth || 0)
|
||
let anim = `<div class="tab-animate-close" style="display: inline-block; flex-shrink: 0; width: ${width}px; transition: width 0.25s"></div>`
|
||
let $anim = $tab.replace(anim)
|
||
setTimeout(() => { $anim.css({ width: '0px' }) }, 1)
|
||
setTimeout(() => {
|
||
$anim.remove()
|
||
this.resize()
|
||
resolve()
|
||
}, 500)
|
||
})
|
||
}
|
||
animateInsert(id, tab) {
|
||
return new Promise((resolve, reject) => {
|
||
let $before = query(this.box).find('#tabs_'+ this.name +'_tab_'+ w2utils.escapeId(id))
|
||
let $tab = query.html(this.getTabHTML(tab.id))
|
||
if ($before.length == 0) {
|
||
$before = query(this.box).find('#tabs_tabs_right')
|
||
$before.before($tab)
|
||
this.resize()
|
||
} else {
|
||
$tab.css({ opacity: 0 })
|
||
// first insert tab on the right to get its proper dimentions
|
||
query(this.box).find('#tabs_tabs_right').before($tab.get(0))
|
||
let $tmp = query(this.box).find('#' + $tab.attr('id'))
|
||
let width = $tmp.get(0).clientWidth ?? 0
|
||
// insert animation div
|
||
let $anim = query.html('<div class="tab-animate-insert" style="flex-shrink: 0; width: 0; transition: width 0.25s"></div>')
|
||
$before.before($anim)
|
||
// hide tab and move it in the right position
|
||
$tab.hide()
|
||
$anim.before($tab[0])
|
||
setTimeout(() => { $anim.css({ width: width + 'px' }) }, 1)
|
||
setTimeout(() => {
|
||
$anim.remove()
|
||
$tab.css({ opacity: 1 }).show()
|
||
this.refresh(tab.id)
|
||
this.resize()
|
||
resolve()
|
||
}, 500)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tabs, w2toolbar
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
* - remove jQuery dependency
|
||
* - layout.confirm - refactored
|
||
* - layout.message - refactored
|
||
* - panel.removed
|
||
*/
|
||
|
||
let w2panels = ['top', 'left', 'main', 'preview', 'right', 'bottom']
|
||
class w2layout extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.box = null // DOM Element that holds the element
|
||
this.name = null // unique name for w2ui
|
||
this.panels = []
|
||
this.last = {}
|
||
this.padding = 1 // panel padding
|
||
this.resizer = 4 // resizer width or height
|
||
this.style = ''
|
||
this.onShow = null
|
||
this.onHide = null
|
||
this.onResizing = null
|
||
this.onResizerClick = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onChange = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.panel_template = {
|
||
type: null, // left, right, top, bottom
|
||
title: '',
|
||
size: 100, // width or height depending on panel name
|
||
minSize: 20,
|
||
maxSize: false,
|
||
hidden: false,
|
||
resizable: false,
|
||
overflow: 'auto',
|
||
style: '',
|
||
html: '', // can be String or Object with .render(box) method
|
||
tabs: null,
|
||
toolbar: null,
|
||
width: null, // read only
|
||
height: null, // read only
|
||
show: {
|
||
toolbar: false,
|
||
tabs: false
|
||
},
|
||
removed: null, // function to call when content is overwritten
|
||
onRefresh: null,
|
||
onShow: null,
|
||
onHide: null
|
||
}
|
||
// mix in options
|
||
Object.assign(this, options)
|
||
if (!Array.isArray(this.panels)) this.panels = []
|
||
// add defined panels
|
||
this.panels.forEach((panel, ind) => {
|
||
this.panels[ind] = w2utils.extend({}, this.panel_template, panel)
|
||
if (w2utils.isPlainObject(panel.tabs) || Array.isArray(panel.tabs)) initTabs(this, panel.type)
|
||
if (w2utils.isPlainObject(panel.toolbar) || Array.isArray(panel.toolbar)) initToolbar(this, panel.type)
|
||
})
|
||
// add all other panels
|
||
w2panels.forEach(tab => {
|
||
if (this.get(tab) != null) return
|
||
this.panels.push(w2utils.extend({}, this.panel_template, { type: tab, hidden: (tab !== 'main'), size: 50 }))
|
||
})
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
function initTabs(object, panel, tabs) {
|
||
let pan = object.get(panel)
|
||
if (pan != null && tabs == null) tabs = pan.tabs
|
||
if (pan == null || tabs == null) return false
|
||
// instantiate tabs
|
||
if (Array.isArray(tabs)) tabs = { tabs: tabs }
|
||
let name = object.name + '_' + panel + '_tabs'
|
||
if (w2ui[name]) w2ui[name].destroy() // destroy if existed
|
||
pan.tabs = new w2tabs(w2utils.extend({}, tabs, { owner: object, name: object.name + '_' + panel + '_tabs' }))
|
||
pan.show.tabs = true
|
||
return true
|
||
}
|
||
function initToolbar(object, panel, toolbar) {
|
||
let pan = object.get(panel)
|
||
if (pan != null && toolbar == null) toolbar = pan.toolbar
|
||
if (pan == null || toolbar == null) return false
|
||
// instantiate toolbar
|
||
if (Array.isArray(toolbar)) toolbar = { items: toolbar }
|
||
let name = object.name + '_' + panel + '_toolbar'
|
||
if (w2ui[name]) w2ui[name].destroy() // destroy if existed
|
||
pan.toolbar = new w2toolbar(w2utils.extend({}, toolbar, { owner: object, name: object.name + '_' + panel + '_toolbar' }))
|
||
pan.show.toolbar = true
|
||
return true
|
||
}
|
||
}
|
||
html(panel, data, transition) {
|
||
let p = this.get(panel)
|
||
let promise = {
|
||
panel: panel,
|
||
html: p.html,
|
||
error: false,
|
||
cancelled: false,
|
||
removed(cb) {
|
||
if (typeof cb == 'function') {
|
||
p.removed = cb
|
||
}
|
||
}
|
||
}
|
||
if (typeof p.removed == 'function') {
|
||
p.removed({ panel: panel, html: p.html, html_new: data, transition: transition || 'none' })
|
||
p.removed = null // this is one time call back only
|
||
}
|
||
// if it is CSS panel
|
||
if (panel == 'css') {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_css').html('<style>'+ data +'</style>')
|
||
promise.status = true
|
||
return promise
|
||
}
|
||
if (p == null) {
|
||
console.log('ERROR: incorrect panel name. Panel name can be main, left, right, top, bottom, preview or css')
|
||
promise.error = true
|
||
return promise
|
||
}
|
||
if (data == null) {
|
||
return promise
|
||
}
|
||
// event before
|
||
let edata = this.trigger('change', { target: panel, panel: p, html_new: data, transition: transition })
|
||
if (edata.isCancelled === true) {
|
||
promise.cancelled = true
|
||
return promise
|
||
}
|
||
let pname = '#layout_'+ this.name + '_panel_'+ p.type
|
||
let current = query(this.box).find(pname + '> .w2ui-panel-content')
|
||
let panelTop = 0
|
||
if (current.length > 0) {
|
||
query(this.box).find(pname).get(0).scrollTop = 0
|
||
panelTop = query(current).css('top')
|
||
}
|
||
if (p.html === '') {
|
||
p.html = data
|
||
this.refresh(panel)
|
||
} else {
|
||
p.html = data
|
||
if (!p.hidden) {
|
||
if (transition != null && transition !== '') {
|
||
// apply transition
|
||
query(this.box).addClass('animating')
|
||
let div1 = query(this.box).find(pname + '> .w2ui-panel-content')
|
||
div1.after('<div class="w2ui-panel-content new-panel" style="'+ div1[0].style.cssText +'"></div>')
|
||
let div2 = query(this.box).find(pname + '> .w2ui-panel-content.new-panel')
|
||
div1.css('top', panelTop)
|
||
div2.css('top', panelTop)
|
||
if (typeof data == 'object') {
|
||
data.box = div2[0] // do not do .render(box);
|
||
data.render()
|
||
} else {
|
||
div2.hide().html(data)
|
||
}
|
||
w2utils.transition(div1[0], div2[0], transition, () => {
|
||
div1.remove()
|
||
div2.removeClass('new-panel')
|
||
div2.css('overflow', p.overflow)
|
||
// make sure only one content left
|
||
query(query(this.box).find(pname + '> .w2ui-panel-content').get(1)).remove()
|
||
query(this.box).removeClass('animating')
|
||
this.refresh(panel)
|
||
})
|
||
} else {
|
||
this.refresh(panel)
|
||
}
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
return promise
|
||
}
|
||
message(panel, options) {
|
||
let p = this.get(panel)
|
||
let box = query(this.box).find('#layout_'+ this.name + '_panel_'+ p.type)
|
||
let oldOverflow = box.css('overflow')
|
||
box.css('overflow', 'hidden')
|
||
let prom = w2utils.message({
|
||
owner: this,
|
||
box : box.get(0),
|
||
after: '.w2ui-panel-title',
|
||
param: panel
|
||
}, options)
|
||
if (prom) {
|
||
prom.self.on('close:after', () => {
|
||
box.css('overflow', oldOverflow)
|
||
})
|
||
}
|
||
return prom
|
||
}
|
||
confirm(panel, options) {
|
||
let p = this.get(panel)
|
||
let box = query(this.box).find('#layout_'+ this.name + '_panel_'+ p.type)
|
||
let oldOverflow = box.css('overflow')
|
||
box.css('overflow', 'hidden')
|
||
let prom = w2utils.confirm({
|
||
owner : this,
|
||
box : box.get(0),
|
||
after : '.w2ui-panel-title',
|
||
param : panel
|
||
}, options)
|
||
if (prom) {
|
||
prom.self.on('close:after', () => {
|
||
box.css('overflow', oldOverflow)
|
||
})
|
||
}
|
||
return prom
|
||
}
|
||
load(panel, url, transition) {
|
||
return new Promise((resolve, reject) => {
|
||
if ((panel == 'css' || this.get(panel) != null) && url != null) {
|
||
fetch(url)
|
||
.then(resp => resp.text())
|
||
.then(text => {
|
||
this.resize()
|
||
resolve(this.html(panel, text, transition))
|
||
})
|
||
} else {
|
||
reject()
|
||
}
|
||
})
|
||
}
|
||
sizeTo(panel, size, instant) {
|
||
let pan = this.get(panel)
|
||
if (pan == null) return false
|
||
// resize
|
||
query(this.box).find(':scope > div > .w2ui-panel')
|
||
.css('transition', (instant !== true ? '.2s' : '0s'))
|
||
setTimeout(() => { this.set(panel, { size: size }) }, 1)
|
||
// clean
|
||
setTimeout(() => {
|
||
query(this.box).find(':scope > div > .w2ui-panel').css('transition', '0s')
|
||
this.resize()
|
||
}, 300)
|
||
return true
|
||
}
|
||
show(panel, immediate) {
|
||
// event before
|
||
let edata = this.trigger('show', { target: panel, thisect: this.get(panel), immediate: immediate })
|
||
if (edata.isCancelled === true) return
|
||
let p = this.get(panel)
|
||
if (p == null) return false
|
||
p.hidden = false
|
||
if (immediate === true) {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+panel)
|
||
.css({ 'opacity': '1' })
|
||
edata.finish()
|
||
this.resize()
|
||
} else {
|
||
// resize
|
||
query(this.box).addClass('animating')
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+panel)
|
||
.css({ 'opacity': '0' })
|
||
query(this.box).find(':scope > div > .w2ui-panel')
|
||
.css('transition', '.2s')
|
||
setTimeout(() => { this.resize() }, 1)
|
||
// show
|
||
setTimeout(() => {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+ panel).css({ 'opacity': '1' })
|
||
}, 250)
|
||
// clean
|
||
setTimeout(() => {
|
||
query(this.box).find(':scope > div > .w2ui-panel')
|
||
.css('transition', '0s')
|
||
query(this.box).removeClass('animating')
|
||
edata.finish()
|
||
this.resize()
|
||
}, 300)
|
||
}
|
||
return true
|
||
}
|
||
hide(panel, immediate) {
|
||
// event before
|
||
let edata = this.trigger('hide', { target: panel, object: this.get(panel), immediate: immediate })
|
||
if (edata.isCancelled === true) return
|
||
let p = this.get(panel)
|
||
if (p == null) return false
|
||
p.hidden = true
|
||
if (immediate === true) {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+panel)
|
||
.css({ 'opacity': '0' })
|
||
edata.finish()
|
||
this.resize()
|
||
} else {
|
||
// hide
|
||
query(this.box).addClass('animating')
|
||
query(this.box).find(':scope > div > .w2ui-panel')
|
||
.css('transition', '.2s')
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+panel)
|
||
.css({ 'opacity': '0' })
|
||
setTimeout(() => { this.resize() }, 1)
|
||
// clean
|
||
setTimeout(() => {
|
||
query(this.box).find(':scope > div > .w2ui-panel')
|
||
.css('transition', '0s')
|
||
query(this.box).removeClass('animating')
|
||
edata.finish()
|
||
this.resize()
|
||
}, 300)
|
||
}
|
||
return true
|
||
}
|
||
toggle(panel, immediate) {
|
||
let p = this.get(panel)
|
||
if (p == null) return false
|
||
if (p.hidden) return this.show(panel, immediate); else return this.hide(panel, immediate)
|
||
}
|
||
set(panel, options) {
|
||
let ind = this.get(panel, true)
|
||
if (ind == null) return false
|
||
w2utils.extend(this.panels[ind], options)
|
||
// refresh only when content changed
|
||
if (options.html != null || options.resizable != null) {
|
||
this.refresh(panel)
|
||
}
|
||
// show/hide resizer
|
||
this.resize() // resize is needed when panel size is changed
|
||
return true
|
||
}
|
||
get(panel, returnIndex) {
|
||
for (let p = 0; p < this.panels.length; p++) {
|
||
if (this.panels[p].type == panel) {
|
||
if (returnIndex === true) return p; else return this.panels[p]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
el(panel) {
|
||
let el = query(this.box).find('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-content')
|
||
if (el.length != 1) return null
|
||
return el[0]
|
||
}
|
||
hideToolbar(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
pan.show.toolbar = false
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-toolbar').hide()
|
||
this.resize()
|
||
}
|
||
showToolbar(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
pan.show.toolbar = true
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-toolbar').show()
|
||
this.resize()
|
||
}
|
||
toggleToolbar(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
if (pan.show.toolbar) this.hideToolbar(panel); else this.showToolbar(panel)
|
||
}
|
||
assignToolbar(panel, toolbar) {
|
||
if (typeof toolbar == 'string' && w2ui[toolbar] != null) toolbar = w2ui[toolbar]
|
||
let pan = this.get(panel)
|
||
pan.toolbar = toolbar
|
||
let tmp = query(this.box).find(panel +'> .w2ui-panel-toolbar')
|
||
if (pan.toolbar != null) {
|
||
if (tmp.find('[name='+ pan.toolbar.name +']').length === 0) {
|
||
pan.toolbar.render(tmp.get(0))
|
||
} else if (pan.toolbar != null) {
|
||
pan.toolbar.refresh()
|
||
}
|
||
toolbar.owner = this
|
||
this.showToolbar(panel)
|
||
this.refresh(panel)
|
||
} else {
|
||
tmp.html('')
|
||
this.hideToolbar(panel)
|
||
}
|
||
}
|
||
hideTabs(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
pan.show.tabs = false
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-tabs').hide()
|
||
this.resize()
|
||
}
|
||
showTabs(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
pan.show.tabs = true
|
||
query(this.box).find('#layout_'+ this.name +'_panel_'+ panel +'> .w2ui-panel-tabs').show()
|
||
this.resize()
|
||
}
|
||
toggleTabs(panel) {
|
||
let pan = this.get(panel)
|
||
if (!pan) return
|
||
if (pan.show.tabs) this.hideTabs(panel); else this.showTabs(panel)
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
let self = this
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find('#layout_'+ this.name +'_panel_main').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-layout')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.box) return false
|
||
// render layout
|
||
query(this.box)
|
||
.attr('name', this.name)
|
||
.addClass('w2ui-layout')
|
||
.html('<div></div>')
|
||
if (query(this.box).length > 0) {
|
||
query(this.box)[0].style.cssText += this.style
|
||
}
|
||
// create all panels
|
||
for (let p1 = 0; p1 < w2panels.length; p1++) {
|
||
let html = '<div id="layout_'+ this.name + '_panel_'+ w2panels[p1] +'" class="w2ui-panel">'+
|
||
' <div class="w2ui-panel-title"></div>'+
|
||
' <div class="w2ui-panel-tabs"></div>'+
|
||
' <div class="w2ui-panel-toolbar"></div>'+
|
||
' <div class="w2ui-panel-content"></div>'+
|
||
'</div>'+
|
||
'<div id="layout_'+ this.name + '_resizer_'+ w2panels[p1] +'" class="w2ui-resizer"></div>'
|
||
query(this.box).find(':scope > div').append(html)
|
||
}
|
||
query(this.box).find(':scope > div')
|
||
.append('<div id="layout_'+ this.name + '_panel_css" style="position: absolute; top: 10000px;"></div>')
|
||
this.refresh() // if refresh is not called here, the layout will not be available right after initialization
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
// process event
|
||
edata.finish()
|
||
// re-init events
|
||
setTimeout(() => { // needed this timeout to allow browser to render first if there are tabs or toolbar
|
||
self.last.events = { resizeStart, mouseMove, mouseUp }
|
||
this.resize()
|
||
}, 0)
|
||
return Date.now() - time
|
||
function resizeStart(type, evnt) {
|
||
if (!self.box) return
|
||
if (!evnt) evnt = window.event
|
||
query(document)
|
||
.off('mousemove', self.last.events.mouseMove)
|
||
.on('mousemove', self.last.events.mouseMove)
|
||
query(document)
|
||
.off('mouseup', self.last.events.mouseUp)
|
||
.on('mouseup', self.last.events.mouseUp)
|
||
self.last.resize = {
|
||
type : type,
|
||
x : evnt.screenX,
|
||
y : evnt.screenY,
|
||
diff_x : 0,
|
||
diff_y : 0,
|
||
value : 0
|
||
}
|
||
// lock all panels
|
||
w2panels.forEach(panel => {
|
||
let $tmp = query(self.el(panel)).find('.w2ui-lock')
|
||
if ($tmp.length > 0) {
|
||
$tmp.data('locked', 'yes')
|
||
} else {
|
||
self.lock(panel, { opacity: 0 })
|
||
}
|
||
})
|
||
let el = query(self.box).find('#layout_'+ self.name +'_resizer_'+ type).get(0)
|
||
if (type == 'left' || type == 'right') {
|
||
self.last.resize.value = parseInt(el.style.left)
|
||
}
|
||
if (type == 'top' || type == 'preview' || type == 'bottom') {
|
||
self.last.resize.value = parseInt(el.style.top)
|
||
}
|
||
}
|
||
function mouseUp(evnt) {
|
||
if (!self.box) return
|
||
if (!evnt) evnt = window.event
|
||
query(document).off('mousemove', self.last.events.mouseMove)
|
||
query(document).off('mouseup', self.last.events.mouseUp)
|
||
if (self.last.resize == null) return
|
||
// unlock all panels
|
||
w2panels.forEach(panel => {
|
||
let $tmp = query(self.el(panel)).find('.w2ui-lock')
|
||
if ($tmp.data('locked') == 'yes') {
|
||
$tmp.removeData('locked')
|
||
} else {
|
||
self.unlock(panel)
|
||
}
|
||
})
|
||
// set new size
|
||
if (self.last.diff_x !== 0 || self.last.resize.diff_y !== 0) { // only recalculate if changed
|
||
let ptop = self.get('top')
|
||
let pbottom = self.get('bottom')
|
||
let panel = self.get(self.last.resize.type)
|
||
let width = w2utils.getSize(query(self.box), 'width')
|
||
let height = w2utils.getSize(query(self.box), 'height')
|
||
let str = String(panel.size)
|
||
let ns, nd
|
||
switch (self.last.resize.type) {
|
||
case 'top':
|
||
ns = parseInt(panel.sizeCalculated) + self.last.resize.diff_y
|
||
nd = 0
|
||
break
|
||
case 'bottom':
|
||
ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_y
|
||
nd = 0
|
||
break
|
||
case 'preview':
|
||
ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_y
|
||
nd = (ptop && !ptop.hidden ? ptop.sizeCalculated : 0) +
|
||
(pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0)
|
||
break
|
||
case 'left':
|
||
ns = parseInt(panel.sizeCalculated) + self.last.resize.diff_x
|
||
nd = 0
|
||
break
|
||
case 'right':
|
||
ns = parseInt(panel.sizeCalculated) - self.last.resize.diff_x
|
||
nd = 0
|
||
break
|
||
}
|
||
// set size
|
||
if (str.substr(str.length-1) == '%') {
|
||
panel.size = Math.floor(ns * 100 / (panel.type == 'left' || panel.type == 'right' ? width : height - nd) * 100) / 100 + '%'
|
||
} else {
|
||
if (String(panel.size).substr(0, 1) == '-') {
|
||
panel.size = parseInt(panel.size) - panel.sizeCalculated + ns
|
||
} else {
|
||
panel.size = ns
|
||
}
|
||
}
|
||
self.resize()
|
||
}
|
||
query(self.box)
|
||
.find('#layout_'+ self.name + '_resizer_'+ self.last.resize.type)
|
||
.removeClass('active')
|
||
delete self.last.resize
|
||
}
|
||
function mouseMove(evnt) {
|
||
if (!self.box) return
|
||
if (!evnt) evnt = window.event
|
||
if (self.last.resize == null) return
|
||
let panel = self.get(self.last.resize.type)
|
||
// event before
|
||
let tmp = self.last.resize
|
||
let edata = self.trigger('resizing', { target: self.name, object: panel, originalEvent: evnt,
|
||
panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 })
|
||
if (edata.isCancelled === true) return
|
||
let p = query(self.box).find('#layout_'+ self.name + '_resizer_'+ tmp.type)
|
||
let resize_x = (evnt.screenX - tmp.x)
|
||
let resize_y = (evnt.screenY - tmp.y)
|
||
let mainPanel = self.get('main')
|
||
if (!p.hasClass('active')) p.addClass('active')
|
||
switch (tmp.type) {
|
||
case 'left':
|
||
if (panel.minSize - resize_x > panel.width) {
|
||
resize_x = panel.minSize - panel.width
|
||
}
|
||
if (panel.maxSize && (panel.width + resize_x > panel.maxSize)) {
|
||
resize_x = panel.maxSize - panel.width
|
||
}
|
||
if (mainPanel.minSize + resize_x > mainPanel.width) {
|
||
resize_x = mainPanel.width - mainPanel.minSize
|
||
}
|
||
break
|
||
case 'right':
|
||
if (panel.minSize + resize_x > panel.width) {
|
||
resize_x = panel.width - panel.minSize
|
||
}
|
||
if (panel.maxSize && (panel.width - resize_x > panel.maxSize)) {
|
||
resize_x = panel.width - panel.maxSize
|
||
}
|
||
if (mainPanel.minSize - resize_x > mainPanel.width) {
|
||
resize_x = mainPanel.minSize - mainPanel.width
|
||
}
|
||
break
|
||
case 'top':
|
||
if (panel.minSize - resize_y > panel.height) {
|
||
resize_y = panel.minSize - panel.height
|
||
}
|
||
if (panel.maxSize && (panel.height + resize_y > panel.maxSize)) {
|
||
resize_y = panel.maxSize - panel.height
|
||
}
|
||
if (mainPanel.minSize + resize_y > mainPanel.height) {
|
||
resize_y = mainPanel.height - mainPanel.minSize
|
||
}
|
||
break
|
||
case 'preview':
|
||
case 'bottom':
|
||
if (panel.minSize + resize_y > panel.height) {
|
||
resize_y = panel.height - panel.minSize
|
||
}
|
||
if (panel.maxSize && (panel.height - resize_y > panel.maxSize)) {
|
||
resize_y = panel.height - panel.maxSize
|
||
}
|
||
if (mainPanel.minSize - resize_y > mainPanel.height) {
|
||
resize_y = mainPanel.minSize - mainPanel.height
|
||
}
|
||
break
|
||
}
|
||
tmp.diff_x = resize_x
|
||
tmp.diff_y = resize_y
|
||
switch (tmp.type) {
|
||
case 'top':
|
||
case 'preview':
|
||
case 'bottom':
|
||
tmp.diff_x = 0
|
||
if (p.length > 0) p[0].style.top = (tmp.value + tmp.diff_y) + 'px'
|
||
break
|
||
case 'left':
|
||
case 'right':
|
||
tmp.diff_y = 0
|
||
if (p.length > 0) p[0].style.left = (tmp.value + tmp.diff_x) + 'px'
|
||
break
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
refresh(panel) {
|
||
let self = this
|
||
// if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
|
||
if (panel == null) panel = null
|
||
let time = Date.now()
|
||
// event before
|
||
let edata = self.trigger('refresh', { target: (panel != null ? panel : self.name), object: self.get(panel) })
|
||
if (edata.isCancelled === true) return
|
||
// self.unlock(panel);
|
||
if (typeof panel == 'string') {
|
||
let p = self.get(panel)
|
||
if (p == null) return
|
||
let pname = '#layout_'+ self.name + '_panel_'+ p.type
|
||
let rname = '#layout_'+ self.name +'_resizer_'+ p.type
|
||
// apply properties to the panel
|
||
query(self.box).find(pname).css({ display: p.hidden ? 'none' : 'block' })
|
||
if (p.resizable) {
|
||
query(self.box).find(rname).show()
|
||
} else {
|
||
query(self.box).find(rname).hide()
|
||
}
|
||
// insert content
|
||
if (typeof p.html == 'object' && typeof p.html.render === 'function') {
|
||
p.html.box = query(self.box).find(pname +'> .w2ui-panel-content')[0]
|
||
setTimeout(() => {
|
||
// need to remove unnecessary classes
|
||
if (query(self.box).find(pname +'> .w2ui-panel-content').length > 0) {
|
||
query(self.box).find(pname +'> .w2ui-panel-content')
|
||
.removeClass()
|
||
.removeAttr('name')
|
||
.addClass('w2ui-panel-content')
|
||
.css('overflow', p.overflow)[0].style.cssText += ';' + p.style
|
||
}
|
||
if (p.html && typeof p.html.render == 'function') {
|
||
p.html.render() // do not do .render(box);
|
||
}
|
||
}, 1)
|
||
} else {
|
||
// need to remove unnecessary classes
|
||
if (query(self.box).find(pname +'> .w2ui-panel-content').length > 0) {
|
||
query(self.box).find(pname +'> .w2ui-panel-content')
|
||
.removeClass()
|
||
.removeAttr('name')
|
||
.addClass('w2ui-panel-content')
|
||
.html(p.html)
|
||
.css('overflow', p.overflow)[0].style.cssText += ';' + p.style
|
||
}
|
||
}
|
||
// if there are tabs and/or toolbar - render it
|
||
let tmp = query(self.box).find(pname +'> .w2ui-panel-tabs')
|
||
if (p.show.tabs) {
|
||
if (tmp.find('[name='+ p.tabs.name +']').length === 0 && p.tabs != null) {
|
||
p.tabs.render(tmp.get(0))
|
||
} else {
|
||
p.tabs.refresh()
|
||
}
|
||
} else {
|
||
tmp.html('').removeClass('w2ui-tabs').hide()
|
||
}
|
||
tmp = query(self.box).find(pname +'> .w2ui-panel-toolbar')
|
||
if (p.show.toolbar) {
|
||
if (tmp.find('[name='+ p.toolbar.name +']').length === 0 && p.toolbar != null) {
|
||
p.toolbar.render(tmp.get(0))
|
||
} else {
|
||
p.toolbar.refresh()
|
||
}
|
||
} else {
|
||
tmp.html('').removeClass('w2ui-toolbar').hide()
|
||
}
|
||
// show title
|
||
tmp = query(self.box).find(pname +'> .w2ui-panel-title')
|
||
if (p.title) {
|
||
tmp.html(p.title).show()
|
||
} else {
|
||
tmp.html('').hide()
|
||
}
|
||
} else {
|
||
if (query(self.box).find('#layout_'+ self.name +'_panel_main').length === 0) {
|
||
self.render()
|
||
return
|
||
}
|
||
self.resize()
|
||
// refresh all of them
|
||
for (let p1 = 0; p1 < this.panels.length; p1++) { self.refresh(this.panels[p1].type) }
|
||
}
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
resize() {
|
||
// if (window.getSelection) window.getSelection().removeAllRanges(); // clear selection
|
||
if (!this.box) return false
|
||
let time = Date.now()
|
||
// event before
|
||
let tmp = this.last.resize
|
||
let edata = this.trigger('resize', { target: this.name,
|
||
panel: tmp ? tmp.type : 'all', diff_x: tmp ? tmp.diff_x : 0, diff_y: tmp ? tmp.diff_y : 0 })
|
||
if (edata.isCancelled === true) return
|
||
if (this.padding < 0) this.padding = 0
|
||
// layout itself
|
||
// width includes border and padding, we need to exclude that so panels
|
||
// are sized correctly
|
||
let width = w2utils.getSize(query(this.box), 'width')
|
||
let height = w2utils.getSize(query(this.box), 'height')
|
||
let self = this
|
||
// panels
|
||
let pmain = this.get('main')
|
||
let pprev = this.get('preview')
|
||
let pleft = this.get('left')
|
||
let pright = this.get('right')
|
||
let ptop = this.get('top')
|
||
let pbottom = this.get('bottom')
|
||
let sprev = (pprev != null && pprev.hidden !== true ? true : false)
|
||
let sleft = (pleft != null && pleft.hidden !== true ? true : false)
|
||
let sright = (pright != null && pright.hidden !== true ? true : false)
|
||
let stop = (ptop != null && ptop.hidden !== true ? true : false)
|
||
let sbottom = (pbottom != null && pbottom.hidden !== true ? true : false)
|
||
let l, t, w, h
|
||
// calculate %
|
||
for (let p = 0; p < w2panels.length; p++) {
|
||
if (w2panels[p] === 'main') continue
|
||
tmp = this.get(w2panels[p])
|
||
if (!tmp) continue
|
||
let str = String(tmp.size || 0)
|
||
if (str.substr(str.length-1) == '%') {
|
||
let tmph = height
|
||
if (tmp.type == 'preview') {
|
||
tmph = tmph -
|
||
(ptop && !ptop.hidden ? ptop.sizeCalculated : 0) -
|
||
(pbottom && !pbottom.hidden ? pbottom.sizeCalculated : 0)
|
||
}
|
||
tmp.sizeCalculated = parseInt((tmp.type == 'left' || tmp.type == 'right' ? width : tmph) * parseFloat(tmp.size) / 100)
|
||
} else {
|
||
tmp.sizeCalculated = parseInt(tmp.size)
|
||
}
|
||
tmp.sizeCalculated = Math.max(tmp.sizeCalculated, parseInt(tmp.minSize))
|
||
}
|
||
// negative size
|
||
if (String(pright.size).substr(0, 1) == '-') {
|
||
if (sleft && String(pleft.size).substr(0, 1) == '-') {
|
||
console.log('ERROR: you cannot have both left panel.size and right panel.size be negative.')
|
||
} else {
|
||
pright.sizeCalculated = width - (sleft ? pleft.sizeCalculated : 0) + parseInt(pright.size)
|
||
}
|
||
}
|
||
if (String(pleft.size).substr(0, 1) == '-') {
|
||
if (sright && String(pright.size).substr(0, 1) == '-') {
|
||
console.log('ERROR: you cannot have both left panel.size and right panel.size be negative.')
|
||
} else {
|
||
pleft.sizeCalculated = width - (sright ? pright.sizeCalculated : 0) + parseInt(pleft.size)
|
||
}
|
||
}
|
||
// top if any
|
||
if (ptop != null && ptop.hidden !== true) {
|
||
l = 0
|
||
t = 0
|
||
w = width
|
||
h = ptop.sizeCalculated
|
||
query(this.box).find('#layout_'+ this.name +'_panel_top')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
ptop.width = w
|
||
ptop.height = h
|
||
// resizer
|
||
if (ptop.resizable) {
|
||
t = ptop.sizeCalculated - (this.padding === 0 ? this.resizer : 0)
|
||
h = (this.resizer > this.padding ? this.resizer : this.padding)
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_top')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px',
|
||
'cursor': 'ns-resize'
|
||
})
|
||
.off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
event.preventDefault()
|
||
// event before
|
||
let edata = self.trigger('resizerClick', { target: 'top', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
w2ui[self.name].last.events.resizeStart('top', event)
|
||
// event after
|
||
edata.finish()
|
||
return false
|
||
})
|
||
}
|
||
} else {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_top').hide()
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_top').hide()
|
||
}
|
||
// left if any
|
||
if (pleft != null && pleft.hidden !== true) {
|
||
l = 0
|
||
t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0)
|
||
w = pleft.sizeCalculated
|
||
h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
|
||
(sbottom ? pbottom.sizeCalculated + this.padding : 0)
|
||
query(this.box).find('#layout_'+ this.name +'_panel_left')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
pleft.width = w
|
||
pleft.height = h
|
||
// resizer
|
||
if (pleft.resizable) {
|
||
l = pleft.sizeCalculated - (this.padding === 0 ? this.resizer : 0)
|
||
w = (this.resizer > this.padding ? this.resizer : this.padding)
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_left')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px',
|
||
'cursor': 'ew-resize'
|
||
})
|
||
.off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
event.preventDefault()
|
||
// event before
|
||
let edata = self.trigger('resizerClick', { target: 'left', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
w2ui[self.name].last.events.resizeStart('left', event)
|
||
// event after
|
||
edata.finish()
|
||
return false
|
||
})
|
||
}
|
||
} else {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_left').hide()
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_left').hide()
|
||
}
|
||
// right if any
|
||
if (pright != null && pright.hidden !== true) {
|
||
l = width - pright.sizeCalculated
|
||
t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0)
|
||
w = pright.sizeCalculated
|
||
h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
|
||
(sbottom ? pbottom.sizeCalculated + this.padding : 0)
|
||
query(this.box).find('#layout_'+ this.name +'_panel_right')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
pright.width = w
|
||
pright.height = h
|
||
// resizer
|
||
if (pright.resizable) {
|
||
l = l - this.padding
|
||
w = (this.resizer > this.padding ? this.resizer : this.padding)
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_right')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px',
|
||
'cursor': 'ew-resize'
|
||
})
|
||
.off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
event.preventDefault()
|
||
// event before
|
||
let edata = self.trigger('resizerClick', { target: 'right', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
w2ui[self.name].last.events.resizeStart('right', event)
|
||
// event after
|
||
edata.finish()
|
||
return false
|
||
})
|
||
}
|
||
} else {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_right').hide()
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_right').hide()
|
||
}
|
||
// bottom if any
|
||
if (pbottom != null && pbottom.hidden !== true) {
|
||
l = 0
|
||
t = height - pbottom.sizeCalculated
|
||
w = width
|
||
h = pbottom.sizeCalculated
|
||
query(this.box).find('#layout_'+ this.name +'_panel_bottom')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
pbottom.width = w
|
||
pbottom.height = h
|
||
// resizer
|
||
if (pbottom.resizable) {
|
||
t = t - (this.padding === 0 ? 0 : this.padding)
|
||
h = (this.resizer > this.padding ? this.resizer : this.padding)
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_bottom')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px',
|
||
'cursor': 'ns-resize'
|
||
})
|
||
.off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
event.preventDefault()
|
||
// event before
|
||
let edata = self.trigger('resizerClick', { target: 'bottom', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
w2ui[self.name].last.events.resizeStart('bottom', event)
|
||
// event after
|
||
edata.finish()
|
||
return false
|
||
})
|
||
}
|
||
} else {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_bottom').hide()
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_bottom').hide()
|
||
}
|
||
// main - always there
|
||
l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0)
|
||
t = 0 + (stop ? ptop.sizeCalculated + this.padding : 0)
|
||
w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) -
|
||
(sright ? pright.sizeCalculated + this.padding: 0)
|
||
h = height - (stop ? ptop.sizeCalculated + this.padding : 0) -
|
||
(sbottom ? pbottom.sizeCalculated + this.padding : 0) -
|
||
(sprev ? pprev.sizeCalculated + this.padding : 0)
|
||
query(this.box)
|
||
.find('#layout_'+ this.name +'_panel_main')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
pmain.width = w
|
||
pmain.height = h
|
||
// preview if any
|
||
if (pprev != null && pprev.hidden !== true) {
|
||
l = 0 + (sleft ? pleft.sizeCalculated + this.padding : 0)
|
||
t = height - (sbottom ? pbottom.sizeCalculated + this.padding : 0) - pprev.sizeCalculated
|
||
w = width - (sleft ? pleft.sizeCalculated + this.padding : 0) -
|
||
(sright ? pright.sizeCalculated + this.padding : 0)
|
||
h = pprev.sizeCalculated
|
||
query(this.box).find('#layout_'+ this.name +'_panel_preview')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px'
|
||
})
|
||
pprev.width = w
|
||
pprev.height = h
|
||
// resizer
|
||
if (pprev.resizable) {
|
||
t = t - (this.padding === 0 ? 0 : this.padding)
|
||
h = (this.resizer > this.padding ? this.resizer : this.padding)
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_preview')
|
||
.css({
|
||
'display': 'block',
|
||
'left': l + 'px',
|
||
'top': t + 'px',
|
||
'width': w + 'px',
|
||
'height': h + 'px',
|
||
'cursor': 'ns-resize'
|
||
})
|
||
.off('mousedown')
|
||
.on('mousedown', function(event) {
|
||
event.preventDefault()
|
||
// event before
|
||
let edata = self.trigger('resizerClick', { target: 'preview', originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
w2ui[self.name].last.events.resizeStart('preview', event)
|
||
// event after
|
||
edata.finish()
|
||
return false
|
||
})
|
||
}
|
||
} else {
|
||
query(this.box).find('#layout_'+ this.name +'_panel_preview').hide()
|
||
query(this.box).find('#layout_'+ this.name +'_resizer_preview').hide()
|
||
}
|
||
// display tabs and toolbar if needed
|
||
for (let p1 = 0; p1 < w2panels.length; p1++) {
|
||
let pan = this.get(w2panels[p1])
|
||
let tmp2 = '#layout_'+ this.name +'_panel_'+ w2panels[p1] +' > .w2ui-panel-'
|
||
let tabHeight = 0
|
||
if (pan) {
|
||
if (pan.title) {
|
||
let el = query(this.box).find(tmp2 + 'title').css({ top: tabHeight + 'px', display: 'block' })
|
||
tabHeight += w2utils.getSize(el, 'height')
|
||
}
|
||
if (pan.show.tabs) {
|
||
let el = query(this.box).find(tmp2 + 'tabs').css({ top: tabHeight + 'px', display: 'block' })
|
||
tabHeight += w2utils.getSize(el, 'height')
|
||
}
|
||
if (pan.show.toolbar) {
|
||
let el = query(this.box).find(tmp2 + 'toolbar').css({ top: tabHeight + 'px', display: 'block' })
|
||
tabHeight += w2utils.getSize(el, 'height')
|
||
}
|
||
}
|
||
query(this.box).find(tmp2 + 'content').css({ display: 'block' }).css({ top: tabHeight + 'px' })
|
||
}
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
if (w2ui[this.name] == null) return false
|
||
// clean up
|
||
if (query(this.box).find('#layout_'+ this.name +'_panel_main').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-layout')
|
||
.html('')
|
||
}
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
if (this.last.events && this.last.events.resize) {
|
||
query(window).off('resize', this.last.events.resize)
|
||
}
|
||
return true
|
||
}
|
||
lock(panel, msg, showSpinner) {
|
||
if (w2panels.indexOf(panel) == -1) {
|
||
console.log('ERROR: First parameter needs to be the a valid panel name.')
|
||
return
|
||
}
|
||
let args = Array.from(arguments)
|
||
args[0] = '#layout_'+ this.name + '_panel_' + panel
|
||
w2utils.lock(...args)
|
||
}
|
||
unlock(panel, speed) {
|
||
if (w2panels.indexOf(panel) == -1) {
|
||
console.log('ERROR: First parameter needs to be the a valid panel name.')
|
||
return
|
||
}
|
||
let nm = '#layout_'+ this.name + '_panel_' + panel
|
||
w2utils.unlock(nm, speed)
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: jQuery, w2utils, w2base, w2toolbar, w2field
|
||
*
|
||
* == TODO ==
|
||
* - problem with .set() and arrays, array get extended too, but should be replaced
|
||
* - allow functions in routeData (also add routeData to list/enum)
|
||
* - send parsed URL to the event if there is routeData
|
||
* - add selectType: 'none' so that no selection can be make but with mouse
|
||
* - focus/blur for selectType = cell not display grayed out selection
|
||
* - allow enum in inline edit (see https://github.com/vitmalina/w2ui/issues/911#issuecomment-107341193)
|
||
* - remote source, but localSort/localSearch
|
||
* - promise for request, load, save, etc.
|
||
* - onloadmore event (so it will be easy to implement remote data source with local sort)
|
||
* - status() - clears on next select, etc. Should not if it is off
|
||
*
|
||
* == DEMOS To create ==
|
||
* - batch for disabled buttons
|
||
* - natural sort
|
||
* - resize on max content
|
||
*
|
||
* == 2.0 changes
|
||
* - toolbarInput - deprecated, toolbarSearch stays
|
||
* - searchSuggest
|
||
* - searchSave, searchSelected, savedSearches, defaultSearches, useLocalStorage, searchFieldTooltip
|
||
* - cache, cacheSave
|
||
* - onSearchSave, onSearchRemove, onSearchSelect
|
||
* - show.searchLogic
|
||
* - show.searchSave
|
||
* - refreshSearch
|
||
* - initAllFields -> searchInitInput
|
||
* - textSearch - deprecated in favor of defaultOperator
|
||
* - grid.confirm - refactored
|
||
* - grid.message - refactored
|
||
* - search.type == 'text' can have 'in' and 'not in' operators, then it will switch to enum
|
||
* - grid.find(..., displayedOnly)
|
||
* - column.render(..., this) - added
|
||
* - observeResize for the box
|
||
* - remove edit.type == 'select'
|
||
* - editDone(...)
|
||
* - liveSearch
|
||
* - deprecated onUnselect event
|
||
* - requestComplete(data, action, callBack, resolve, reject) - new argument list
|
||
* - msgAJAXError -> msgHTTPError
|
||
* - aded msgServerError
|
||
* - deleted grid.method
|
||
* - added grid.prepareParams
|
||
* - added mouseEnter/mouseLeave
|
||
* - grid.show.columnReorder -> grid.reorderRows
|
||
* - updagte docs search.label (not search.text)
|
||
*/
|
||
|
||
class w2grid extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.name = null
|
||
this.box = null // HTML element that hold this element
|
||
this.columns = [] // { field, text, size, attr, render, hidden, gridMinWidth, editable }
|
||
this.columnGroups = [] // { span: int, text: 'string', main: true/false, style: 'string' }
|
||
this.records = [] // { recid: int(required), field1: 'value1', ... fieldN: 'valueN', style: 'string', changes: object }
|
||
this.summary = [] // array of summary records, same structure as records array
|
||
this.searches = [] // { type, label, field, attr, text, hidden }
|
||
this.toolbar = {} // if not empty object; then it is toolbar object
|
||
this.ranges = []
|
||
this.contextMenu = []
|
||
this.searchMap = {} // re-map search fields
|
||
this.searchData = []
|
||
this.sortMap = {} // re-map sort fields
|
||
this.sortData = []
|
||
this.savedSearches = []
|
||
this.defaultSearches = []
|
||
this.total = 0 // server total
|
||
this.recid = null // field from records to be used as recid
|
||
// internal
|
||
this.last = {
|
||
field : '', // last search field, e.g. 'all'
|
||
label : '', // last search field label, e.g. 'All Fields'
|
||
logic : 'AND', // last search logic, e.g. 'AND' or 'OR'
|
||
search : '', // last search text
|
||
searchIds : [], // last search IDs
|
||
selection : { // last selection details
|
||
indexes : [],
|
||
columns : {}
|
||
},
|
||
saved_sel : null, // last result of selectionSave()
|
||
multi : false, // last multi flag, true when searching for multiple fields
|
||
scrollTop : 0, // last scrollTop position
|
||
scrollLeft : 0, // last scrollLeft position
|
||
colStart : 0, // for column virtual scrolling
|
||
colEnd : 0, // for column virtual scrolling
|
||
fetch: {
|
||
action : '', // last fetch command, e.g. 'load'
|
||
offset : null, // last fetch offset, integer
|
||
start : 0, // timestamp of start of last fetch request
|
||
response : 0, // time it took to complete the last fetch request in seconds
|
||
options : null,
|
||
controller: null,
|
||
loaded : false, // data is loaded from the server
|
||
hasMore : false // flag to indicate if there are more items to pull from the server
|
||
},
|
||
pull_more : false,
|
||
pull_refresh : true,
|
||
range_start : null, // last range start cell
|
||
range_end : null, // last range end cell
|
||
sel_ind : null, // last selected cell index
|
||
sel_col : null, // last selected column
|
||
sel_type : null, // last selection type, e.g. 'click' or 'key'
|
||
sel_recid : null, // last selected record id
|
||
idCache : {}, // object, id cache for get()
|
||
move : null, // object, move details
|
||
cancelClick : null, // boolean flag to indicate if the click event should be ignored, set during mouseMove()
|
||
inEditMode : false, // flag to indicate if we're currently in edit mode during inline editing
|
||
_edit : null, // object with details on the last edited cell, { value, index, column, recid }
|
||
kbd_timer : null, // last id of blur() timer
|
||
marker_timer : null, // last id of markSearch() timer
|
||
click_time : null, // timestamp of last click
|
||
click_recid : null, // last clicked record id
|
||
bubbleEl : null, // last bubble element
|
||
colResizing : false, // flag to indicate that a column is currently being resized
|
||
tmp : null, // object with last column resizing details
|
||
copy_event : null, // last copy event
|
||
userSelect : '', // last user select type, e.g. 'text'
|
||
columnDrag : false, // false or an object with a remove() method
|
||
state : null, // last grid state
|
||
show_extra : 0, // last show extra for virtual scrolling
|
||
toolbar_height: 0, // height of grid's toolbar
|
||
}
|
||
this.header = ''
|
||
this.url = ''
|
||
this.limit = 100
|
||
this.offset = 0 // how many records to skip (for infinite scroll) when pulling from server
|
||
this.postData = {}
|
||
this.routeData = {}
|
||
this.httpHeaders = {}
|
||
this.show = {
|
||
header : false,
|
||
toolbar : false,
|
||
footer : false,
|
||
columnMenu : true,
|
||
columnHeaders : true,
|
||
lineNumbers : false,
|
||
expandColumn : false,
|
||
selectColumn : false,
|
||
emptyRecords : true,
|
||
toolbarReload : true,
|
||
toolbarColumns : false,
|
||
toolbarSearch : true,
|
||
toolbarAdd : false,
|
||
toolbarEdit : false,
|
||
toolbarDelete : false,
|
||
toolbarSave : false,
|
||
searchAll : true,
|
||
searchLogic : true,
|
||
searchHiddenMsg : false,
|
||
searchSave : true,
|
||
statusRange : true,
|
||
statusBuffered : false,
|
||
statusRecordID : true,
|
||
statusSelection : true,
|
||
statusResponse : true,
|
||
statusSort : false,
|
||
statusSearch : false,
|
||
recordTitles : false,
|
||
selectionBorder : true,
|
||
skipRecords : true,
|
||
saveRestoreState: true
|
||
}
|
||
this.stateId = null // Custom state name for stateSave, stateRestore and stateReset
|
||
this.hasFocus = false
|
||
this.autoLoad = true // for infinite scroll
|
||
this.fixedBody = true // if false; then grid grows with data
|
||
this.recordHeight = 32
|
||
this.lineNumberWidth = 34
|
||
this.keyboard = true
|
||
this.selectType = 'row' // can be row|cell
|
||
this.liveSearch = false // if true, it will auto search if typed in search_all
|
||
this.multiSearch = true
|
||
this.multiSelect = true
|
||
this.multiSort = true
|
||
this.reorderColumns = false
|
||
this.reorderRows = false
|
||
this.showExtraOnSearch = 0 // show extra records before and after on search
|
||
this.markSearch = true
|
||
this.columnTooltip = 'top|bottom' // can be top, bottom, left, right
|
||
this.disableCVS = false // disable Column Virtual Scroll
|
||
this.nestedFields = true // use field name containing dots as separator to look into object
|
||
this.vs_start = 150
|
||
this.vs_extra = 5
|
||
this.style = ''
|
||
this.tabIndex = null
|
||
this.dataType = null // if defined, then overwrites w2utils.settings.dataType
|
||
this.parser = null
|
||
this.advanceOnEdit = true // automatically begin editing the next cell after submitting an inline edit?
|
||
this.useLocalStorage = true
|
||
// default values for the column
|
||
this.colTemplate = {
|
||
text : '', // column text (can be a function)
|
||
field : '', // field name to map the column to a record
|
||
size : null, // size of column in px or %
|
||
min : 20, // minimum width of column in px
|
||
max : null, // maximum width of column in px
|
||
gridMinWidth : null, // minimum width of the grid when column is visible
|
||
sizeCorrected : null, // read only, corrected size (see explanation below)
|
||
sizeCalculated : null, // read only, size in px (see explanation below)
|
||
sizeOriginal : null, // size as defined
|
||
sizeType : null, // px or %
|
||
hidden : false, // indicates if column is hidden
|
||
sortable : false, // indicates if column is sortable
|
||
sortMode : null, // sort mode ('default'|'natural'|'i18n') or custom compare function
|
||
searchable : false, // bool/string: int,float,date,... or an object to create search field
|
||
resizable : true, // indicates if column is resizable
|
||
hideable : true, // indicates if column can be hidden
|
||
autoResize : null, // indicates if column can be auto-resized by double clicking on the resizer
|
||
attr : '', // string that will be inside the <td ... attr> tag
|
||
style : '', // additional style for the td tag
|
||
render : null, // string or render function
|
||
title : null, // string or function for the title property for the column cells
|
||
tooltip : null, // string for the title property for the column header
|
||
editable : {}, // editable object (see explanation below)
|
||
frozen : false, // indicates if the column is fixed to the left
|
||
info : null, // info bubble, can be bool/object
|
||
clipboardCopy : false, // if true (or string or function), it will display clipboard copy icon
|
||
}
|
||
// these column properties will be saved in stateSave()
|
||
this.stateColProps = {
|
||
text : false,
|
||
field : true,
|
||
size : true,
|
||
min : false,
|
||
max : false,
|
||
gridMinWidth : false,
|
||
sizeCorrected : false,
|
||
sizeCalculated : true,
|
||
sizeOriginal : true,
|
||
sizeType : true,
|
||
hidden : true,
|
||
sortable : false,
|
||
sortMode : true,
|
||
searchable : false,
|
||
resizable : false,
|
||
hideable : false,
|
||
autoResize : false,
|
||
attr : false,
|
||
style : false,
|
||
render : false,
|
||
title : false,
|
||
tooltip : false,
|
||
editable : false,
|
||
frozen : true,
|
||
info : false,
|
||
clipboardCopy : false
|
||
}
|
||
this.msgDelete = 'Are you sure you want to delete ${count} ${records}?'
|
||
this.msgNotJSON = 'Returned data is not in valid JSON format.'
|
||
this.msgHTTPError = 'HTTP error. See console for more details.'
|
||
this.msgServerError= 'Server error'
|
||
this.msgRefresh = 'Refreshing...'
|
||
this.msgNeedReload = 'Your remote data source record count has changed, reloading from the first record.'
|
||
this.msgEmpty = '' // if not blank, then it is message when server returns no records
|
||
this.buttons = {
|
||
'reload' : { type: 'button', id: 'w2ui-reload', icon: 'w2ui-icon-reload', tooltip: 'Reload data in the list' },
|
||
'columns' : { type: 'menu-check', id: 'w2ui-column-on-off', icon: 'w2ui-icon-columns', tooltip: 'Show/hide columns',
|
||
overlay: { align: 'none' }
|
||
},
|
||
'search' : { type: 'html', id: 'w2ui-search',
|
||
html: '<div class="w2ui-icon w2ui-icon-search w2ui-search-down w2ui-action" data-click="searchShowFields"></div>'
|
||
},
|
||
'add' : { type: 'button', id: 'w2ui-add', text: 'Add New', tooltip: 'Add new record', icon: 'w2ui-icon-plus' },
|
||
'edit' : { type: 'button', id: 'w2ui-edit', text: 'Edit', tooltip: 'Edit selected record', icon: 'w2ui-icon-pencil', batch: 1, disabled: true },
|
||
'delete' : { type: 'button', id: 'w2ui-delete', text: 'Delete', tooltip: 'Delete selected records', icon: 'w2ui-icon-cross', batch: true, disabled: true },
|
||
'save' : { type: 'button', id: 'w2ui-save', text: 'Save', tooltip: 'Save changed records', icon: 'w2ui-icon-check' }
|
||
}
|
||
this.operators = { // for search fields
|
||
'text' : ['is', 'begins', 'contains', 'ends'], // could have "in" and "not in"
|
||
'number' : ['=', 'between', '>', '<', '>=', '<='],
|
||
'date' : ['is', { oper: 'less', text: 'before'}, { oper: 'more', text: 'since' }, 'between'],
|
||
'list' : ['is'],
|
||
'hex' : ['is', 'between'],
|
||
'color' : ['is', 'begins', 'contains', 'ends'],
|
||
'enum' : ['in', 'not in']
|
||
// -- all possible
|
||
// "text" : ['is', 'begins', 'contains', 'ends'],
|
||
// "number" : ['is', 'between', 'less:less than', 'more:more than', 'null:is null', 'not null:is not null'],
|
||
// "list" : ['is', 'null:is null', 'not null:is not null'],
|
||
// "enum" : ['in', 'not in', 'null:is null', 'not null:is not null']
|
||
}
|
||
this.defaultOperator = {
|
||
'text' : 'begins',
|
||
'number' : '=',
|
||
'date' : 'is',
|
||
'list' : 'is',
|
||
'enum' : 'in',
|
||
'hex' : 'begins',
|
||
'color' : 'begins'
|
||
}
|
||
// map search field type to operator
|
||
this.operatorsMap = {
|
||
'text' : 'text',
|
||
'int' : 'number',
|
||
'float' : 'number',
|
||
'money' : 'number',
|
||
'currency' : 'number',
|
||
'percent' : 'number',
|
||
'hex' : 'hex',
|
||
'alphanumeric' : 'text',
|
||
'color' : 'color',
|
||
'date' : 'date',
|
||
'time' : 'date',
|
||
'datetime' : 'date',
|
||
'list' : 'list',
|
||
'combo' : 'text',
|
||
'enum' : 'enum',
|
||
'file' : 'enum',
|
||
'select' : 'list',
|
||
'radio' : 'list',
|
||
'checkbox' : 'list',
|
||
'toggle' : 'list'
|
||
}
|
||
// events
|
||
this.onAdd = null
|
||
this.onEdit = null
|
||
this.onRequest = null // called on any server event
|
||
this.onLoad = null
|
||
this.onDelete = null
|
||
this.onSave = null
|
||
this.onSelect = null
|
||
this.onClick = null
|
||
this.onDblClick = null
|
||
this.onContextMenu = null
|
||
this.onContextMenuClick = null // when context menu item selected
|
||
this.onColumnClick = null
|
||
this.onColumnDblClick = null
|
||
this.onColumnResize = null
|
||
this.onColumnAutoResize = null
|
||
this.onSort = null
|
||
this.onSearch = null
|
||
this.onSearchOpen = null
|
||
this.onChange = null // called when editable record is changed
|
||
this.onRestore = null // called when editable record is restored
|
||
this.onExpand = null
|
||
this.onCollapse = null
|
||
this.onError = null
|
||
this.onKeydown = null
|
||
this.onToolbar = null // all events from toolbar
|
||
this.onColumnOnOff = null
|
||
this.onCopy = null
|
||
this.onPaste = null
|
||
this.onSelectionExtend = null
|
||
this.onEditField = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onReload = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.onStateSave = null
|
||
this.onStateRestore = null
|
||
this.onFocus = null
|
||
this.onBlur = null
|
||
this.onReorderRow = null
|
||
this.onSearchSave = null
|
||
this.onSearchRemove = null
|
||
this.onSearchSelect = null
|
||
this.onColumnSelect = null
|
||
this.onColumnDragStart = null
|
||
this.onColumnDragEnd = null
|
||
this.onResizerDblClick = null
|
||
this.onMouseEnter = null // mouse enter over record event
|
||
this.onMouseLeave = null
|
||
// need deep merge, should be extend, not objectAssign
|
||
w2utils.extend(this, options)
|
||
// check if there are records without recid
|
||
if (Array.isArray(this.records)) {
|
||
let remove = [] // remove from records as they are summary
|
||
this.records.forEach((rec, ind) => {
|
||
if (rec[this.recid] != null) {
|
||
rec.recid = rec[this.recid]
|
||
}
|
||
if (rec.recid == null) {
|
||
console.log('ERROR: Cannot add records without recid. (obj: '+ this.name +')')
|
||
}
|
||
if (rec.w2ui?.summary === true) {
|
||
this.summary.push(rec)
|
||
remove.push(ind) // cannot remove here as it will mess up array walk thru
|
||
}
|
||
})
|
||
remove.sort()
|
||
for (let t = remove.length-1; t >= 0; t--) {
|
||
this.records.splice(remove[t], 1)
|
||
}
|
||
}
|
||
// add searches
|
||
if (Array.isArray(this.columns)) {
|
||
this.columns.forEach((col, ind) => {
|
||
col = w2utils.extend({}, this.colTemplate, col)
|
||
this.columns[ind] = col
|
||
let search = col.searchable
|
||
if (search == null || search === false || this.getSearch(col.field) != null) return
|
||
if (w2utils.isPlainObject(search)) {
|
||
this.addSearch(w2utils.extend({ field: col.field, label: col.text, type: 'text' }, search))
|
||
} else {
|
||
let stype = col.searchable
|
||
let attr = ''
|
||
if (col.searchable === true) {
|
||
stype = 'text'
|
||
attr = 'size="20"'
|
||
}
|
||
this.addSearch({ field: col.field, label: col.text, type: stype, attr: attr })
|
||
}
|
||
})
|
||
}
|
||
// add icon to default searches if not defined
|
||
if (Array.isArray(this.defaultSearches)) {
|
||
this.defaultSearches.forEach((search, ind) => {
|
||
search.id = 'default-'+ ind
|
||
search.icon ??= 'w2ui-icon-search'
|
||
})
|
||
}
|
||
// check if there are saved searches in localStorage
|
||
let data = this.cache('searches')
|
||
if (Array.isArray(data)) {
|
||
data.forEach(search => {
|
||
this.savedSearches.push({
|
||
id: search.id ?? 'none',
|
||
text: search.text ?? 'none',
|
||
icon: 'w2ui-icon-search',
|
||
remove: true,
|
||
logic: search.logic ?? 'AND',
|
||
data: search.data ?? []
|
||
})
|
||
})
|
||
}
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
}
|
||
add(record, first) {
|
||
if (!Array.isArray(record)) record = [record]
|
||
let added = 0
|
||
for (let i = 0; i < record.length; i++) {
|
||
let rec = record[i]
|
||
if (rec[this.recid] != null) {
|
||
rec.recid = rec[this.recid]
|
||
}
|
||
if (rec.recid == null) {
|
||
console.log('ERROR: Cannot add record without recid. (obj: '+ this.name +')')
|
||
continue
|
||
}
|
||
if (rec.w2ui?.summary === true) {
|
||
if (first) this.summary.unshift(rec); else this.summary.push(rec)
|
||
} else {
|
||
if (first) this.records.unshift(rec); else this.records.push(rec)
|
||
}
|
||
added++
|
||
}
|
||
let url = this.url?.get ?? this.url
|
||
if (!url) {
|
||
this.total = this.records.length
|
||
this.localSort(false, true)
|
||
this.localSearch()
|
||
// do not call this.refresh(), this is unnecessary, heavy, and messes with the toolbar.
|
||
// this.refreshBody()
|
||
// this.resizeRecords()
|
||
this.refresh()
|
||
} else {
|
||
this.refresh() // ?? should it be reload?
|
||
}
|
||
return added
|
||
}
|
||
find(obj, returnIndex, displayedOnly) {
|
||
if (obj == null) obj = {}
|
||
let recs = []
|
||
let hasDots = false
|
||
// check if property is nested - needed for speed
|
||
for (let o in obj) if (String(o).indexOf('.') != -1) hasDots = true
|
||
// look for an item
|
||
let start = displayedOnly ? this.last.range_start : 0
|
||
let end = displayedOnly ? this.last.range_end + 1: this.records.length
|
||
if (end > this.records.length) end = this.records.length
|
||
for (let i = start; i < end; i++) {
|
||
let match = true
|
||
for (let o in obj) {
|
||
let val = this.records[i][o]
|
||
if (hasDots && String(o).indexOf('.') != -1) val = this.parseField(this.records[i], o)
|
||
if (obj[o] == 'not-null') {
|
||
if (val == null || val === '') match = false
|
||
} else {
|
||
if (obj[o] != val) match = false
|
||
}
|
||
}
|
||
if (match && returnIndex !== true) recs.push(this.records[i].recid)
|
||
if (match && returnIndex === true) recs.push(i)
|
||
}
|
||
return recs
|
||
}
|
||
set(recid, record, noRefresh) { // does not delete existing, but overrides on top of it
|
||
if ((typeof recid == 'object') && (recid !== null)) {
|
||
noRefresh = record
|
||
record = recid
|
||
recid = null
|
||
}
|
||
// update all records
|
||
if (recid == null) {
|
||
for (let i = 0; i < this.records.length; i++) {
|
||
w2utils.extend(this.records[i], record) // recid is the whole record
|
||
}
|
||
if (noRefresh !== true) this.refresh()
|
||
} else { // find record to update
|
||
let ind = this.get(recid, true)
|
||
if (ind == null) return false
|
||
let isSummary = (this.records[ind] && this.records[ind].recid == recid ? false : true)
|
||
if (isSummary) {
|
||
w2utils.extend(this.summary[ind], record)
|
||
} else {
|
||
w2utils.extend(this.records[ind], record)
|
||
}
|
||
if (noRefresh !== true) this.refreshRow(recid, ind) // refresh only that record
|
||
}
|
||
return true
|
||
}
|
||
get(recid, returnIndex) {
|
||
// search records
|
||
if (Array.isArray(recid)) {
|
||
let recs = []
|
||
for (let i = 0; i < recid.length; i++) {
|
||
let v = this.get(recid[i], returnIndex)
|
||
if (v !== null)
|
||
recs.push(v)
|
||
}
|
||
return recs
|
||
} else {
|
||
// get() must be fast, implements a cache to bypass loop over all records
|
||
// most of the time.
|
||
let idCache = this.last.idCache
|
||
if (!idCache) {
|
||
this.last.idCache = idCache = {}
|
||
}
|
||
let i = idCache[recid]
|
||
if (typeof(i) === 'number') {
|
||
if (i >= 0 && i < this.records.length && this.records[i].recid == recid) {
|
||
if (returnIndex === true) return i; else return this.records[i]
|
||
}
|
||
// summary indexes are stored as negative numbers, try them now.
|
||
i = ~i
|
||
if (i >= 0 && i < this.summary.length && this.summary[i].recid == recid) {
|
||
if (returnIndex === true) return i; else return this.summary[i]
|
||
}
|
||
// wrong index returned, clear cache
|
||
this.last.idCache = idCache = {}
|
||
}
|
||
for (let i = 0; i < this.records.length; i++) {
|
||
if (this.records[i].recid == recid) {
|
||
idCache[recid] = i
|
||
if (returnIndex === true) return i; else return this.records[i]
|
||
}
|
||
}
|
||
// search summary
|
||
for (let i = 0; i < this.summary.length; i++) {
|
||
if (this.summary[i].recid == recid) {
|
||
idCache[recid] = ~i
|
||
if (returnIndex === true) return i; else return this.summary[i]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
}
|
||
getFirst(offset) {
|
||
if (this.records.length == 0) return null
|
||
let rec = this.records[0]
|
||
let tmp = this.last.searchIds
|
||
if (this.searchData.length > 0) {
|
||
if (Array.isArray(tmp) && tmp.length > 0) {
|
||
rec = this.records[tmp[offset || 0]]
|
||
} else {
|
||
rec = null
|
||
}
|
||
}
|
||
return rec
|
||
}
|
||
remove() {
|
||
let removed = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.records.length-1; r >= 0; r--) {
|
||
if (this.records[r].recid == arguments[a]) { this.records.splice(r, 1); removed++ }
|
||
}
|
||
for (let r = this.summary.length-1; r >= 0; r--) {
|
||
if (this.summary[r].recid == arguments[a]) { this.summary.splice(r, 1); removed++ }
|
||
}
|
||
}
|
||
let url = this.url?.get ?? this.url
|
||
if (!url) {
|
||
this.localSort(false, true)
|
||
this.localSearch()
|
||
}
|
||
this.refresh()
|
||
return removed
|
||
}
|
||
addColumn(before, columns) {
|
||
let added = 0
|
||
if (arguments.length == 1) {
|
||
columns = before
|
||
before = this.columns.length
|
||
} else {
|
||
if (typeof before == 'string') before = this.getColumn(before, true)
|
||
if (before == null) before = this.columns.length
|
||
}
|
||
if (!Array.isArray(columns)) columns = [columns]
|
||
for (let i = 0; i < columns.length; i++) {
|
||
let col = w2utils.extend({}, this.colTemplate, columns[i])
|
||
this.columns.splice(before, 0, col)
|
||
// if column is searchable, add search field
|
||
if (columns[i].searchable) {
|
||
let stype = columns[i].searchable
|
||
let attr = ''
|
||
if (columns[i].searchable === true) { stype = 'text'; attr = 'size="20"' }
|
||
this.addSearch({ field: columns[i].field, label: columns[i].text, type: stype, attr: attr })
|
||
}
|
||
before++
|
||
added++
|
||
}
|
||
this.refresh()
|
||
return added
|
||
}
|
||
removeColumn() {
|
||
let removed = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.columns.length-1; r >= 0; r--) {
|
||
if (this.columns[r].field == arguments[a]) {
|
||
if (this.columns[r].searchable) this.removeSearch(arguments[a])
|
||
this.columns.splice(r, 1)
|
||
removed++
|
||
}
|
||
}
|
||
}
|
||
this.refresh()
|
||
return removed
|
||
}
|
||
getColumn(field, returnIndex) {
|
||
// no arguments - return fields of all columns
|
||
if (arguments.length === 0) {
|
||
let ret = []
|
||
for (let i = 0; i < this.columns.length; i++) ret.push(this.columns[i].field)
|
||
return ret
|
||
}
|
||
// find column
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
if (this.columns[i].field == field) {
|
||
if (returnIndex === true) return i; else return this.columns[i]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
updateColumn(fields, updates) {
|
||
let effected = 0
|
||
fields = (Array.isArray(fields) ? fields : [fields])
|
||
fields.forEach((colName) => {
|
||
this.columns.forEach((col) => {
|
||
if (col.field == colName) {
|
||
let _updates = w2utils.clone(updates)
|
||
Object.keys(_updates).forEach((key) => {
|
||
// if it is a function
|
||
if (typeof _updates[key] == 'function') {
|
||
_updates[key] = _updates[key](col)
|
||
}
|
||
if (col[key] != _updates[key]) effected++
|
||
})
|
||
w2utils.extend(col, _updates)
|
||
}
|
||
})
|
||
})
|
||
if (effected > 0) {
|
||
this.refresh() // need full refresh due to colgroups not reassigning properly
|
||
}
|
||
return effected
|
||
}
|
||
toggleColumn() {
|
||
return this.updateColumn(Array.from(arguments), { hidden(col) { return !col.hidden } })
|
||
}
|
||
showColumn() {
|
||
return this.updateColumn(Array.from(arguments), { hidden: false })
|
||
}
|
||
hideColumn() {
|
||
return this.updateColumn(Array.from(arguments), { hidden: true })
|
||
}
|
||
addSearch(before, search) {
|
||
let added = 0
|
||
if (arguments.length == 1) {
|
||
search = before
|
||
before = this.searches.length
|
||
} else {
|
||
if (typeof before == 'string') before = this.getSearch(before, true)
|
||
if (before == null) before = this.searches.length
|
||
}
|
||
if (!Array.isArray(search)) search = [search]
|
||
for (let i = 0; i < search.length; i++) {
|
||
this.searches.splice(before, 0, search[i])
|
||
before++
|
||
added++
|
||
}
|
||
this.searchClose()
|
||
return added
|
||
}
|
||
removeSearch() {
|
||
let removed = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.searches.length-1; r >= 0; r--) {
|
||
if (this.searches[r].field == arguments[a]) { this.searches.splice(r, 1); removed++ }
|
||
}
|
||
}
|
||
this.searchClose()
|
||
return removed
|
||
}
|
||
getSearch(field, returnIndex) {
|
||
// no arguments - return fields of all searches
|
||
if (arguments.length === 0) {
|
||
let ret = []
|
||
for (let i = 0; i < this.searches.length; i++) ret.push(this.searches[i].field)
|
||
return ret
|
||
}
|
||
// find search
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
if (this.searches[i].field == field) {
|
||
if (returnIndex === true) return i; else return this.searches[i]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
toggleSearch() {
|
||
let effected = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.searches.length-1; r >= 0; r--) {
|
||
if (this.searches[r].field == arguments[a]) {
|
||
this.searches[r].hidden = !this.searches[r].hidden
|
||
effected++
|
||
}
|
||
}
|
||
}
|
||
this.searchClose()
|
||
return effected
|
||
}
|
||
showSearch() {
|
||
let shown = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.searches.length-1; r >= 0; r--) {
|
||
if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== false) {
|
||
this.searches[r].hidden = false
|
||
shown++
|
||
}
|
||
}
|
||
}
|
||
this.searchClose()
|
||
return shown
|
||
}
|
||
hideSearch() {
|
||
let hidden = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
for (let r = this.searches.length-1; r >= 0; r--) {
|
||
if (this.searches[r].field == arguments[a] && this.searches[r].hidden !== true) {
|
||
this.searches[r].hidden = true
|
||
hidden++
|
||
}
|
||
}
|
||
}
|
||
this.searchClose()
|
||
return hidden
|
||
}
|
||
getSearchData(field) {
|
||
for (let i = 0; i < this.searchData.length; i++) {
|
||
if (this.searchData[i].field == field) return this.searchData[i]
|
||
}
|
||
return null
|
||
}
|
||
localSort(silent, noResetRefresh) {
|
||
let obj = this
|
||
let url = this.url?.get ?? this.url
|
||
if (url) {
|
||
console.log('ERROR: grid.localSort can only be used on local data source, grid.url should be empty.')
|
||
return
|
||
}
|
||
if (Object.keys(this.sortData).length === 0) return
|
||
let time = Date.now()
|
||
// process date fields
|
||
this.selectionSave()
|
||
this.prepareData()
|
||
if (!noResetRefresh) {
|
||
this.reset()
|
||
}
|
||
// process sortData
|
||
for (let i = 0; i < this.sortData.length; i++) {
|
||
let column = this.getColumn(this.sortData[i].field)
|
||
if (!column) return // TODO: ability to sort columns when they are not part of colums array
|
||
if (typeof column.render == 'string') {
|
||
if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) {
|
||
this.sortData[i].field_ = column.field + '_'
|
||
}
|
||
if (['time'].indexOf(column.render.split(':')[0]) != -1) {
|
||
this.sortData[i].field_ = column.field + '_'
|
||
}
|
||
}
|
||
}
|
||
// prepare paths and process sort
|
||
preparePaths()
|
||
this.records.sort((a, b) => {
|
||
return compareRecordPaths(a, b)
|
||
})
|
||
cleanupPaths()
|
||
this.selectionRestore(noResetRefresh)
|
||
time = Date.now() - time
|
||
if (silent !== true && this.show.statusSort) {
|
||
setTimeout(() => {
|
||
this.status(w2utils.lang('Sorting took ${count} seconds', { count: time/1000 }))
|
||
}, 10)
|
||
}
|
||
return time
|
||
// grab paths before sorting for efficiency and because calling obj.get()
|
||
// while sorting 'obj.records' is unsafe, at least on webkit
|
||
function preparePaths() {
|
||
for (let i = 0; i < obj.records.length; i++) {
|
||
let rec = obj.records[i]
|
||
if (rec.w2ui?.parent_recid != null) {
|
||
rec.w2ui._path = getRecordPath(rec)
|
||
}
|
||
}
|
||
}
|
||
// cleanup and release memory allocated by preparePaths()
|
||
function cleanupPaths() {
|
||
for (let i = 0; i < obj.records.length; i++) {
|
||
let rec = obj.records[i]
|
||
if (rec.w2ui?.parent_recid != null) {
|
||
rec.w2ui._path = null
|
||
}
|
||
}
|
||
}
|
||
// compare two paths, from root of tree to given records
|
||
function compareRecordPaths(a, b) {
|
||
if ((!a.w2ui || a.w2ui.parent_recid == null) && (!b.w2ui || b.w2ui.parent_recid == null)) {
|
||
return compareRecords(a, b) // no tree, fast path
|
||
}
|
||
let pa = getRecordPath(a)
|
||
let pb = getRecordPath(b)
|
||
for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
|
||
let diff = compareRecords(pa[i], pb[i])
|
||
if (diff !== 0) return diff // different subpath
|
||
}
|
||
if (pa.length > pb.length) return 1
|
||
if (pa.length < pb.length) return -1
|
||
console.log('ERROR: two paths should not be equal.')
|
||
return 0
|
||
}
|
||
// return an array of all records from root to and including 'rec'
|
||
function getRecordPath(rec) {
|
||
if (!rec.w2ui || rec.w2ui.parent_recid == null) return [rec]
|
||
if (rec.w2ui._path)
|
||
return rec.w2ui._path
|
||
// during actual sort, we should never reach this point
|
||
let subrec = obj.get(rec.w2ui.parent_recid)
|
||
if (!subrec) {
|
||
console.log('ERROR: no parent record: ' + rec.w2ui.parent_recid)
|
||
return [rec]
|
||
}
|
||
return (getRecordPath(subrec).concat(rec))
|
||
}
|
||
// compare two records according to sortData and finally recid
|
||
function compareRecords(a, b) {
|
||
if (a === b) return 0 // optimize, same object
|
||
for (let i = 0; i < obj.sortData.length; i++) {
|
||
let fld = obj.sortData[i].field
|
||
let sortFld = (obj.sortData[i].field_) ? obj.sortData[i].field_ : fld
|
||
let aa = a[sortFld]
|
||
let bb = b[sortFld]
|
||
if (String(fld).indexOf('.') != -1) {
|
||
aa = obj.parseField(a, sortFld)
|
||
bb = obj.parseField(b, sortFld)
|
||
}
|
||
let col = obj.getColumn(fld)
|
||
if (col && Object.keys(col.editable).length > 0) { // for drop editable fields and drop downs
|
||
if (w2utils.isPlainObject(aa) && aa.text) aa = aa.text
|
||
if (w2utils.isPlainObject(bb) && bb.text) bb = bb.text
|
||
}
|
||
let ret = compareCells(aa, bb, i, obj.sortData[i].direction, col.sortMode || 'default')
|
||
if (ret !== 0) return ret
|
||
}
|
||
// break tie for similar records,
|
||
// required to have consistent ordering for tree paths
|
||
let ret = compareCells(a.recid, b.recid, -1, 'asc')
|
||
return ret
|
||
}
|
||
// compare two values, aa and bb, producing consistent ordering
|
||
function compareCells(aa, bb, i, direction, sortMode) {
|
||
// if both objects are strictly equal, we're done
|
||
if (aa === bb)
|
||
return 0
|
||
// all nulls, empty and undefined on bottom
|
||
if ((aa == null || aa === '') && (bb != null && bb !== ''))
|
||
return 1
|
||
if ((aa != null && aa !== '') && (bb == null || bb === ''))
|
||
return -1
|
||
let dir = (direction.toLowerCase() === 'asc') ? 1 : -1
|
||
// for different kind of objects, sort by object type
|
||
if (typeof aa != typeof bb)
|
||
return (typeof aa > typeof bb) ? dir : -dir
|
||
// for different kind of classes, sort by classes
|
||
if (aa.constructor.name != bb.constructor.name)
|
||
return (aa.constructor.name > bb.constructor.name) ? dir : -dir
|
||
// if we're dealing with non-null objects, call valueOf().
|
||
// this mean that Date() or custom objects will compare properly.
|
||
if (aa && typeof aa == 'object')
|
||
aa = aa.valueOf()
|
||
if (bb && typeof bb == 'object')
|
||
bb = bb.valueOf()
|
||
// if we're still dealing with non-null objects that have
|
||
// a useful Object => String conversion, convert to string.
|
||
let defaultToString = {}.toString
|
||
if (aa && typeof aa == 'object' && aa.toString != defaultToString)
|
||
aa = String(aa)
|
||
if (bb && typeof bb == 'object' && bb.toString != defaultToString)
|
||
bb = String(bb)
|
||
// do case-insensitive string comparison
|
||
if (typeof aa == 'string')
|
||
aa = aa.toLowerCase().trim()
|
||
if (typeof bb == 'string')
|
||
bb = bb.toLowerCase().trim()
|
||
switch (sortMode) {
|
||
case 'natural':
|
||
sortMode = w2utils.naturalCompare
|
||
break
|
||
case 'i18n':
|
||
sortMode = w2utils.i18nCompare
|
||
break
|
||
}
|
||
if (typeof sortMode == 'function') {
|
||
return sortMode(aa,bb) * dir
|
||
}
|
||
// compare both objects
|
||
if (aa > bb)
|
||
return dir
|
||
if (aa < bb)
|
||
return -dir
|
||
return 0
|
||
}
|
||
}
|
||
localSearch(silent) {
|
||
let obj = this
|
||
let url = this.url?.get ?? this.url
|
||
if (url) {
|
||
console.log('ERROR: grid.localSearch can only be used on local data source, grid.url should be empty.')
|
||
return
|
||
}
|
||
let time = Date.now()
|
||
let defaultToString = {}.toString
|
||
let duplicateMap = {}
|
||
this.total = this.records.length
|
||
// mark all records as shown
|
||
this.last.searchIds = []
|
||
// prepare date/time fields
|
||
this.prepareData()
|
||
// hide records that did not match
|
||
if (this.searchData.length > 0 && !url) {
|
||
this.total = 0
|
||
for (let i = 0; i < this.records.length; i++) {
|
||
let rec = this.records[i]
|
||
let match = searchRecord(rec)
|
||
if (match) {
|
||
if (rec?.w2ui) addParent(rec.w2ui.parent_recid)
|
||
if (this.showExtraOnSearch > 0) {
|
||
let before = this.showExtraOnSearch
|
||
let after = this.showExtraOnSearch
|
||
if (i < before) before = i
|
||
if (i + after > this.records.length) after = this.records.length - i
|
||
if (before > 0) {
|
||
for (let j = i - before; j < i; j++) {
|
||
if (this.last.searchIds.indexOf(j) < 0)
|
||
this.last.searchIds.push(j)
|
||
}
|
||
}
|
||
if (this.last.searchIds.indexOf(i) < 0) this.last.searchIds.push(i)
|
||
if (after > 0) {
|
||
for (let j = (i + 1) ; j <= (i + after) ; j++) {
|
||
if (this.last.searchIds.indexOf(j) < 0) this.last.searchIds.push(j)
|
||
}
|
||
}
|
||
} else {
|
||
this.last.searchIds.push(i)
|
||
}
|
||
}
|
||
}
|
||
this.total = this.last.searchIds.length
|
||
}
|
||
time = Date.now() - time
|
||
if (silent !== true && this.show.statusSearch) {
|
||
setTimeout(() => {
|
||
this.status(w2utils.lang('Search took ${count} seconds', { count: time/1000 }))
|
||
}, 10)
|
||
}
|
||
return time
|
||
// check if a record (or one of its closed children) matches the search data
|
||
function searchRecord(rec) {
|
||
let fl = 0, val1, val2, val3, tmp
|
||
let orEqual = false
|
||
for (let j = 0; j < obj.searchData.length; j++) {
|
||
let sdata = obj.searchData[j]
|
||
let search = obj.getSearch(sdata.field)
|
||
if (sdata == null) continue
|
||
if (search == null) search = { field: sdata.field, type: sdata.type }
|
||
let val1b = obj.parseField(rec, search.field)
|
||
val1 = (val1b !== null && val1b !== undefined &&
|
||
(typeof val1b != 'object' || val1b.toString != defaultToString)) ?
|
||
String(val1b).toLowerCase() : '' // do not match a bogus string
|
||
if (sdata.value != null) {
|
||
if (!Array.isArray(sdata.value)) {
|
||
val2 = String(sdata.value).toLowerCase()
|
||
} else {
|
||
val2 = sdata.value[0]
|
||
val3 = sdata.value[1]
|
||
}
|
||
}
|
||
switch (sdata.operator) {
|
||
case '=':
|
||
case 'is':
|
||
if (obj.parseField(rec, search.field) == sdata.value) fl++ // do not hide record
|
||
else if (search.type == 'date') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatDate(tmp, 'yyyy-mm-dd')
|
||
val2 = w2utils.formatDate(w2utils.isDate(val2, w2utils.settings.dateFormat, true), 'yyyy-mm-dd')
|
||
if (val1 == val2) fl++
|
||
}
|
||
else if (search.type == 'time') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatTime(tmp, 'hh24:mi')
|
||
val2 = w2utils.formatTime(val2, 'hh24:mi')
|
||
if (val1 == val2) fl++
|
||
}
|
||
else if (search.type == 'datetime') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss')
|
||
val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss')
|
||
if (val1 == val2) fl++
|
||
}
|
||
break
|
||
case 'between':
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
|
||
if (parseFloat(obj.parseField(rec, search.field)) >= parseFloat(val2) && parseFloat(obj.parseField(rec, search.field)) <= parseFloat(val3)) fl++
|
||
}
|
||
else if (search.type == 'date') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true)
|
||
val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true)
|
||
val3 = w2utils.isDate(val3, w2utils.settings.dateFormat, true)
|
||
if (val3 != null) val3 = new Date(val3.getTime() + 86400000) // 1 day
|
||
if (val1 >= val2 && val1 < val3) fl++
|
||
}
|
||
else if (search.type == 'time') {
|
||
val1 = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val2 = w2utils.isTime(val2, true)
|
||
val3 = w2utils.isTime(val3, true)
|
||
val2 = (new Date()).setHours(val2.hours, val2.minutes, val2.seconds ? val2.seconds : 0, 0)
|
||
val3 = (new Date()).setHours(val3.hours, val3.minutes, val3.seconds ? val3.seconds : 0, 0)
|
||
if (val1 >= val2 && val1 < val3) fl++
|
||
}
|
||
else if (search.type == 'datetime') {
|
||
val1 = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val2 = w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true)
|
||
val3 = w2utils.isDateTime(val3, w2utils.settings.datetimeFormat, true)
|
||
if (val3) val3 = new Date(val3.getTime() + 86400000) // 1 day
|
||
if (val1 >= val2 && val1 < val3) fl++
|
||
}
|
||
break
|
||
case '<=':
|
||
orEqual = true
|
||
case '<':
|
||
case 'less':
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
|
||
val1 = parseFloat(obj.parseField(rec, search.field))
|
||
val2 = parseFloat(sdata.value)
|
||
if (val1 < val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'date') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true)
|
||
val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true)
|
||
if (val1 < val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'time') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatTime(tmp, 'hh24:mi')
|
||
val2 = w2utils.formatTime(val2, 'hh24:mi')
|
||
if (val1 < val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'datetime') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss')
|
||
val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss')
|
||
if (val1.length == val2.length && (val1 < val2 || (orEqual && val1 === val2))) fl++
|
||
}
|
||
break
|
||
case '>=':
|
||
orEqual = true
|
||
case '>':
|
||
case 'more':
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
|
||
val1 = parseFloat(obj.parseField(rec, search.field))
|
||
val2 = parseFloat(sdata.value)
|
||
if (val1 > val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'date') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.isDate(tmp, w2utils.settings.dateFormat, true)
|
||
val2 = w2utils.isDate(val2, w2utils.settings.dateFormat, true)
|
||
if (val1 > val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'time') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatTime(tmp, 'hh24:mi')
|
||
val2 = w2utils.formatTime(val2, 'hh24:mi')
|
||
if (val1 > val2 || (orEqual && val1 === val2)) fl++
|
||
}
|
||
else if (search.type == 'datetime') {
|
||
tmp = (obj.parseField(rec, search.field + '_') instanceof Date ? obj.parseField(rec, search.field + '_') : obj.parseField(rec, search.field))
|
||
val1 = w2utils.formatDateTime(tmp, 'yyyy-mm-dd|hh24:mm:ss')
|
||
val2 = w2utils.formatDateTime(w2utils.isDateTime(val2, w2utils.settings.datetimeFormat, true), 'yyyy-mm-dd|hh24:mm:ss')
|
||
if (val1.length == val2.length && (val1 > val2 || (orEqual && val1 === val2))) fl++
|
||
}
|
||
break
|
||
case 'in':
|
||
tmp = sdata.value
|
||
if (sdata.svalue) tmp = sdata.svalue
|
||
if ((tmp.indexOf(w2utils.isFloat(val1b) ? parseFloat(val1b) : val1b) !== -1) || tmp.indexOf(val1) !== -1) fl++
|
||
break
|
||
case 'not in':
|
||
tmp = sdata.value
|
||
if (sdata.svalue) tmp = sdata.svalue
|
||
if (!((tmp.indexOf(w2utils.isFloat(val1b) ? parseFloat(val1b) : val1b) !== -1) || tmp.indexOf(val1) !== -1)) fl++
|
||
break
|
||
case 'begins':
|
||
case 'begins with': // need for back compatibility
|
||
if (val1.indexOf(val2) === 0) fl++ // do not hide record
|
||
break
|
||
case 'contains':
|
||
if (val1.indexOf(val2) >= 0) fl++ // do not hide record
|
||
break
|
||
case 'null':
|
||
if (obj.parseField(rec, search.field) == null) fl++ // do not hide record
|
||
break
|
||
case 'not null':
|
||
if (obj.parseField(rec, search.field) != null) fl++ // do not hide record
|
||
break
|
||
case 'ends':
|
||
case 'ends with': // need for back compatibility
|
||
let lastIndex = val1.lastIndexOf(val2)
|
||
if (lastIndex !== -1 && lastIndex == val1.length - val2.length) fl++ // do not hide record
|
||
break
|
||
}
|
||
}
|
||
if ((obj.last.logic == 'OR' && fl !== 0) ||
|
||
(obj.last.logic == 'AND' && fl == obj.searchData.length))
|
||
return true
|
||
if (rec.w2ui?.children && rec.w2ui?.expanded !== true) {
|
||
// there are closed children, search them too.
|
||
for (let r = 0; r < rec.w2ui.children.length; r++) {
|
||
let subRec = rec.w2ui.children[r]
|
||
if (searchRecord(subRec))
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
// add parents nodes recursively
|
||
function addParent(recid) {
|
||
let i = obj.get(recid, true)
|
||
if (i == null || recid == null || duplicateMap[recid] || obj.last.searchIds.includes(i)) {
|
||
return
|
||
}
|
||
duplicateMap[recid] = true
|
||
let rec = obj.records[i]
|
||
if (rec?.w2ui) {
|
||
addParent(rec.w2ui.parent_recid)
|
||
}
|
||
obj.last.searchIds.push(i)
|
||
}
|
||
}
|
||
getRangeData(range, extra) {
|
||
let rec1 = this.get(range[0].recid, true)
|
||
let rec2 = this.get(range[1].recid, true)
|
||
let col1 = range[0].column
|
||
let col2 = range[1].column
|
||
let res = []
|
||
if (col1 == col2) { // one row
|
||
for (let r = rec1; r <= rec2; r++) {
|
||
let record = this.records[r]
|
||
let dt = record[this.columns[col1].field] || null
|
||
if (extra !== true) {
|
||
res.push(dt)
|
||
} else {
|
||
res.push({ data: dt, column: col1, index: r, record: record })
|
||
}
|
||
}
|
||
} else if (rec1 == rec2) { // one line
|
||
let record = this.records[rec1]
|
||
for (let i = col1; i <= col2; i++) {
|
||
let dt = record[this.columns[i].field] || null
|
||
if (extra !== true) {
|
||
res.push(dt)
|
||
} else {
|
||
res.push({ data: dt, column: i, index: rec1, record: record })
|
||
}
|
||
}
|
||
} else {
|
||
for (let r = rec1; r <= rec2; r++) {
|
||
let record = this.records[r]
|
||
res.push([])
|
||
for (let i = col1; i <= col2; i++) {
|
||
let dt = record[this.columns[i].field]
|
||
if (extra !== true) {
|
||
res[res.length-1].push(dt)
|
||
} else {
|
||
res[res.length-1].push({ data: dt, column: i, index: r, record: record })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
addRange(ranges) {
|
||
let added = 0, first, last
|
||
if (this.selectType == 'row') return added
|
||
if (!Array.isArray(ranges)) ranges = [ranges]
|
||
// if it is selection
|
||
for (let i = 0; i < ranges.length; i++) {
|
||
if (typeof ranges[i] != 'object') ranges[i] = { name: 'selection' }
|
||
if (ranges[i].name == 'selection') {
|
||
if (this.show.selectionBorder === false) continue
|
||
let sel = this.getSelection()
|
||
if (sel.length === 0) {
|
||
this.removeRange('selection')
|
||
continue
|
||
} else {
|
||
first = sel[0]
|
||
last = sel[sel.length-1]
|
||
}
|
||
} else { // other range
|
||
first = ranges[i].range[0]
|
||
last = ranges[i].range[1]
|
||
}
|
||
if (first) {
|
||
let rg = {
|
||
name: ranges[i].name,
|
||
range: [{ recid: first.recid, column: first.column }, { recid: last.recid, column: last.column }],
|
||
style: ranges[i].style || ''
|
||
}
|
||
// add range
|
||
let ind = false
|
||
for (let j = 0; j < this.ranges.length; j++) if (this.ranges[j].name == ranges[i].name) { ind = j; break }
|
||
if (ind !== false) {
|
||
this.ranges[ind] = rg
|
||
} else {
|
||
this.ranges.push(rg)
|
||
}
|
||
added++
|
||
}
|
||
}
|
||
this.refreshRanges()
|
||
return added
|
||
}
|
||
removeRange() {
|
||
let removed = 0
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
let name = arguments[a]
|
||
query(this.box).find('#grid_'+ this.name +'_'+ name).remove()
|
||
query(this.box).find('#grid_'+ this.name +'_f'+ name).remove()
|
||
for (let r = this.ranges.length-1; r >= 0; r--) {
|
||
if (this.ranges[r].name == name) {
|
||
this.ranges.splice(r, 1)
|
||
removed++
|
||
}
|
||
}
|
||
}
|
||
return removed
|
||
}
|
||
refreshRanges() {
|
||
if (this.ranges.length === 0) return
|
||
let self = this
|
||
let range
|
||
let time = Date.now()
|
||
let rec1 = query(this.box).find(`#grid_${this.name}_frecords`)
|
||
let rec2 = query(this.box).find(`#grid_${this.name}_records`)
|
||
for (let i = 0; i < this.ranges.length; i++) {
|
||
let rg = this.ranges[i]
|
||
let first = rg.range[0]
|
||
let last = rg.range[1]
|
||
if (first.index == null) first.index = this.get(first.recid, true)
|
||
if (last.index == null) last.index = this.get(last.recid, true)
|
||
let td1 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(first.recid) + ' td[col="'+ first.column +'"]')
|
||
let td2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(last.recid) + ' td[col="'+ last.column +'"]')
|
||
let td1f = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(first.recid) + ' td[col="'+ first.column +'"]')
|
||
let td2f = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(last.recid) + ' td[col="'+ last.column +'"]')
|
||
let _lastColumn = last.column
|
||
// adjustment due to column virtual scroll
|
||
if (first.column < this.last.colStart && last.column > this.last.colStart) {
|
||
td1 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(first.recid) + ' td[col="start"]')
|
||
}
|
||
if (first.column < this.last.colEnd && last.column > this.last.colEnd) {
|
||
td2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(last.recid) + ' td[col="end"]')
|
||
_lastColumn = '"end"'
|
||
}
|
||
// if virtual scrolling kicked in
|
||
let index_top = parseInt(query(this.box).find('#grid_'+ this.name +'_rec_top').next().attr('index'))
|
||
let index_bottom = parseInt(query(this.box).find('#grid_'+ this.name +'_rec_bottom').prev().attr('index'))
|
||
let index_ftop = parseInt(query(this.box).find('#grid_'+ this.name +'_frec_top').next().attr('index'))
|
||
let index_fbottom = parseInt(query(this.box).find('#grid_'+ this.name +'_frec_bottom').prev().attr('index'))
|
||
if (td1.length === 0 && first.index < index_top && last.index > index_top) {
|
||
td1 = query(this.box).find('#grid_'+ this.name +'_rec_top').next().find('td[col="'+ first.column +'"]')
|
||
}
|
||
if (td2.length === 0 && last.index > index_bottom && first.index < index_bottom) {
|
||
td2 = query(this.box).find('#grid_'+ this.name +'_rec_bottom').prev().find('td[col="'+ _lastColumn +'"]')
|
||
}
|
||
if (td1f.length === 0 && first.index < index_ftop && last.index > index_ftop) { // frozen
|
||
td1f = query(this.box).find('#grid_'+ this.name +'_frec_top').next().find('td[col="'+ first.column +'"]')
|
||
}
|
||
if (td2f.length === 0 && last.index > index_fbottom && first.index < index_fbottom) { // frozen
|
||
td2f = query(this.box).find('#grid_'+ this.name +'_frec_bottom').prev().find('td[col="'+ last.column +'"]')
|
||
}
|
||
// do not show selection cell if it is editable
|
||
let edit = query(this.box).find('#grid_'+ this.name + '_editable')
|
||
let tmp = edit.find('.w2ui-input')
|
||
let tmp1 = tmp.attr('recid')
|
||
let tmp2 = tmp.attr('column')
|
||
if (rg.name == 'selection' && rg.range[0].recid == tmp1 && rg.range[0].column == tmp2) continue
|
||
// frozen regular columns range
|
||
range = query(this.box).find('#grid_'+ this.name +'_f'+ rg.name)
|
||
if (td1f.length > 0 || td2f.length > 0) {
|
||
if (range.length === 0) {
|
||
rec1.append('<div id="grid_'+ this.name +'_f' + rg.name +'" class="w2ui-selection" style="'+ rg.style +'">'+
|
||
(rg.name == 'selection' ? '<div id="grid_'+ this.name +'_resizer" class="w2ui-selection-resizer"></div>' : '')+
|
||
'</div>')
|
||
range = query(this.box).find('#grid_'+ this.name +'_f'+ rg.name)
|
||
} else {
|
||
range.attr('style', rg.style)
|
||
range.find('.w2ui-selection-resizer').show()
|
||
}
|
||
if (td2f.length === 0) {
|
||
td2f = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(last.recid) +' td:last-child')
|
||
if (td2f.length === 0) td2f = query(this.box).find('#grid_'+ this.name +'_frec_bottom td:first-child')
|
||
range.css('border-right', '0px')
|
||
range.find('.w2ui-selection-resizer').hide()
|
||
}
|
||
if (first.recid != null && last.recid != null && td1f.length > 0 && td2f.length > 0) {
|
||
let style = getComputedStyle(td2f[0])
|
||
let top1 = (td1f.prop('offsetTop') - td1f.prop('scrollTop'))
|
||
let left1 = (td1f.prop('offsetLeft') + td1f.prop('scrollLeft'))
|
||
let top2 = (td2f.prop('offsetTop') - td2f.prop('scrollTop'))
|
||
let left2 = (td2f.prop('offsetLeft') + td2f.prop('scrollLeft'))
|
||
range.show().css({
|
||
top : (top1 > 0 ? top1 : 0) + 'px',
|
||
left : (left1 > 0 ? left1 : 0) + 'px',
|
||
width : (left2 - left1 + parseFloat(style.width) + 2) + 'px',
|
||
height : (top2 - top1 + parseFloat(style.height) + 1) + 'px'
|
||
})
|
||
} else {
|
||
range.hide()
|
||
}
|
||
} else {
|
||
range.hide()
|
||
}
|
||
// regular columns range
|
||
range = query(this.box).find('#grid_'+ this.name +'_'+ rg.name)
|
||
if (td1.length > 0 || td2.length > 0) {
|
||
if (range.length === 0) {
|
||
rec2.append('<div id="grid_'+ this.name +'_' + rg.name +'" class="w2ui-selection" style="'+ rg.style +'">'+
|
||
(rg.name == 'selection' ? '<div id="grid_'+ this.name +'_resizer" class="w2ui-selection-resizer"></div>' : '')+
|
||
'</div>')
|
||
range = query(this.box).find('#grid_'+ this.name +'_'+ rg.name)
|
||
} else {
|
||
range.attr('style', rg.style)
|
||
}
|
||
if (td1.length === 0) {
|
||
td1 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(first.recid) +' td:first-child')
|
||
if (td1.length === 0) td1 = query(this.box).find('#grid_'+ this.name +'_rec_top td:first-child')
|
||
}
|
||
if (td2f.length !== 0) {
|
||
range.css('border-left', '0px')
|
||
}
|
||
if (first.recid != null && last.recid != null && td1.length > 0 && td2.length > 0) {
|
||
let style = getComputedStyle(td2[0])
|
||
let top1 = (td1.prop('offsetTop') - td1.prop('scrollTop'))
|
||
let left1 = (td1.prop('offsetLeft') + td1.prop('scrollLeft'))
|
||
let top2 = (td2.prop('offsetTop') - td2.prop('scrollTop'))
|
||
let left2 = (td2.prop('offsetLeft') + td2.prop('scrollLeft'))
|
||
range.show().css({
|
||
top : (top1 > 0 ? top1 : 0) + 'px',
|
||
left : (left1 > 0 ? left1 : 0) + 'px',
|
||
width : (left2 - left1 + parseFloat(style.width) + 2) + 'px',
|
||
height : (top2 - top1 + parseFloat(style.height) + 1) + 'px'
|
||
})
|
||
} else {
|
||
range.hide()
|
||
}
|
||
} else {
|
||
range.hide()
|
||
}
|
||
}
|
||
// add resizer events
|
||
query(this.box).find('.w2ui-selection-resizer')
|
||
.off('.resizer')
|
||
.on('mousedown.resizer', mouseStart)
|
||
.on('dblclick.resizer', (event) => {
|
||
let edata = this.trigger('resizerDblClick', { target: this.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
edata.finish()
|
||
})
|
||
let edata = { target: this.name, originalRange: null, newRange: null }
|
||
return Date.now() - time
|
||
function mouseStart(event) {
|
||
let sel = self.getSelection()
|
||
self.last.move = {
|
||
type : 'expand',
|
||
x : event.screenX,
|
||
y : event.screenY,
|
||
divX : 0,
|
||
divY : 0,
|
||
recid : sel[0].recid,
|
||
column : sel[0].column,
|
||
originalRange : [w2utils.clone(sel[0]), w2utils.clone(sel[sel.length-1]) ],
|
||
newRange : [w2utils.clone(sel[0]), w2utils.clone(sel[sel.length-1]) ]
|
||
}
|
||
query('body')
|
||
.off('.w2ui-' + self.name)
|
||
.on('mousemove.w2ui-' + self.name, mouseMove)
|
||
.on('mouseup.w2ui-' + self.name, mouseStop)
|
||
// do not blur grid
|
||
event.preventDefault()
|
||
}
|
||
function mouseMove(event) {
|
||
let mv = self.last.move
|
||
if (!mv || mv.type != 'expand') return
|
||
mv.divX = (event.screenX - mv.x)
|
||
mv.divY = (event.screenY - mv.y)
|
||
// find new cell
|
||
let recid, column
|
||
let tmp = event.target
|
||
if (tmp.tagName.toUpperCase() != 'TD') tmp = query(tmp).closest('td')[0]
|
||
if (query(tmp).attr('col') != null) column = parseInt(query(tmp).attr('col'))
|
||
if (column == null) {
|
||
return
|
||
}
|
||
tmp = query(tmp).closest('tr')[0]
|
||
recid = self.records[query(tmp).attr('index')].recid
|
||
// new range
|
||
if (mv.newRange[1].recid == recid && mv.newRange[1].column == column) return
|
||
let prevNewRange = w2utils.clone(mv.newRange)
|
||
mv.newRange = [{ recid: mv.recid, column: mv.column }, { recid: recid, column: column }]
|
||
// event before
|
||
if (edata.detail) {
|
||
edata.detail.newRange = w2utils.clone(mv.newRange)
|
||
edata.detail.originalRange = w2utils.clone(mv.originalRange)
|
||
}
|
||
edata = self.trigger('selectionExtend', edata)
|
||
if (edata.isCancelled === true) {
|
||
mv.newRange = prevNewRange
|
||
edata.detail.newRange = prevNewRange
|
||
return
|
||
} else {
|
||
// default behavior
|
||
self.removeRange('grid-selection-expand')
|
||
self.addRange({
|
||
name : 'grid-selection-expand',
|
||
range : mv.newRange,
|
||
style : 'background-color: rgba(100,100,100,0.1); border: 2px dotted rgba(100,100,100,0.5);'
|
||
})
|
||
}
|
||
}
|
||
function mouseStop(event) {
|
||
// default behavior
|
||
self.removeRange('grid-selection-expand')
|
||
delete self.last.move
|
||
query('body').off('.w2ui-' + self.name)
|
||
// event after
|
||
if (edata.finish) edata.finish()
|
||
}
|
||
}
|
||
select() {
|
||
if (arguments.length === 0) return 0
|
||
let selected = 0
|
||
let sel = this.last.selection
|
||
if (!this.multiSelect) this.selectNone(true)
|
||
// if too many arguments > 150k, then it errors off
|
||
let args = Array.from(arguments)
|
||
if (Array.isArray(args[0])) args = args[0]
|
||
// event before
|
||
let tmp = { target: this.name }
|
||
if (args.length == 1) {
|
||
tmp.multiple = false
|
||
if (w2utils.isPlainObject(args[0])) {
|
||
tmp.clicked = {
|
||
recid: args[0].recid,
|
||
column: args[0].column
|
||
}
|
||
} else {
|
||
tmp.recid = args[0]
|
||
}
|
||
} else {
|
||
tmp.multiple = true
|
||
tmp.clicked = { recids: args }
|
||
}
|
||
let edata = this.trigger('select', tmp)
|
||
if (edata.isCancelled === true) return 0
|
||
// default action
|
||
if (this.selectType == 'row') {
|
||
for (let a = 0; a < args.length; a++) {
|
||
let recid = typeof args[a] == 'object' ? args[a].recid : args[a]
|
||
let index = this.get(recid, true)
|
||
if (index == null) continue
|
||
let recEl1 = null
|
||
let recEl2 = null
|
||
if (this.searchData.length !== 0 || (index + 1 >= this.last.range_start && index + 1 <= this.last.range_end)) {
|
||
recEl1 = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid))
|
||
recEl2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid))
|
||
}
|
||
if (this.selectType == 'row') {
|
||
if (sel.indexes.indexOf(index) != -1) continue
|
||
sel.indexes.push(index)
|
||
if (recEl1 && recEl2) {
|
||
recEl1.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
recEl2.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
recEl1.find('.w2ui-grid-select-check').prop('checked', true)
|
||
}
|
||
selected++
|
||
}
|
||
}
|
||
} else {
|
||
// normalize for performance
|
||
let new_sel = {}
|
||
for (let a = 0; a < args.length; a++) {
|
||
let recid = typeof args[a] == 'object' ? args[a].recid : args[a]
|
||
let column = typeof args[a] == 'object' ? args[a].column : null
|
||
new_sel[recid] = new_sel[recid] || []
|
||
if (Array.isArray(column)) {
|
||
new_sel[recid] = column
|
||
} else if (w2utils.isInt(column)) {
|
||
new_sel[recid].push(column)
|
||
} else {
|
||
for (let i = 0; i < this.columns.length; i++) { if (this.columns[i].hidden) continue; new_sel[recid].push(parseInt(i)) }
|
||
}
|
||
}
|
||
// add all
|
||
let col_sel = []
|
||
for (let recid in new_sel) {
|
||
let index = this.get(recid, true)
|
||
if (index == null) continue
|
||
let recEl1 = null
|
||
let recEl2 = null
|
||
if (index + 1 >= this.last.range_start && index + 1 <= this.last.range_end) {
|
||
recEl1 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid))
|
||
recEl2 = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid))
|
||
}
|
||
let s = sel.columns[index] || []
|
||
// default action
|
||
if (sel.indexes.indexOf(index) == -1) {
|
||
sel.indexes.push(index)
|
||
}
|
||
// only only those that are new
|
||
for (let t = 0; t < new_sel[recid].length; t++) {
|
||
if (s.indexOf(new_sel[recid][t]) == -1) s.push(new_sel[recid][t])
|
||
}
|
||
s.sort((a, b) => { return a-b }) // sort function must be for numerical sort
|
||
for (let t = 0; t < new_sel[recid].length; t++) {
|
||
let col = new_sel[recid][t]
|
||
if (col_sel.indexOf(col) == -1) col_sel.push(col)
|
||
if (recEl1) {
|
||
recEl1.find('#grid_'+ this.name +'_data_'+ index +'_'+ col).addClass('w2ui-selected')
|
||
recEl1.find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
recEl1.find('.w2ui-grid-select-check').prop('checked', true)
|
||
}
|
||
if (recEl2) {
|
||
recEl2.find('#grid_'+ this.name +'_data_'+ index +'_'+ col).addClass('w2ui-selected')
|
||
recEl2.find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
recEl2.find('.w2ui-grid-select-check').prop('checked', true)
|
||
}
|
||
selected++
|
||
}
|
||
// save back to selection object
|
||
sel.columns[index] = s
|
||
}
|
||
// select columns (need here for speed)
|
||
for (let c = 0; c < col_sel.length; c++) {
|
||
query(this.box).find('#grid_'+ this.name +'_column_'+ col_sel[c] +' .w2ui-col-header').addClass('w2ui-col-selected')
|
||
}
|
||
}
|
||
// need to sort new selection for speed
|
||
sel.indexes.sort((a, b) => { return a-b })
|
||
// all selected?
|
||
let areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length),
|
||
areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)
|
||
if (areAllSelected || areAllSearchedSelected) {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', true)
|
||
} else {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', false)
|
||
}
|
||
this.status()
|
||
this.addRange('selection')
|
||
this.updateToolbar(sel, areAllSelected)
|
||
// event after
|
||
edata.finish()
|
||
return selected
|
||
}
|
||
unselect() {
|
||
let unselected = 0
|
||
let sel = this.last.selection
|
||
// if too many arguments > 150k, then it errors off
|
||
let args = Array.from(arguments)
|
||
if (Array.isArray(args[0])) args = args[0]
|
||
// event before
|
||
let tmp = { target: this.name }
|
||
if (args.length == 1) {
|
||
tmp.multiple = false
|
||
if (w2utils.isPlainObject(args[0])) {
|
||
tmp.clicked = {
|
||
recid: args[0].recid,
|
||
column: args[0].column
|
||
}
|
||
} else {
|
||
tmp.clicked = { recid: args[0] }
|
||
}
|
||
} else {
|
||
tmp.multiple = true
|
||
tmp.recids = args
|
||
}
|
||
let edata = this.trigger('select', tmp)
|
||
if (edata.isCancelled === true) return 0
|
||
for (let a = 0; a < args.length; a++) {
|
||
let recid = typeof args[a] == 'object' ? args[a].recid : args[a]
|
||
let record = this.get(recid)
|
||
if (record == null) continue
|
||
let index = this.get(record.recid, true)
|
||
let recEl1 = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid))
|
||
let recEl2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid))
|
||
if (this.selectType == 'row') {
|
||
if (sel.indexes.indexOf(index) == -1) continue
|
||
// default action
|
||
sel.indexes.splice(sel.indexes.indexOf(index), 1)
|
||
recEl1.removeClass('w2ui-selected w2ui-inactive').find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
recEl2.removeClass('w2ui-selected w2ui-inactive').find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
if (recEl1.length != 0) {
|
||
recEl1[0].style.cssText = 'height: '+ this.recordHeight +'px; ' + recEl1.attr('custom_style')
|
||
recEl2[0].style.cssText = 'height: '+ this.recordHeight +'px; ' + recEl2.attr('custom_style')
|
||
}
|
||
recEl1.find('.w2ui-grid-select-check').prop('checked', false)
|
||
unselected++
|
||
} else {
|
||
let col = args[a].column
|
||
if (!w2utils.isInt(col)) { // unselect all columns
|
||
let cols = []
|
||
for (let i = 0; i < this.columns.length; i++) { if (this.columns[i].hidden) continue; cols.push({ recid: recid, column: i }) }
|
||
return this.unselect(cols)
|
||
}
|
||
let s = sel.columns[index]
|
||
if (!Array.isArray(s) || s.indexOf(col) == -1) continue
|
||
// default action
|
||
s.splice(s.indexOf(col), 1)
|
||
query(this.box).find(`#grid_${this.name}_rec_${w2utils.escapeId(recid)} > td[col="${col}"]`).removeClass('w2ui-selected w2ui-inactive')
|
||
query(this.box).find(`#grid_${this.name}_frec_${w2utils.escapeId(recid)} > td[col="${col}"]`).removeClass('w2ui-selected w2ui-inactive')
|
||
// check if any row/column still selected
|
||
let isColSelected = false
|
||
let isRowSelected = false
|
||
let tmp = this.getSelection()
|
||
for (let i = 0; i < tmp.length; i++) {
|
||
if (tmp[i].column == col) isColSelected = true
|
||
if (tmp[i].recid == recid) isRowSelected = true
|
||
}
|
||
if (!isColSelected) {
|
||
query(this.box).find(`.w2ui-grid-columns td[col="${col}"] .w2ui-col-header, .w2ui-grid-fcolumns td[col="${col}"] .w2ui-col-header`).removeClass('w2ui-col-selected')
|
||
}
|
||
if (!isRowSelected) {
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid)).find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
}
|
||
unselected++
|
||
if (s.length === 0) {
|
||
delete sel.columns[index]
|
||
sel.indexes.splice(sel.indexes.indexOf(index), 1)
|
||
recEl1.find('.w2ui-grid-select-check').prop('checked', false)
|
||
}
|
||
}
|
||
}
|
||
// all selected?
|
||
let areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length),
|
||
areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)
|
||
if (areAllSelected || areAllSearchedSelected) {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', true)
|
||
} else {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', false)
|
||
}
|
||
// show number of selected
|
||
this.status()
|
||
this.addRange('selection')
|
||
this.updateToolbar(sel, areAllSelected)
|
||
// event after
|
||
edata.finish()
|
||
return unselected
|
||
}
|
||
selectAll() {
|
||
let time = Date.now()
|
||
if (this.multiSelect === false) return
|
||
// default action
|
||
let url = this.url?.get ?? this.url
|
||
let sel = w2utils.clone(this.last.selection)
|
||
let cols = []
|
||
for (let i = 0; i < this.columns.length; i++) cols.push(i)
|
||
// if local data source and searched
|
||
sel.indexes = []
|
||
if (!url && this.searchData.length !== 0) {
|
||
// local search applied
|
||
for (let i = 0; i < this.last.searchIds.length; i++) {
|
||
sel.indexes.push(this.last.searchIds[i])
|
||
if (this.selectType != 'row') sel.columns[this.last.searchIds[i]] = cols.slice() // .slice makes copy of the array
|
||
}
|
||
} else {
|
||
let buffered = this.records.length
|
||
if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length
|
||
for (let i = 0; i < buffered; i++) {
|
||
sel.indexes.push(i)
|
||
if (this.selectType != 'row') sel.columns[i] = cols.slice() // .slice makes copy of the array
|
||
}
|
||
}
|
||
// event before
|
||
let edata = this.trigger('select', { target: this.name, multiple: true, all: true, clicked: sel })
|
||
if (edata.isCancelled === true) return
|
||
this.last.selection = sel
|
||
// add selected class
|
||
if (this.selectType == 'row') {
|
||
query(this.box).find('.w2ui-grid-records tr:not(.w2ui-empty-record)')
|
||
.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-frecords tr:not(.w2ui-empty-record)')
|
||
.addClass('w2ui-selected').find('.w2ui-col-number').addClass('w2ui-row-selected')
|
||
query(this.box).find('input.w2ui-grid-select-check').prop('checked', true)
|
||
} else {
|
||
query(this.box).find('.w2ui-grid-columns td .w2ui-col-header, .w2ui-grid-fcolumns td .w2ui-col-header').addClass('w2ui-col-selected')
|
||
query(this.box).find('.w2ui-grid-records tr .w2ui-col-number').addClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-records tr:not(.w2ui-empty-record)')
|
||
.find('.w2ui-grid-data:not(.w2ui-col-select)').addClass('w2ui-selected')
|
||
query(this.box).find('.w2ui-grid-frecords tr .w2ui-col-number').addClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-frecords tr:not(.w2ui-empty-record)')
|
||
.find('.w2ui-grid-data:not(.w2ui-col-select)').addClass('w2ui-selected')
|
||
query(this.box).find('input.w2ui-grid-select-check').prop('checked', true)
|
||
}
|
||
// enable/disable toolbar buttons
|
||
sel = this.getSelection(true)
|
||
this.addRange('selection')
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', true)
|
||
this.status()
|
||
this.updateToolbar({ indexes: sel }, true)
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
selectNone(skipEvent) {
|
||
let time = Date.now()
|
||
// event before
|
||
let edata
|
||
if (!skipEvent) {
|
||
edata = this.trigger('select', { target: this.name, clicked: [] })
|
||
if (edata.isCancelled === true) return
|
||
}
|
||
// default action
|
||
let sel = this.last.selection
|
||
// remove selected class
|
||
if (this.selectType == 'row') {
|
||
query(this.box).find('.w2ui-grid-records tr.w2ui-selected').removeClass('w2ui-selected w2ui-inactive')
|
||
.find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-frecords tr.w2ui-selected').removeClass('w2ui-selected w2ui-inactive')
|
||
.find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
query(this.box).find('input.w2ui-grid-select-check').prop('checked', false)
|
||
} else {
|
||
query(this.box).find('.w2ui-grid-columns td .w2ui-col-header, .w2ui-grid-fcolumns td .w2ui-col-header').removeClass('w2ui-col-selected')
|
||
query(this.box).find('.w2ui-grid-records tr .w2ui-col-number').removeClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-frecords tr .w2ui-col-number').removeClass('w2ui-row-selected')
|
||
query(this.box).find('.w2ui-grid-data.w2ui-selected').removeClass('w2ui-selected w2ui-inactive')
|
||
query(this.box).find('input.w2ui-grid-select-check').prop('checked', false)
|
||
}
|
||
sel.indexes = []
|
||
sel.columns = {}
|
||
this.removeRange('selection')
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', false)
|
||
this.status()
|
||
this.updateToolbar(sel, false)
|
||
// event after
|
||
if (!skipEvent) {
|
||
edata.finish()
|
||
}
|
||
return Date.now() - time
|
||
}
|
||
updateToolbar(sel) {
|
||
let obj = this
|
||
let cnt = sel && sel.indexes ? sel.indexes.length : 0
|
||
this.toolbar.items.forEach((item) => {
|
||
_checkItem(item, '')
|
||
if (Array.isArray(item.items)) {
|
||
item.items.forEach((it) => {
|
||
_checkItem(it, item.id + ':')
|
||
})
|
||
}
|
||
})
|
||
// enable/disable toolbar search button
|
||
if (this.show.toolbarSave) {
|
||
if (this.getChanges().length > 0) {
|
||
this.toolbar.enable('w2ui-save')
|
||
} else {
|
||
this.toolbar.disable('w2ui-save')
|
||
}
|
||
}
|
||
function _checkItem(item, prefix) {
|
||
if (item.batch != null) {
|
||
let enabled = false
|
||
if (item.batch === true) {
|
||
if (cnt > 0) enabled = true
|
||
} else if (typeof item.batch == 'number') {
|
||
if (cnt === item.batch) enabled = true
|
||
} else if (typeof item.batch == 'function') {
|
||
enabled = item.batch({ cnt, sel })
|
||
}
|
||
if (enabled) {
|
||
obj.toolbar.enable(prefix + item.id)
|
||
} else {
|
||
obj.toolbar.disable(prefix + item.id)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
getSelection(returnIndex) {
|
||
let ret = []
|
||
let sel = this.last.selection
|
||
if (this.selectType == 'row') {
|
||
for (let i = 0; i < sel.indexes.length; i++) {
|
||
if (!this.records[sel.indexes[i]]) continue
|
||
if (returnIndex === true) ret.push(sel.indexes[i]); else ret.push(this.records[sel.indexes[i]].recid)
|
||
}
|
||
return ret
|
||
} else {
|
||
for (let i = 0; i < sel.indexes.length; i++) {
|
||
let cols = sel.columns[sel.indexes[i]]
|
||
if (!this.records[sel.indexes[i]]) continue
|
||
for (let j = 0; j < cols.length; j++) {
|
||
ret.push({ recid: this.records[sel.indexes[i]].recid, index: parseInt(sel.indexes[i]), column: cols[j] })
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
}
|
||
search(field, value) {
|
||
let url = this.url?.get ?? this.url
|
||
let searchData = []
|
||
let last_multi = this.last.multi
|
||
let last_logic = this.last.logic
|
||
let last_field = this.last.field
|
||
let last_search = this.last.search
|
||
let hasHiddenSearches = false
|
||
let overlay = query(`#w2overlay-${this.name}-search-overlay`)
|
||
// add hidden searches
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
if (!this.searches[i].hidden || this.searches[i].value == null) continue
|
||
searchData.push({
|
||
field : this.searches[i].field,
|
||
operator : this.searches[i].operator || 'is',
|
||
type : this.searches[i].type,
|
||
value : this.searches[i].value || ''
|
||
})
|
||
hasHiddenSearches = true
|
||
}
|
||
if (arguments.length === 0 && overlay.length === 0) {
|
||
if (this.multiSearch) {
|
||
field = this.searchData
|
||
value = this.last.logic
|
||
} else {
|
||
field = this.last.field
|
||
value = this.last.search
|
||
}
|
||
}
|
||
// 1: search() - advanced search (reads from popup)
|
||
if (arguments.length === 0 && overlay.length !== 0) {
|
||
this.focus() // otherwise search drop down covers searches
|
||
last_logic = overlay.find(`#grid_${this.name}_logic`).val()
|
||
last_search = ''
|
||
// advanced search
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
let search = this.searches[i]
|
||
let operator = overlay.find('#grid_'+ this.name + '_operator_'+ i).val()
|
||
let field1 = overlay.find('#grid_'+ this.name + '_field_'+ i)
|
||
let field2 = overlay.find('#grid_'+ this.name + '_field2_'+ i)
|
||
let value1 = field1.val()
|
||
let value2 = field2.val()
|
||
let svalue = null
|
||
let text = null
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1) {
|
||
let fld1 = field1[0]._w2field
|
||
let fld2 = field2[0]._w2field
|
||
if (fld1) value1 = fld1.clean(value1)
|
||
if (fld2) value2 = fld2.clean(value2)
|
||
}
|
||
if (['list', 'enum'].indexOf(search.type) != -1 || ['in', 'not in'].indexOf(operator) != -1) {
|
||
value1 = field1[0]._w2field.selected || {}
|
||
if (Array.isArray(value1)) {
|
||
svalue = []
|
||
for (let j = 0; j < value1.length; j++) {
|
||
svalue.push(w2utils.isFloat(value1[j].id) ? parseFloat(value1[j].id) : String(value1[j].id).toLowerCase())
|
||
delete value1[j].hidden
|
||
}
|
||
if (Object.keys(value1).length === 0) value1 = ''
|
||
} else {
|
||
text = value1.text || ''
|
||
value1 = value1.id || ''
|
||
}
|
||
}
|
||
if ((value1 !== '' && value1 != null) || (value2 != null && value2 !== '')) {
|
||
let tmp = {
|
||
field : search.field,
|
||
type : search.type,
|
||
operator : operator
|
||
}
|
||
if (operator == 'between') {
|
||
w2utils.extend(tmp, { value: [value1, value2] })
|
||
} else if (operator == 'in' && typeof value1 == 'string') {
|
||
w2utils.extend(tmp, { value: value1.split(',') })
|
||
} else if (operator == 'not in' && typeof value1 == 'string') {
|
||
w2utils.extend(tmp, { value: value1.split(',') })
|
||
} else {
|
||
w2utils.extend(tmp, { value: value1 })
|
||
}
|
||
if (svalue) w2utils.extend(tmp, { svalue: svalue })
|
||
if (text) w2utils.extend(tmp, { text: text })
|
||
// convert date to unix time
|
||
try {
|
||
if (search.type == 'date' && operator == 'between') {
|
||
tmp.value[0] = value1 // w2utils.isDate(value1, w2utils.settings.dateFormat, true).getTime();
|
||
tmp.value[1] = value2 // w2utils.isDate(value2, w2utils.settings.dateFormat, true).getTime();
|
||
}
|
||
if (search.type == 'date' && operator == 'is') {
|
||
tmp.value = value1 // w2utils.isDate(value1, w2utils.settings.dateFormat, true).getTime();
|
||
}
|
||
} catch (e) {
|
||
}
|
||
searchData.push(tmp)
|
||
last_multi = true // if only hidden searches, then do not set
|
||
}
|
||
}
|
||
}
|
||
// 2: search(field, value) - regular search
|
||
if (typeof field == 'string') {
|
||
// if only one argument - search all
|
||
if (arguments.length == 1) {
|
||
value = field
|
||
field = 'all'
|
||
}
|
||
last_field = field
|
||
last_search = value
|
||
last_multi = false
|
||
last_logic = (hasHiddenSearches ? 'AND' : 'OR')
|
||
// loop through all searches and see if it applies
|
||
if (value != null) {
|
||
if (field.toLowerCase() == 'all') {
|
||
// if there are search fields loop thru them
|
||
if (this.searches.length > 0) {
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
let search = this.searches[i]
|
||
if (search.type == 'text' || (search.type == 'alphanumeric' && w2utils.isAlphaNumeric(value))
|
||
|| (search.type == 'int' && w2utils.isInt(value)) || (search.type == 'float' && w2utils.isFloat(value))
|
||
|| (search.type == 'percent' && w2utils.isFloat(value)) || ((search.type == 'hex' || search.type == 'color') && w2utils.isHex(value))
|
||
|| (search.type == 'currency' && w2utils.isMoney(value)) || (search.type == 'money' && w2utils.isMoney(value))
|
||
|| (search.type == 'date' && w2utils.isDate(value)) || (search.type == 'time' && w2utils.isTime(value))
|
||
|| (search.type == 'datetime' && w2utils.isDateTime(value)) || (search.type == 'datetime' && w2utils.isDate(value))
|
||
|| (search.type == 'enum' && w2utils.isAlphaNumeric(value)) || (search.type == 'list' && w2utils.isAlphaNumeric(value))
|
||
) {
|
||
let def = this.defaultOperator[this.operatorsMap[search.type]]
|
||
let tmp = {
|
||
field : search.field,
|
||
type : search.type,
|
||
operator : (search.operator != null ? search.operator : def),
|
||
value : value
|
||
}
|
||
if (String(value).trim() != '') searchData.push(tmp)
|
||
}
|
||
// range in global search box
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(search.type) != -1 && String(value).trim().split('-').length == 2) {
|
||
let t = String(value).trim().split('-')
|
||
let tmp = {
|
||
field : search.field,
|
||
type : search.type,
|
||
operator : (search.operator != null ? search.operator : 'between'),
|
||
value : [t[0], t[1]]
|
||
}
|
||
searchData.push(tmp)
|
||
}
|
||
// lists fields
|
||
if (['list', 'enum'].indexOf(search.type) != -1) {
|
||
let new_values = []
|
||
if (search.options == null) search.options = {}
|
||
if (!Array.isArray(search.options.items)) search.options.items = []
|
||
for (let j = 0; j < search.options.items; j++) {
|
||
let tmp = search.options.items[j]
|
||
try {
|
||
let re = new RegExp(value, 'i')
|
||
if (re.test(tmp)) new_values.push(j)
|
||
if (tmp.text && re.test(tmp.text)) new_values.push(tmp.id)
|
||
} catch (e) {}
|
||
}
|
||
if (new_values.length > 0) {
|
||
let tmp = {
|
||
field : search.field,
|
||
type : search.type,
|
||
operator : (search.operator != null ? search.operator : 'in'),
|
||
value : new_values
|
||
}
|
||
searchData.push(tmp)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// no search fields, loop thru columns
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let tmp = {
|
||
field : this.columns[i].field,
|
||
type : 'text',
|
||
operator : this.defaultOperator.text,
|
||
value : value
|
||
}
|
||
searchData.push(tmp)
|
||
}
|
||
}
|
||
} else {
|
||
let el = overlay.find('#grid_'+ this.name +'_search_all')
|
||
let search = this.getSearch(field)
|
||
if (search == null) search = { field: field, type: 'text' }
|
||
if (search.field == field) this.last.label = search.label
|
||
if (value !== '') {
|
||
let op = this.defaultOperator[this.operatorsMap[search.type]]
|
||
let val = value
|
||
if (['date', 'time', 'datetime'].indexOf(search.type) != -1) op = 'is'
|
||
if (['list', 'enum'].indexOf(search.type) != -1) {
|
||
op = 'is'
|
||
let tmp = el._w2field.get()
|
||
if (tmp && Object.keys(tmp).length > 0) val = tmp.id; else val = ''
|
||
}
|
||
if (search.type == 'int' && value !== '') {
|
||
op = 'is'
|
||
if (String(value).indexOf('-') != -1) {
|
||
let tmp = value.split('-')
|
||
if (tmp.length == 2) {
|
||
op = 'between'
|
||
val = [parseInt(tmp[0]), parseInt(tmp[1])]
|
||
}
|
||
}
|
||
if (String(value).indexOf(',') != -1) {
|
||
let tmp = value.split(',')
|
||
op = 'in'
|
||
val = []
|
||
for (let i = 0; i < tmp.length; i++) val.push(tmp[i])
|
||
}
|
||
}
|
||
if (search.operator != null) op = search.operator
|
||
let tmp = {
|
||
field : search.field,
|
||
type : search.type,
|
||
operator : op,
|
||
value : val
|
||
}
|
||
searchData.push(tmp)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 3: search([{ field, value, [operator,] [type] }, { field, value, [operator,] [type] } ], logic) - submit whole structure
|
||
if (Array.isArray(field)) {
|
||
let logic = 'AND'
|
||
if (typeof value == 'string') {
|
||
logic = value.toUpperCase()
|
||
if (logic != 'OR' && logic != 'AND') logic = 'AND'
|
||
}
|
||
last_search = ''
|
||
last_multi = true
|
||
last_logic = logic
|
||
for (let i = 0; i < field.length; i++) {
|
||
let data = field[i]
|
||
if (typeof data.value == 'number' && data.operator == null) data.operator = this.defaultOperator.number
|
||
if (typeof data.value == 'string' && data.operator == null) data.operator = this.defaultOperator.text
|
||
if (Array.isArray(data.value) && data.operator == null) data.operator = this.defaultOperator.enum
|
||
if (w2utils.isDate(data.value) && data.operator == null) data.operator = this.defaultOperator.date
|
||
// merge current field and search if any
|
||
searchData.push(data)
|
||
}
|
||
}
|
||
// event before
|
||
let edata = this.trigger('search', {
|
||
target: this.name,
|
||
multi: (arguments.length === 0 ? true : false),
|
||
searchField: (field ? field : 'multi'),
|
||
searchValue: (field ? value : 'multi'),
|
||
searchData: searchData,
|
||
searchLogic: last_logic
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
this.searchData = edata.detail.searchData
|
||
this.last.field = last_field
|
||
this.last.search = last_search
|
||
this.last.multi = last_multi
|
||
this.last.logic = edata.detail.searchLogic
|
||
this.last.scrollTop = 0
|
||
this.last.scrollLeft = 0
|
||
this.last.selection.indexes = []
|
||
this.last.selection.columns = {}
|
||
// -- clear all search field
|
||
this.searchClose()
|
||
// apply search
|
||
if (url) {
|
||
this.last.fetch.offset = 0
|
||
this.reload()
|
||
} else {
|
||
// local search
|
||
this.localSearch()
|
||
this.refresh()
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
// open advanced search popover
|
||
searchOpen() {
|
||
if (!this.box) return
|
||
if (this.searches.length === 0) return
|
||
// event before
|
||
let edata = this.trigger('searchOpen', { target: this.name })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
let $btn = query(this.toolbar.box).find('.w2ui-grid-search-input .w2ui-search-drop')
|
||
$btn.addClass('checked')
|
||
// show search
|
||
w2tooltip.show({
|
||
name: this.name + '-search-overlay',
|
||
anchor: query(this.box).find('#grid_'+ this.name +'_search_all').get(0),
|
||
position: 'bottom|top',
|
||
html: this.getSearchesHTML(),
|
||
align: 'left',
|
||
arrowSize: 12,
|
||
class: 'w2ui-grid-search-advanced',
|
||
hideOn: ['doc-click']
|
||
})
|
||
.then(event => {
|
||
this.initSearches()
|
||
this.last.search_opened = true
|
||
let overlay = query(`#w2overlay-${this.name}-search-overlay`)
|
||
overlay
|
||
.data('gridName', this.name)
|
||
.off('.grid-search')
|
||
.on('click.grid-search', () => {
|
||
// hide any tooltip opened by searches
|
||
overlay.find('input, select').each(el => {
|
||
let names = query(el).data('tooltipName')
|
||
if (names) names.forEach(name => {
|
||
w2tooltip.hide(name)
|
||
})
|
||
})
|
||
})
|
||
w2utils.bindEvents(overlay.find('select, input, button'), this)
|
||
// init first field
|
||
let sfields = query(`#w2overlay-${this.name}-search-overlay *[rel=search]`)
|
||
if (sfields.length > 0) sfields[0].focus()
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
.hide(event => {
|
||
$btn.removeClass('checked')
|
||
this.last.search_opened = false
|
||
})
|
||
}
|
||
searchClose() {
|
||
w2tooltip.hide(this.name + '-search-overlay')
|
||
}
|
||
// if clicked on a field in the search strip
|
||
searchFieldTooltip(ind, sd_ind, el) {
|
||
let sf = this.searches[ind]
|
||
let sd = this.searchData[sd_ind]
|
||
let oper = sd.operator
|
||
if (oper == 'more' && sd.type == 'date') oper = 'since'
|
||
if (oper == 'less' && sd.type == 'date') oper = 'before'
|
||
let options = ''
|
||
let val = sd.value
|
||
if (Array.isArray(sd.value)) { // && Array.isArray(sf.options.items)) {
|
||
sd.value.forEach(opt => {
|
||
options += `<span class="value">${opt.text || opt}</span>`
|
||
})
|
||
if (sd.type == 'date') {
|
||
options = ''
|
||
sd.value.forEach(opt => {
|
||
options += `<span class="value">${w2utils.formatDate(opt)}</span>`
|
||
})
|
||
}
|
||
} else {
|
||
if (sd.type == 'date') {
|
||
val = w2utils.formatDateTime(val)
|
||
}
|
||
}
|
||
w2tooltip.hide(this.name + '-search-props')
|
||
w2tooltip.show({
|
||
name: this.name + '-search-props',
|
||
anchor: el,
|
||
class: 'w2ui-white',
|
||
hideOn: 'doc-click',
|
||
html: `
|
||
<div class="w2ui-grid-search-single">
|
||
<span class="field">${sf.label}</span>
|
||
<span class="operator">${w2utils.lang(oper)}</span>
|
||
${Array.isArray(sd.value)
|
||
? `${options}`
|
||
: `<span class="value">${val}</span>`
|
||
}
|
||
<div class="buttons">
|
||
<button id="remove" class="w2ui-btn">${w2utils.lang('Remove This Field')}</button>
|
||
</div>
|
||
</div>`
|
||
}).then(event => {
|
||
query(event.detail.overlay.box).find('#remove').on('click', () => {
|
||
this.searchData.splice(`${sd_ind}`, 1)
|
||
this.reload()
|
||
this.localSearch()
|
||
w2tooltip.hide(this.name + '-search-props')
|
||
})
|
||
})
|
||
}
|
||
// drop down with save searches
|
||
searchSuggest(imediate, forceHide, input) {
|
||
clearTimeout(this.last.kbd_timer)
|
||
clearTimeout(this.last.overlay_timer)
|
||
this.searchShowFields(true)
|
||
this.searchClose()
|
||
if (forceHide === true) {
|
||
w2tooltip.hide(this.name + '-search-suggest')
|
||
return
|
||
}
|
||
if (query(`#w2overlay-${this.name}-search-suggest`).length > 0) {
|
||
// already shown
|
||
return
|
||
}
|
||
if (!imediate) {
|
||
this.last.overlay_timer = setTimeout(() => { this.searchSuggest(true) }, 100)
|
||
return
|
||
}
|
||
let el = query(this.box).find(`#grid_${this.name}_search_all`).get(0)
|
||
let searches = [
|
||
...this.defaultSearches ?? [],
|
||
...this.defaultSearches?.length > 0 && this.savedSearches?.length > 0 ? ['--'] : [],
|
||
...this.savedSearches ?? []
|
||
]
|
||
if (Array.isArray(searches) && searches.length > 0) {
|
||
w2menu.show({
|
||
name: this.name + '-search-suggest',
|
||
anchor: el,
|
||
align: 'both',
|
||
items: searches,
|
||
hideOn: ['doc-click', 'sleect', 'remove'],
|
||
render(item) {
|
||
let ret = item.text
|
||
if (item.isDefault) ret = `<b>${ret}</b>`
|
||
return ret
|
||
}
|
||
})
|
||
.select(event => {
|
||
let edata = this.trigger('searchSelect', {
|
||
target: this.name,
|
||
index: event.detail.index,
|
||
item: event.detail.item
|
||
})
|
||
if (edata.isCancelled === true) {
|
||
event.preventDefault()
|
||
return
|
||
}
|
||
event.detail.overlay.hide()
|
||
this.last.logic = event.detail.item.logic || 'AND'
|
||
this.last.search = ''
|
||
this.last.label = '[Multiple Fields]'
|
||
this.searchData = w2utils.clone(event.detail.item.data)
|
||
this.searchSelected = w2utils.clone(event.detail.item, { exclude: ['icon', 'remove'] })
|
||
this.reload()
|
||
edata.finish()
|
||
})
|
||
.remove(event => {
|
||
let item = event.detail.item
|
||
let edata = this.trigger('searchRemove', { target: this.name, index: event.detail.index, item })
|
||
if (edata.isCancelled === true) {
|
||
event.preventDefault()
|
||
return
|
||
}
|
||
event.detail.overlay.hide()
|
||
this.confirm(w2utils.lang('Do you want to delete search "${item}"?', { item: item.text }))
|
||
.yes(evt => {
|
||
// remove from searches
|
||
let search = this.savedSearches.findIndex((s) => s.id == item.id ? true : false)
|
||
if (search !== -1) {
|
||
this.savedSearches.splice(search, 1)
|
||
}
|
||
this.cacheSave('searches', this.savedSearches.map(s => w2utils.clone(s, { exclude: ['remove', 'icon'] })))
|
||
evt.detail.self.close()
|
||
// evt after
|
||
edata.finish()
|
||
})
|
||
.no(evt => {
|
||
evt.detail.self.close()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
searchSave() {
|
||
let value = ''
|
||
if (this.searchSelected) {
|
||
value = this.searchSelected.text
|
||
}
|
||
let ind = this.savedSearches.findIndex(s => { return s.id == this.searchSelected?.id ? true : false })
|
||
// event before
|
||
let edata = this.trigger('searchSave', { target: this.name, saveLocalStorage: true })
|
||
if (edata.isCancelled === true) return
|
||
this.message({
|
||
width: 350,
|
||
height: 150,
|
||
body: `<div class="w2ui-grid-save-search">
|
||
<span>${w2utils.lang(ind != -1 ? 'Update Search' : 'Save New Search')}</span>
|
||
<input class="search-name w2ui-input" placeholder="${w2utils.lang('Search name')}">
|
||
</div>`,
|
||
buttons: `
|
||
<button id="grid-search-cancel" class="w2ui-btn">${w2utils.lang('Cancel')}</button>
|
||
<button id="grid-search-save" class="w2ui-btn w2ui-btn-blue" ${String(value).trim() == '' ? 'disabled': ''}>${w2utils.lang('Save')}</button>
|
||
`
|
||
}).open(async (event) => {
|
||
query(event.detail.box).find('input, button').eq(0).val(value)
|
||
await event.complete
|
||
query(event.detail.box).find('#grid-search-cancel').on('click', () => {
|
||
this.message()
|
||
})
|
||
query(event.detail.box).find('#grid-search-save').on('click', () => {
|
||
let name = query(event.detail.box).find('.w2ui-message .search-name').val()
|
||
// save in savedSearches
|
||
if (this.searchSelected && ind != -1) {
|
||
Object.assign(this.savedSearches[ind], {
|
||
id: name,
|
||
text: name,
|
||
logic: this.last.logic,
|
||
data: w2utils.clone(this.searchData)
|
||
})
|
||
} else {
|
||
this.savedSearches.push({
|
||
id: name,
|
||
text: name,
|
||
icon: 'w2ui-icon-search',
|
||
remove: true,
|
||
logic: this.last.logic,
|
||
data: this.searchData
|
||
})
|
||
}
|
||
// save local storage
|
||
this.cacheSave('searches', this.savedSearches.map(s => w2utils.clone(s, { exclude: ['remove', 'icon'] })))
|
||
this.message()
|
||
// update on screen
|
||
if (this.searchSelected) {
|
||
this.searchSelected.text = name
|
||
query(this.box).find(`#grid_${this.name}_search_name .name-text`).html(name)
|
||
} else {
|
||
this.searchSelected = {
|
||
text: name,
|
||
logic: this.last.logic,
|
||
data: w2utils.clone(this.searchData)
|
||
}
|
||
query(event.detail.box).find(`#grid_${this.name}_search_all`).val(' ').prop('readOnly', true)
|
||
query(event.detail.box).find(`#grid_${this.name}_search_name`).show().find('.name-text').html(name)
|
||
}
|
||
edata.finish({ name })
|
||
})
|
||
query(event.detail.box).find('input, button')
|
||
.off('.message')
|
||
.on('keydown.message', evt => {
|
||
let val = String(query(event.detail.box).find('.w2ui-message-body input').val()).trim()
|
||
if (evt.keyCode == 13 && val != '') {
|
||
query(event.detail.box).find('#grid-search-save').trigger('click') // enter
|
||
}
|
||
if (evt.keyCode == 27) { // escape
|
||
this.message()
|
||
}
|
||
})
|
||
.eq(0)
|
||
.on('input.message', evt => {
|
||
let $save = query(event.detail.box).closest('.w2ui-message').find('#grid-search-save')
|
||
if (String(query(event.detail.box).val()).trim() === '') {
|
||
$save.prop('disabled', true)
|
||
} else {
|
||
$save.prop('disabled', false)
|
||
}
|
||
})
|
||
.get(0)
|
||
.focus()
|
||
})
|
||
}
|
||
cache(type) {
|
||
if (w2utils.hasLocalStorage && this.useLocalStorage) {
|
||
try {
|
||
let data = JSON.parse(localStorage.w2ui || '{}')
|
||
data[(this.stateId || this.name)] ??= {}
|
||
return data[(this.stateId || this.name)][type]
|
||
} catch (e) {
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
cacheSave(type, value) {
|
||
if (w2utils.hasLocalStorage && this.useLocalStorage) {
|
||
try {
|
||
let data = JSON.parse(localStorage.w2ui || '{}')
|
||
data[(this.stateId || this.name)] ??= {}
|
||
data[(this.stateId || this.name)][type] = value
|
||
localStorage.w2ui = JSON.stringify(data)
|
||
return true
|
||
} catch (e) {
|
||
delete localStorage.w2ui
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
searchReset(noReload) {
|
||
let searchData = []
|
||
let hasHiddenSearches = false
|
||
// add hidden searches
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
if (!this.searches[i].hidden || this.searches[i].value == null) continue
|
||
searchData.push({
|
||
field : this.searches[i].field,
|
||
operator : this.searches[i].operator || 'is',
|
||
type : this.searches[i].type,
|
||
value : this.searches[i].value || ''
|
||
})
|
||
hasHiddenSearches = true
|
||
}
|
||
// event before
|
||
let edata = this.trigger('search', { reset: true, target: this.name, searchData: searchData })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
let input = query(this.box).find('#grid_'+ this.name +'_search_all')
|
||
this.searchData = edata.detail.searchData
|
||
this.searchSelected = null
|
||
this.last.search = ''
|
||
this.last.logic = (hasHiddenSearches ? 'AND' : 'OR')
|
||
// --- do not reset to All Fields (I think)
|
||
input.next().hide() // advanced search button
|
||
if (this.searches.length > 0) {
|
||
if (!this.multiSearch || !this.show.searchAll) {
|
||
let tmp = 0
|
||
while (tmp < this.searches.length && (this.searches[tmp].hidden || this.searches[tmp].simple === false)) tmp++
|
||
if (tmp >= this.searches.length) {
|
||
// all searches are hidden
|
||
this.last.field = ''
|
||
this.last.label = ''
|
||
} else {
|
||
this.last.field = this.searches[tmp].field
|
||
this.last.label = this.searches[tmp].label
|
||
}
|
||
} else {
|
||
this.last.field = 'all'
|
||
this.last.label = 'All Fields'
|
||
input.next().show() // advanced search button
|
||
}
|
||
}
|
||
this.last.multi = false
|
||
this.last.fetch.offset = 0
|
||
// reset scrolling position
|
||
this.last.scrollTop = 0
|
||
this.last.scrollLeft = 0
|
||
this.last.selection.indexes = []
|
||
this.last.selection.columns = {}
|
||
// -- clear all search field
|
||
this.searchClose()
|
||
let all = input.val('').get(0)
|
||
if (all?._w2field) { all._w2field.reset() }
|
||
// apply search
|
||
if (!noReload) this.reload()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
searchShowFields(forceHide) {
|
||
if (forceHide === true) {
|
||
w2tooltip.hide(this.name + '-search-fields')
|
||
return
|
||
}
|
||
let items = []
|
||
for (let s = -1; s < this.searches.length; s++) {
|
||
let search = this.searches[s]
|
||
let sField = (search ? search.field : null)
|
||
let column = this.getColumn(sField)
|
||
let disabled = false
|
||
let tooltip = null
|
||
if (this.show.searchHiddenMsg == true && s != -1
|
||
&& (column == null || (column.hidden === true && column.hideable !== false))) {
|
||
disabled = true
|
||
tooltip = w2utils.lang(`This column ${column == null ? 'does not exist' : 'is hidden'}`)
|
||
}
|
||
if (s == -1) { // -1 is All Fields search
|
||
if (!this.multiSearch || !this.show.searchAll) continue
|
||
search = { field: 'all', label: 'All Fields' }
|
||
} else {
|
||
if (column != null && column.hideable === false) continue
|
||
if (search.hidden === true) {
|
||
tooltip = w2utils.lang('This column is hidden')
|
||
// don't show hidden (not simple) searches
|
||
if (search.simple === false) continue
|
||
}
|
||
}
|
||
if (search.label == null && search.caption != null) {
|
||
console.log('NOTICE: grid search.caption property is deprecated, please use search.label. Search ->', search)
|
||
search.label = search.caption
|
||
}
|
||
items.push({
|
||
id: search.field,
|
||
text: w2utils.lang(search.label),
|
||
search,
|
||
tooltip,
|
||
disabled,
|
||
checked: (search.field == this.last.field)
|
||
})
|
||
}
|
||
w2menu.show({
|
||
type: 'radio',
|
||
name: this.name + '-search-fields',
|
||
anchor: query(this.box).find('#grid_'+ this.name +'_search_name').parent().find('.w2ui-search-down').get(0),
|
||
items,
|
||
align: 'none',
|
||
hideOn: ['doc-click', 'select']
|
||
})
|
||
.select(event => {
|
||
this.searchInitInput(event.detail.item.search.field)
|
||
})
|
||
}
|
||
searchInitInput(field, value) {
|
||
let search
|
||
let el = query(this.box).find('#grid_'+ this.name +'_search_all')
|
||
if (field == 'all') {
|
||
search = { field: 'all', label: w2utils.lang('All Fields') }
|
||
} else {
|
||
search = this.getSearch(field)
|
||
if (search == null) return
|
||
}
|
||
// update field
|
||
if (this.last.search != '') {
|
||
this.last.label = search.label
|
||
this.search(search.field, this.last.search)
|
||
} else {
|
||
this.last.field = search.field
|
||
this.last.label = search.label
|
||
}
|
||
el.attr('placeholder', w2utils.lang('Search') + ' ' + w2utils.lang(search.label || search.caption || search.field, true))
|
||
}
|
||
// clears records and related params
|
||
clear(noRefresh) {
|
||
this.total = 0
|
||
this.records = []
|
||
this.summary = []
|
||
this.last.fetch.offset = 0 // need this for reload button to work on remote data set
|
||
this.last.idCache = {} // optimization to free memory
|
||
this.last.selection = { indexes: [], columns: {} }
|
||
this.reset(true)
|
||
// refresh
|
||
if (!noRefresh) this.refresh()
|
||
}
|
||
// clears scroll position, selection, ranges
|
||
reset(noRefresh) {
|
||
// position
|
||
this.last.scrollTop = 0
|
||
this.last.scrollLeft = 0
|
||
this.last.range_start = null
|
||
this.last.range_end = null
|
||
// additional
|
||
query(this.box).find(`#grid_${this.name}_records`).prop('scrollTop', 0)
|
||
// refresh
|
||
if (!noRefresh) this.refresh()
|
||
}
|
||
skip(offset, callBack) {
|
||
let url = this.url?.get ?? this.url
|
||
if (url) {
|
||
this.offset = parseInt(offset)
|
||
if (this.offset > this.total) this.offset = this.total - this.limit
|
||
if (this.offset < 0 || !w2utils.isInt(this.offset)) this.offset = 0
|
||
this.clear(true)
|
||
this.reload(callBack)
|
||
} else {
|
||
console.log('ERROR: grid.skip() can only be called when you have remote data source.')
|
||
}
|
||
}
|
||
load(url, callBack) {
|
||
if (url == null) {
|
||
console.log('ERROR: You need to provide url argument when calling .load() method of "'+ this.name +'" object.')
|
||
return new Promise((resolve, reject) => { reject() })
|
||
}
|
||
// default action
|
||
this.clear(true)
|
||
return this.request('load', {}, url, callBack)
|
||
}
|
||
reload(callBack) {
|
||
let grid = this
|
||
let url = this.url?.get ?? this.url
|
||
grid.selectionSave()
|
||
if (url) {
|
||
// need to remember selection (not just last.selection object)
|
||
return this.load(url, () => {
|
||
grid.selectionRestore()
|
||
if (typeof callBack == 'function') callBack()
|
||
})
|
||
} else {
|
||
this.reset(true)
|
||
this.localSearch()
|
||
this.selectionRestore()
|
||
if (typeof callBack == 'function') callBack({ status: 'success' })
|
||
return new Promise(resolve => { resolve() })
|
||
}
|
||
}
|
||
prepareParams(url, fetchOptions) {
|
||
let dataType = this.dataType ?? w2utils.settings.dataType
|
||
let postParams = fetchOptions.body
|
||
switch (dataType) {
|
||
case 'HTTPJSON':
|
||
postParams = { request: postParams }
|
||
if (['PUT', 'DELETE'].includes(fetchOptions.method)) {
|
||
fetchOptions.method = 'POST'
|
||
}
|
||
body2params()
|
||
break
|
||
case 'HTTP':
|
||
if (['PUT', 'DELETE'].includes(fetchOptions.method)) {
|
||
fetchOptions.method = 'POST'
|
||
}
|
||
body2params()
|
||
break
|
||
case 'RESTFULL':
|
||
if (['PUT', 'DELETE'].includes(fetchOptions.method)) {
|
||
fetchOptions.headers['Content-Type'] = 'application/json'
|
||
} else {
|
||
body2params()
|
||
}
|
||
break
|
||
case 'JSON':
|
||
if (fetchOptions.method == 'GET') {
|
||
postParams = { request: postParams }
|
||
body2params()
|
||
} else {
|
||
fetchOptions.headers['Content-Type'] = 'application/json'
|
||
fetchOptions.method = 'POST'
|
||
}
|
||
break
|
||
}
|
||
fetchOptions.body = typeof fetchOptions.body == 'string' ? fetchOptions.body : JSON.stringify(fetchOptions.body)
|
||
return fetchOptions
|
||
function body2params() {
|
||
Object.keys(postParams).forEach(key => {
|
||
let param = postParams[key]
|
||
if (typeof param == 'object') param = JSON.stringify(param)
|
||
url.searchParams.append(key, param)
|
||
})
|
||
delete fetchOptions.body
|
||
}
|
||
}
|
||
request(action, postData, url, callBack) {
|
||
let self = this
|
||
let resolve, reject
|
||
let requestProm = new Promise((res, rej) => { resolve = res; reject = rej })
|
||
if (postData == null) postData = {}
|
||
if (!url) url = this.url
|
||
if (!url) return new Promise((resolve, reject) => { reject() })
|
||
// build parameters list
|
||
if (!w2utils.isInt(this.offset)) this.offset = 0
|
||
if (!w2utils.isInt(this.last.fetch.offset)) this.last.fetch.offset = 0
|
||
// add list params
|
||
let edata
|
||
let params = {
|
||
limit: this.limit,
|
||
offset: parseInt(this.offset) + parseInt(this.last.fetch.offset),
|
||
searchLogic: this.last.logic,
|
||
search: this.searchData.map((search) => {
|
||
let _search = w2utils.clone(search)
|
||
if (this.searchMap && this.searchMap[_search.field]) _search.field = this.searchMap[_search.field]
|
||
return _search
|
||
}),
|
||
sort: this.sortData.map((sort) => {
|
||
let _sort = w2utils.clone(sort)
|
||
if (this.sortMap && this.sortMap[_sort.field]) _sort.field = this.sortMap[_sort.field]
|
||
return _sort
|
||
})
|
||
}
|
||
if (this.searchData.length === 0) {
|
||
delete params.search
|
||
delete params.searchLogic
|
||
}
|
||
if (this.sortData.length === 0) {
|
||
delete params.sort
|
||
}
|
||
// append other params
|
||
w2utils.extend(params, this.postData)
|
||
w2utils.extend(params, postData)
|
||
// other actions
|
||
if (action == 'delete' || action == 'save') {
|
||
delete params.limit
|
||
delete params.offset
|
||
params.action = action
|
||
if (action == 'delete') {
|
||
params[this.recid || 'recid'] = this.getSelection()
|
||
}
|
||
}
|
||
// event before
|
||
if (action == 'load') {
|
||
edata = this.trigger('request', { target: this.name, url, postData: params, httpMethod: 'GET',
|
||
httpHeaders: this.httpHeaders })
|
||
if (edata.isCancelled === true) return new Promise((resolve, reject) => { reject() })
|
||
} else {
|
||
edata = { detail: {
|
||
url,
|
||
postData: params,
|
||
httpMethod: action == 'save' ? 'PUT' : 'DELETE',
|
||
httpHeaders: this.httpHeaders
|
||
}}
|
||
}
|
||
// call server to get data
|
||
if (this.last.fetch.offset === 0) {
|
||
this.lock(w2utils.lang(this.msgRefresh), true)
|
||
}
|
||
if (this.last.fetch.controller) try { this.last.fetch.controller.abort() } catch (e) {}
|
||
// URL
|
||
url = edata.detail.url
|
||
switch (action) {
|
||
case 'save':
|
||
if (url?.save) url = url.save
|
||
break
|
||
case 'delete':
|
||
if (url?.remove) url = url.remove
|
||
break
|
||
default:
|
||
url = url?.get ?? url
|
||
}
|
||
// process url with routeData
|
||
if (Object.keys(this.routeData).length > 0) {
|
||
let info = w2utils.parseRoute(url)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (this.routeData[info.keys[k].name] == null) continue
|
||
url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
}
|
||
url = new URL(url, location)
|
||
// ajax options
|
||
let fetchOptions = this.prepareParams(url, {
|
||
method: edata.detail.httpMethod,
|
||
headers: edata.detail.httpHeaders,
|
||
body: edata.detail.postData
|
||
})
|
||
Object.assign(this.last.fetch, {
|
||
action: action,
|
||
options: fetchOptions,
|
||
controller: new AbortController(),
|
||
start: Date.now(),
|
||
loaded: false
|
||
})
|
||
fetchOptions.signal = this.last.fetch.controller.signal
|
||
fetch(url, fetchOptions)
|
||
.catch(processError)
|
||
.then(resp => {
|
||
if (resp == null) return // request aborted
|
||
if (resp?.status != 200) {
|
||
processError(resp ?? {})
|
||
return
|
||
}
|
||
self.unlock()
|
||
resp.json()
|
||
.catch(processError)
|
||
.then(data => {
|
||
this.requestComplete(data, action, callBack, resolve, reject)
|
||
})
|
||
})
|
||
if (action == 'load') {
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
return requestProm
|
||
function processError(response) {
|
||
if (response?.name === 'AbortError') {
|
||
// request was aborted by the grid
|
||
return
|
||
}
|
||
self.unlock()
|
||
// trigger event
|
||
let edata2 = self.trigger('error', { response, lastFetch: self.last.fetch })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
if (response.status && response.status != 200) {
|
||
self.error(response.status + ': ' + response.statusText)
|
||
} else {
|
||
console.log('ERROR: Server communication failed.',
|
||
'\n EXPECTED:', { total: 5, records: [{ recid: 1, field: 'value' }] },
|
||
'\n OR:', { error: true, message: 'error message' })
|
||
self.requestComplete({ error: true, message: w2utils.lang(this.msgHTTPError), response }, action, callBack, resolve, reject)
|
||
}
|
||
// event after
|
||
edata2.finish()
|
||
}
|
||
}
|
||
requestComplete(data, action, callBack, resolve, reject) {
|
||
let error = data.error ?? false
|
||
if (data.error == null && data.status === 'error') error = true
|
||
this.last.fetch.response = (Date.now() - this.last.fetch.start) / 1000
|
||
setTimeout(() => {
|
||
if (this.show.statusResponse) {
|
||
this.status(w2utils.lang('Server Response ${count} seconds', { count: this.last.fetch.response }))
|
||
}
|
||
}, 10)
|
||
this.last.pull_more = false
|
||
this.last.pull_refresh = true
|
||
// event before
|
||
let event_name = 'load'
|
||
if (this.last.fetch.action == 'save') event_name = 'save'
|
||
if (this.last.fetch.action == 'delete') event_name = 'delete'
|
||
let edata = this.trigger(event_name, { target: this.name, error, data, lastFetch: this.last.fetch })
|
||
if (edata.isCancelled === true) {
|
||
reject()
|
||
return
|
||
}
|
||
// parse server response
|
||
if (!error) {
|
||
// default action
|
||
if (typeof this.parser == 'function') {
|
||
data = this.parser(data)
|
||
if (typeof data != 'object') {
|
||
console.log('ERROR: Your parser did not return proper object')
|
||
}
|
||
} else {
|
||
if (data == null) {
|
||
data = {
|
||
error: true,
|
||
message: w2utils.lang(this.msgNotJSON),
|
||
}
|
||
} else if (Array.isArray(data)) {
|
||
// if it is plain array, assume these are records
|
||
data = {
|
||
error,
|
||
records: data,
|
||
total: data.length
|
||
}
|
||
}
|
||
}
|
||
if (action == 'load') {
|
||
if (data.total == null) data.total = -1
|
||
if (data.records == null) {
|
||
data.records = []
|
||
}
|
||
if (data.records.length == this.limit) {
|
||
let loaded = this.records.length + data.records.length
|
||
this.last.fetch.hasMore = (loaded == this.total ? false : true)
|
||
} else {
|
||
this.last.fetch.hasMore = false
|
||
this.total = this.offset + this.last.fetch.offset + data.records.length
|
||
}
|
||
if (!this.last.fetch.hasMore) {
|
||
// if no more records, then hide spinner
|
||
query(this.box).find('#grid_'+ this.name +'_rec_more, #grid_'+ this.name +'_frec_more').hide()
|
||
}
|
||
if (this.last.fetch.offset === 0) {
|
||
this.records = []
|
||
this.summary = []
|
||
} else {
|
||
if (data.total != -1 && parseInt(data.total) != parseInt(this.total)) {
|
||
let grid = this
|
||
this.message(w2utils.lang(this.msgNeedReload))
|
||
.ok(() => {
|
||
delete grid.last.fetch.offset
|
||
grid.reload()
|
||
})
|
||
return new Promise(resolve => { resolve() })
|
||
}
|
||
}
|
||
if (w2utils.isInt(data.total)) this.total = parseInt(data.total)
|
||
// records
|
||
if (data.records) {
|
||
data.records.forEach(rec => {
|
||
if (this.recid) {
|
||
rec.recid = this.parseField(rec, this.recid)
|
||
}
|
||
if (rec.recid == null) {
|
||
rec.recid = 'recid-' + this.records.length
|
||
}
|
||
if (rec.w2ui?.summary === true) {
|
||
this.summary.push(rec)
|
||
} else {
|
||
this.records.push(rec)
|
||
}
|
||
})
|
||
}
|
||
// summary records (if any)
|
||
if (data.summary) {
|
||
this.summary = [] // reset summary with each call
|
||
data.summary.forEach(rec => {
|
||
if (this.recid) {
|
||
rec.recid = this.parseField(rec, this.recid)
|
||
}
|
||
if (rec.recid == null) {
|
||
rec.recid = 'recid-' + this.summary.length
|
||
}
|
||
this.summary.push(rec)
|
||
})
|
||
}
|
||
} else if (action == 'delete') {
|
||
this.reset() // unselect old selections
|
||
return this.reload()
|
||
}
|
||
} else {
|
||
this.error(w2utils.lang(data.message ?? this.msgServerError))
|
||
reject(data)
|
||
}
|
||
// event after
|
||
let url = this.url?.get ?? this.url
|
||
if (!url) {
|
||
this.localSort()
|
||
this.localSearch()
|
||
}
|
||
this.total = parseInt(this.total)
|
||
// do not refresh if loading on infinite scroll
|
||
if (this.last.fetch.offset === 0) {
|
||
this.refresh()
|
||
} else {
|
||
this.scroll()
|
||
this.resize()
|
||
}
|
||
// call back
|
||
if (typeof callBack == 'function') callBack(data) // need to be before event:after
|
||
resolve(data)
|
||
// after event
|
||
edata.finish()
|
||
this.last.fetch.loaded = true
|
||
}
|
||
error(msg) {
|
||
// let the management of the error outside of the grid
|
||
let edata = this.trigger('error', { target: this.name, message: msg })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
this.message(msg)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
getChanges(recordsBase) {
|
||
let changes = []
|
||
if (typeof recordsBase == 'undefined') {
|
||
recordsBase = this.records
|
||
}
|
||
for (let r = 0; r < recordsBase.length; r++) {
|
||
let rec = recordsBase[r]
|
||
if (rec?.w2ui) {
|
||
if (rec.w2ui.changes != null) {
|
||
let obj = {}
|
||
obj[this.recid || 'recid'] = rec.recid
|
||
changes.push(w2utils.extend(obj, rec.w2ui.changes))
|
||
}
|
||
// recursively look for changes in non-expanded children
|
||
if (rec.w2ui.expanded !== true && rec.w2ui.children && rec.w2ui.children.length) {
|
||
changes.push(...this.getChanges(rec.w2ui.children))
|
||
}
|
||
}
|
||
}
|
||
return changes
|
||
}
|
||
mergeChanges() {
|
||
let changes = this.getChanges()
|
||
for (let c = 0; c < changes.length; c++) {
|
||
let record = this.get(changes[c][this.recid || 'recid'])
|
||
for (let s in changes[c]) {
|
||
if (s == 'recid' || (this.recid && s == this.recid)) continue // do not allow to change recid
|
||
if (typeof changes[c][s] === 'object') changes[c][s] = changes[c][s].text
|
||
try {
|
||
_setValue(record, s, changes[c][s])
|
||
} catch (e) {
|
||
console.log('ERROR: Cannot merge. ', e.message || '', e)
|
||
}
|
||
if (record.w2ui) delete record.w2ui.changes
|
||
}
|
||
}
|
||
this.refresh()
|
||
function _setValue(obj, field, value) {
|
||
let fld = field.split('.')
|
||
if (fld.length == 1) {
|
||
obj[field] = value
|
||
} else {
|
||
obj = obj[fld[0]]
|
||
fld.shift()
|
||
_setValue(obj, fld.join('.'), value)
|
||
}
|
||
}
|
||
}
|
||
save(callBack) {
|
||
let changes = this.getChanges()
|
||
let url = this.url?.save ?? this.url
|
||
// event before
|
||
let edata = this.trigger('save', { target: this.name, changes: changes })
|
||
if (edata.isCancelled === true) return
|
||
if (url) {
|
||
this.request('save', { 'changes' : edata.detail.changes }, null,
|
||
(data) => {
|
||
if (!data.error) {
|
||
// only merge changes, if save was successful
|
||
this.mergeChanges()
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
// call back
|
||
if (typeof callBack == 'function') callBack(data)
|
||
}
|
||
)
|
||
} else {
|
||
this.mergeChanges()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
editField(recid, column, value, event) {
|
||
let self = this
|
||
if (this.last.inEditMode === true) {
|
||
// This is triggerign when user types fast
|
||
if (event && event.keyCode == 13) {
|
||
let { index, column, value } = this.last._edit
|
||
this.editChange({ type: 'custom', value }, index, column, event)
|
||
this.editDone(index, column, event)
|
||
} else {
|
||
// when 2 chars entered fast (spreadsheet)
|
||
let input = query(this.box).find('div.w2ui-edit-box .w2ui-input')
|
||
if (input.length > 0) {
|
||
if (input.get(0).tagName == 'DIV') {
|
||
input.text(input.text() + value)
|
||
w2utils.setCursorPosition(input.get(0), input.text().length)
|
||
} else {
|
||
input.val(input.val() + value)
|
||
w2utils.setCursorPosition(input.get(0), input.val().length)
|
||
}
|
||
}
|
||
}
|
||
return
|
||
}
|
||
let index = this.get(recid, true)
|
||
let edit = this.getCellEditable(index, column)
|
||
if (!edit || ['checkbox', 'check'].includes(edit.type)) return
|
||
let rec = this.records[index]
|
||
let col = this.columns[column]
|
||
let prefix = (col.frozen === true ? '_f' : '_')
|
||
if (['list', 'enum', 'file'].indexOf(edit.type) != -1) {
|
||
console.log('ERROR: input types "list", "enum" and "file" are not supported in inline editing.')
|
||
return
|
||
}
|
||
// event before
|
||
let edata = this.trigger('editField', { target: this.name, recid, column, value, index, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
value = edata.detail.value
|
||
// default behaviour
|
||
this.last.inEditMode = true
|
||
this.last.editColumn = column
|
||
this.last._edit = { value: value, index: index, column: column, recid: recid }
|
||
this.selectNone(true) // no need to trigger select event
|
||
this.select({ recid: recid, column: column })
|
||
// create input element
|
||
let tr = query(this.box).find('#grid_'+ this.name + prefix +'rec_' + w2utils.escapeId(recid))
|
||
let div = tr.find('[col="'+ column +'"] > div') // TD -> DIV
|
||
this.last._edit.tr = tr
|
||
this.last._edit.div = div
|
||
// clear previous if any (spreadsheet)
|
||
query(this.box).find('div.w2ui-edit-box').remove()
|
||
// for spreadsheet - insert into selection
|
||
if (this.selectType != 'row') {
|
||
query(this.box).find('#grid_'+ this.name + prefix + 'selection')
|
||
.attr('id', 'grid_'+ this.name + '_editable')
|
||
.removeClass('w2ui-selection')
|
||
.addClass('w2ui-edit-box')
|
||
.prepend('<div style="position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px;"></div>')
|
||
.find('.w2ui-selection-resizer')
|
||
.remove()
|
||
div = query(this.box).find('#grid_'+ this.name + '_editable > div:first-child')
|
||
}
|
||
edit.attr = edit.attr ?? ''
|
||
edit.text = edit.text ?? ''
|
||
edit.style = edit.style ?? ''
|
||
edit.items = edit.items ?? []
|
||
let val = (rec.w2ui?.changes?.[col.field] != null
|
||
? w2utils.stripTags(rec.w2ui.changes[col.field])
|
||
: w2utils.stripTags(self.parseField(rec, col.field)))
|
||
if (val == null) val = ''
|
||
let prevValue = (typeof val != 'object' ? val : '')
|
||
if (edata.detail.prevValue != null) prevValue = edata.detail.prevValue
|
||
if (value != null) val = value
|
||
let addStyle = (col.style != null ? col.style + ';' : '')
|
||
if (typeof col.render == 'string'
|
||
&& ['number', 'int', 'float', 'money', 'percent', 'size'].includes(col.render.split(':')[0])) {
|
||
addStyle += 'text-align: right;'
|
||
}
|
||
// normalize items, if not yet normlized
|
||
if (edit.items.length > 0 && !w2utils.isPlainObject(edit.items[0])) {
|
||
edit.items = w2utils.normMenu(edit.items)
|
||
}
|
||
let input
|
||
let dropTypes = ['date', 'time', 'datetime', 'color', 'list', 'combo']
|
||
let styles = getComputedStyle(tr.find('[col="'+ column +'"] > div').get(0))
|
||
let font = `font-family: ${styles['font-family']}; font-size: ${styles['font-size']};`
|
||
switch (edit.type) {
|
||
case 'div': {
|
||
div.addClass('w2ui-editable')
|
||
.html(w2utils.stripSpaces(`<div id="grid_${this.name}_edit_${recid}_${column}" class="w2ui-input w2ui-focus"
|
||
contenteditable autocorrect="off" autocomplete="off" spellcheck="false"
|
||
style="${font + addStyle + edit.style}"
|
||
field="${col.field}" recid="${recid}" column="${column}" ${edit.attr}>
|
||
</div>${edit.text}`))
|
||
input = div.find('div.w2ui-input').get(0)
|
||
input.innerText = (typeof val != 'object' ? val : '')
|
||
if (value != null) {
|
||
w2utils.setCursorPosition(input, input.innerText.length)
|
||
} else {
|
||
w2utils.setCursorPosition(input, 0, input.innerText.length)
|
||
}
|
||
break
|
||
}
|
||
default: {
|
||
div.addClass('w2ui-editable')
|
||
.html(w2utils.stripSpaces(`<input id="grid_${this.name}_edit_${recid}_${column}" class="w2ui-input"
|
||
autocorrect="off" autocomplete="off" spellcheck="false" type="text"
|
||
style="${font + addStyle + edit.style}"
|
||
field="${col.field}" recid="${recid}" column="${column}" ${edit.attr}>${edit.text}`))
|
||
input = div.find('input').get(0)
|
||
// issue #499
|
||
if (edit.type == 'number') {
|
||
val = w2utils.formatNumber(val)
|
||
}
|
||
if (edit.type == 'date') {
|
||
val = w2utils.formatDate(w2utils.isDate(val, edit.format, true) || new Date(), edit.format)
|
||
}
|
||
input.value = (typeof val != 'object' ? val : '')
|
||
// init w2field, attached to input._w2field
|
||
let doHide = (event) => {
|
||
let escKey = this.last._edit?.escKey
|
||
// check if any element is selected in drop down
|
||
let selected = false
|
||
let name = query(input).data('tooltipName')
|
||
if (name && w2tooltip.get(name[0])?.selected != null) {
|
||
selected = true
|
||
}
|
||
// trigger change on new value if selected from overlay
|
||
if (this.last.inEditMode && !escKey && dropTypes.includes(edit.type) // drop down types
|
||
&& (event.detail.overlay.anchor?.id == this.last._edit.input?.id || edit.type == 'list')) {
|
||
this.editChange()
|
||
this.editDone(undefined, undefined, { keyCode: selected ? 13 : 0 }) // advance on select
|
||
}
|
||
}
|
||
new w2field(w2utils.extend({}, edit, {
|
||
el: input,
|
||
selected: val,
|
||
onSelect: doHide,
|
||
onHide: doHide
|
||
}))
|
||
if (value == null && input) {
|
||
// if no new value, then select content
|
||
input.select()
|
||
}
|
||
}
|
||
}
|
||
Object.assign(this.last._edit, { input, edit })
|
||
query(input)
|
||
.off('.w2ui-editable')
|
||
.on('blur.w2ui-editable', (event) => {
|
||
if (this.last.inEditMode) {
|
||
let type = this.last._edit.edit.type
|
||
let name = query(input).data('tooltipName') // if popup is open
|
||
if (dropTypes.includes(type) && name) {
|
||
// drop downs finish edit when popover is closed
|
||
return
|
||
}
|
||
this.editChange(input, index, column, event)
|
||
this.editDone()
|
||
}
|
||
})
|
||
.on('mousedown.w2ui-editable', (event) => {
|
||
event.stopPropagation()
|
||
})
|
||
.on('click.w2ui-editable', (event) => {
|
||
expand.call(input, event)
|
||
})
|
||
.on('paste.w2ui-editable', (event) => {
|
||
// clean paste to be plain text
|
||
event.preventDefault()
|
||
let text = event.clipboardData.getData('text/plain')
|
||
document.execCommand('insertHTML', false, text)
|
||
})
|
||
.on('keyup.w2ui-editable', (event) => {
|
||
expand.call(input, event)
|
||
})
|
||
.on('keydown.w2ui-editable', (event) => {
|
||
switch (event.keyCode) {
|
||
case 8: // backspace;
|
||
if (edit.type == 'list' && !input._w2field) { // cancel backspace when deleting element
|
||
event.preventDefault()
|
||
}
|
||
break
|
||
case 9:
|
||
case 13:
|
||
event.preventDefault()
|
||
break
|
||
case 27: // esc button exits edit mode, but if in a popup, it will also close the popup, hence
|
||
// if tooltip is open - hide it
|
||
let name = query(input).data('tooltipName')
|
||
if (name && name.length > 0) {
|
||
this.last._edit.escKey = true
|
||
w2tooltip.hide(name[0])
|
||
event.preventDefault()
|
||
}
|
||
event.stopPropagation()
|
||
break
|
||
}
|
||
// need timeout so, this handler is executed after key is processed by browser
|
||
setTimeout(() => {
|
||
switch (event.keyCode) {
|
||
case 9: { // tab
|
||
let next = event.shiftKey
|
||
? self.prevCell(index, column, true)
|
||
: self.nextCell(index, column, true)
|
||
if (next != null) {
|
||
let recid = self.records[next.index].recid
|
||
this.editChange(input, index, column, event)
|
||
this.editDone(index, column, event)
|
||
if (self.selectType != 'row') {
|
||
self.selectNone(true) // no need to trigger select event
|
||
self.select({ recid, column: next.colIndex })
|
||
} else {
|
||
self.editField(recid, next.colIndex, null, event)
|
||
}
|
||
if (event.preventDefault) event.preventDefault()
|
||
}
|
||
break
|
||
}
|
||
case 13: { // enter
|
||
// check if any element is selected in drop down
|
||
let selected = false
|
||
let name = query(input).data('tooltipName')
|
||
if (name && w2tooltip.get(name[0]).selected != null) {
|
||
selected = true
|
||
}
|
||
// if tooltip is not open or no element is selected
|
||
if (!name || !selected) {
|
||
this.editChange(input, index, column, event)
|
||
this.editDone(index, column, event)
|
||
}
|
||
break
|
||
}
|
||
case 27: { // escape
|
||
this.last._edit.escKey = false
|
||
let old = self.parseField(rec, col.field)
|
||
if (rec.w2ui?.changes?.[col.field] != null) old = rec.w2ui.changes[col.field]
|
||
if (input._prevValue != null) old = input._prevValue
|
||
if (input.tagName == 'DIV') {
|
||
input.innerText = old != null ? old : ''
|
||
} else {
|
||
input.value = old != null ? old : ''
|
||
}
|
||
this.editDone(index, column, event)
|
||
setTimeout(() => { self.select({ recid: recid, column: column }) }, 1)
|
||
break
|
||
}
|
||
}
|
||
// if input too small - expand
|
||
expand(input)
|
||
}, 1)
|
||
})
|
||
// save previous value
|
||
if (input) input._prevValue = prevValue
|
||
// focus and select
|
||
setTimeout(() => {
|
||
if (!this.last.inEditMode) return
|
||
if (input) {
|
||
input.focus()
|
||
clearTimeout(this.last.kbd_timer) // keep focus
|
||
input.resize = expand
|
||
expand(input)
|
||
}
|
||
}, 50)
|
||
// event after
|
||
edata.finish({ input })
|
||
return
|
||
function expand(input) {
|
||
try {
|
||
let styles = getComputedStyle(input)
|
||
let val = (input.tagName.toUpperCase() == 'DIV' ? input.innerText : input.value)
|
||
let editBox = query(self.box).find('#grid_'+ self.name + '_editable').get(0)
|
||
let style = `font-family: ${styles['font-family']}; font-size: ${styles['font-size']}; white-space: no-wrap;`
|
||
let width = w2utils.getStrWidth(val, style)
|
||
if (width + 20 > editBox.clientWidth) {
|
||
query(editBox).css('width', width + 20 + 'px')
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
}
|
||
editChange(input, index, column, event) {
|
||
// if params are not specified
|
||
input = input ?? this.last._edit.input
|
||
index = index ?? this.last._edit.index
|
||
column = column ?? this.last._edit.column
|
||
event = event ?? {}
|
||
// all other fields
|
||
let summary = index < 0
|
||
index = index < 0 ? -index - 1 : index
|
||
let records = summary ? this.summary : this.records
|
||
let rec = records[index]
|
||
let col = this.columns[column]
|
||
let new_val = (input?.tagName == 'DIV' ? input.innerText : input.value)
|
||
let fld = input._w2field
|
||
if (fld) {
|
||
if (fld.type == 'list') {
|
||
new_val = fld.selected
|
||
}
|
||
if (Object.keys(new_val).length === 0 || new_val == null) new_val = ''
|
||
if (!w2utils.isPlainObject(new_val)) new_val = fld.clean(new_val)
|
||
}
|
||
if (input.type == 'checkbox') {
|
||
if (rec.w2ui?.editable === false) input.checked = !input.checked
|
||
new_val = input.checked
|
||
}
|
||
let old_val = this.parseField(rec, col.field)
|
||
let prev_val = (rec.w2ui?.changes && rec.w2ui.changes.hasOwnProperty(col.field) ? rec.w2ui.changes[col.field]: old_val)
|
||
// change/restore event
|
||
let edata = {
|
||
target: this.name, input,
|
||
recid: rec.recid, index, column,
|
||
originalEvent: event,
|
||
value: {
|
||
new: new_val,
|
||
previous: prev_val,
|
||
original: old_val,
|
||
}
|
||
}
|
||
if (event.target?._prevValue != null) edata.value.previous = event.target._prevValue
|
||
let count = 0 // just in case to avoid infinite loop
|
||
while (count < 20) {
|
||
count++
|
||
new_val = edata.value.new
|
||
if ((typeof new_val != 'object' && String(old_val) != String(new_val)) ||
|
||
(typeof new_val == 'object' && new_val && new_val.id != old_val
|
||
&& (typeof old_val != 'object' || old_val == null || new_val.id != old_val.id))) {
|
||
// change event
|
||
edata = this.trigger('change', edata)
|
||
if (edata.isCancelled !== true) {
|
||
if (new_val !== edata.detail.value.new) {
|
||
// re-evaluate the type of change to be made
|
||
continue
|
||
}
|
||
// default action
|
||
if ((edata.detail.value.new === '' || edata.detail.value.new == null) && (prev_val === '' || prev_val == null)) {
|
||
// value did not change, was empty is empty
|
||
} else {
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
rec.w2ui.changes = rec.w2ui.changes ?? {}
|
||
rec.w2ui.changes[col.field] = edata.detail.value.new
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
} else {
|
||
// restore event
|
||
edata = this.trigger('restore', edata)
|
||
if (edata.isCancelled !== true) {
|
||
if (new_val !== edata.detail.value.new) {
|
||
// re-evaluate the type of change to be made
|
||
continue
|
||
}
|
||
// default action
|
||
if (rec.w2ui?.changes) {
|
||
delete rec.w2ui.changes[col.field]
|
||
if (Object.keys(rec.w2ui.changes).length === 0) {
|
||
delete rec.w2ui.changes
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
editDone(index, column, event) {
|
||
// if params are not specified
|
||
index = index ?? this.last._edit.index
|
||
column = column ?? this.last._edit.column
|
||
event = event ?? {}
|
||
// removal of input happens when TR is redrawn
|
||
if (this.advanceOnEdit && event.keyCode == 13) {
|
||
let next = event.shiftKey ? this.prevRow(index, column, 1) : this.nextRow(index, column, 1)
|
||
if (next == null) next = index // keep the same
|
||
setTimeout(() => {
|
||
if (this.selectType != 'row') {
|
||
this.selectNone(true) // no need to trigger select event
|
||
this.select({ recid: this.records[next].recid, column: column })
|
||
} else {
|
||
this.editField(this.records[next].recid, column, null, event)
|
||
}
|
||
}, 1)
|
||
}
|
||
let summary = index < 0
|
||
let cell = query(this.last._edit.tr).find('[col="'+ column +'"]')
|
||
let rec = this.records[index]
|
||
let col = this.columns[column]
|
||
// need to set before remove, as remove will trigger blur
|
||
this.last.inEditMode = false
|
||
this.last._edit = null
|
||
// remove - by updating cell data
|
||
if (!summary) {
|
||
if (rec.w2ui?.changes?.[col.field] != null) {
|
||
cell.addClass('w2ui-changed')
|
||
} else {
|
||
cell.removeClass('w2ui-changed')
|
||
}
|
||
cell.replace(this.getCellHTML(index, column, summary))
|
||
}
|
||
// remove - spreadsheet
|
||
query(this.box).find('div.w2ui-edit-box').remove()
|
||
// update toolbar buttons
|
||
this.updateToolbar()
|
||
// keep grid in focus if needed
|
||
setTimeout(() => {
|
||
let input = query(this.box).find(`#grid_${this.name}_focus`).get(0)
|
||
if (document.activeElement !== input && !this.last.inEditMode) {
|
||
input.focus()
|
||
}
|
||
}, 10)
|
||
}
|
||
'delete'(force) {
|
||
// event before
|
||
let edata = this.trigger('delete', { target: this.name, force: force })
|
||
if (force) this.message() // close message
|
||
if (edata.isCancelled === true) return
|
||
force = edata.detail.force
|
||
// default action
|
||
let recs = this.getSelection()
|
||
if (recs.length === 0) return
|
||
if (this.msgDelete != '' && !force) {
|
||
this.confirm({
|
||
text: w2utils.lang(this.msgDelete, {
|
||
count: recs.length,
|
||
records: w2utils.lang( recs.length == 1 ? 'record' : 'records')
|
||
}),
|
||
width: 380,
|
||
height: 170,
|
||
yes_text: 'Delete',
|
||
yes_class: 'w2ui-btn-red',
|
||
no_text: 'Cancel',
|
||
})
|
||
.yes(event => {
|
||
event.detail.self.close()
|
||
this.delete(true)
|
||
})
|
||
.no(event => {
|
||
event.detail.self.close()
|
||
})
|
||
return
|
||
}
|
||
// call delete script
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.remove)
|
||
if (url) {
|
||
this.request('delete')
|
||
} else {
|
||
if (typeof recs[0] != 'object') {
|
||
this.selectNone()
|
||
this.remove.apply(this, recs)
|
||
} else {
|
||
// clear cells
|
||
for (let r = 0; r < recs.length; r++) {
|
||
let fld = this.columns[recs[r].column].field
|
||
let ind = this.get(recs[r].recid, true)
|
||
let rec = this.records[ind]
|
||
if (ind != null && fld != 'recid') {
|
||
this.records[ind][fld] = ''
|
||
if (rec.w2ui?.changes) delete rec.w2ui.changes[fld]
|
||
// -- style should not be deleted
|
||
// if (rec.style != null && w2utils.isPlainObject(rec.style) && rec.style[recs[r].column]) {
|
||
// delete rec.style[recs[r].column];
|
||
// }
|
||
}
|
||
}
|
||
this.update()
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
click(recid, event) {
|
||
let time = Date.now()
|
||
let column = null
|
||
if (this.last.cancelClick == true || (event && event.altKey)) return
|
||
if ((typeof recid == 'object') && (recid !== null)) {
|
||
column = recid.column
|
||
recid = recid.recid
|
||
}
|
||
if (event == null) event = {}
|
||
// check for double click
|
||
if (time - parseInt(this.last.click_time) < 350 && this.last.click_recid == recid && event.type == 'click') {
|
||
this.dblClick(recid, event)
|
||
return
|
||
}
|
||
// hide bubble
|
||
if (this.last.bubbleEl) {
|
||
this.last.bubbleEl = null
|
||
}
|
||
this.last.click_time = time
|
||
let last_recid = this.last.click_recid
|
||
this.last.click_recid = recid
|
||
// column user clicked on
|
||
if (column == null && event.target) {
|
||
let trg = event.target
|
||
if (trg.tagName != 'TD') trg = query(trg).closest('td')[0]
|
||
if (query(trg).attr('col') != null) column = parseInt(query(trg).attr('col'))
|
||
}
|
||
// event before
|
||
let edata = this.trigger('click', { target: this.name, recid, column, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
let sel = this.getSelection()
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', false)
|
||
let ind = this.get(recid, true)
|
||
let selectColumns = []
|
||
this.last.sel_ind = ind
|
||
this.last.sel_col = column
|
||
this.last.sel_recid = recid
|
||
this.last.sel_type = 'click'
|
||
// multi select with shift key
|
||
let start, end, t1, t2
|
||
if (event.shiftKey && sel.length > 0 && this.multiSelect) {
|
||
if (sel[0].recid) {
|
||
start = this.get(sel[0].recid, true)
|
||
end = this.get(recid, true)
|
||
if (column > sel[0].column) {
|
||
t1 = sel[0].column
|
||
t2 = column
|
||
} else {
|
||
t1 = column
|
||
t2 = sel[0].column
|
||
}
|
||
for (let c = t1; c <= t2; c++) selectColumns.push(c)
|
||
} else {
|
||
start = this.get(last_recid, true)
|
||
end = this.get(recid, true)
|
||
}
|
||
let sel_add = []
|
||
if (start > end) { let tmp = start; start = end; end = tmp }
|
||
let url = this.url?.get ? this.url.get : this.url
|
||
for (let i = start; i <= end; i++) {
|
||
if (this.searchData.length > 0 && !url && !this.last.searchIds.includes(i)) continue
|
||
if (this.selectType == 'row') {
|
||
sel_add.push(this.records[i].recid)
|
||
} else {
|
||
for (let sc = 0; sc < selectColumns.length; sc++) {
|
||
sel_add.push({ recid: this.records[i].recid, column: selectColumns[sc] })
|
||
}
|
||
}
|
||
//sel.push(this.records[i].recid);
|
||
}
|
||
this.select(sel_add)
|
||
} else {
|
||
let last = this.last.selection
|
||
let flag = (last.indexes.indexOf(ind) != -1 ? true : false)
|
||
let fselect = false
|
||
// if clicked on the checkbox
|
||
if (query(event.target).closest('td').hasClass('w2ui-col-select')) fselect = true
|
||
// clear other if necessary
|
||
if (((!event.ctrlKey && !event.shiftKey && !event.metaKey && !fselect) || !this.multiSelect) && !this.showSelectColumn) {
|
||
if (this.selectType != 'row' && !last.columns[ind]?.includes(column)) flag = false
|
||
this.selectNone(true) // no need to trigger select event
|
||
if (flag === true && sel.length == 1) {
|
||
this.unselect({ recid: recid, column: column })
|
||
} else {
|
||
this.select({ recid: recid, column: column })
|
||
}
|
||
} else {
|
||
if (this.selectType != 'row' && !last.columns[ind]?.includes(column)) flag = false
|
||
if (flag === true) {
|
||
this.unselect({ recid: recid, column: column })
|
||
} else {
|
||
this.select({ recid: recid, column: column })
|
||
}
|
||
}
|
||
}
|
||
this.status()
|
||
this.initResize()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
columnClick(field, event) {
|
||
// ignore click if column was resized
|
||
if (this.last.colResizing === true) {
|
||
return
|
||
}
|
||
// event before
|
||
let edata = this.trigger('columnClick', { target: this.name, field: field, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default behaviour
|
||
if (this.selectType == 'row') {
|
||
let column = this.getColumn(field)
|
||
if (column && column.sortable) this.sort(field, null, (event && (event.ctrlKey || event.metaKey) ? true : false))
|
||
if (edata.detail.field == 'line-number') {
|
||
if (this.getSelection().length >= this.records.length) {
|
||
this.selectNone()
|
||
} else {
|
||
this.selectAll()
|
||
}
|
||
}
|
||
} else {
|
||
if (event.altKey){
|
||
let column = this.getColumn(field)
|
||
if (column && column.sortable) this.sort(field, null, (event && (event.ctrlKey || event.metaKey) ? true : false))
|
||
}
|
||
// select entire column
|
||
if (edata.detail.field == 'line-number') {
|
||
if (this.getSelection().length >= this.records.length) {
|
||
this.selectNone()
|
||
} else {
|
||
this.selectAll()
|
||
}
|
||
} else {
|
||
if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
|
||
this.selectNone(true)
|
||
}
|
||
let tmp = this.getSelection()
|
||
let column = this.getColumn(edata.detail.field, true)
|
||
let sel = []
|
||
let cols = []
|
||
// check if there was a selection before
|
||
if (tmp.length != 0 && event.shiftKey) {
|
||
let start = column
|
||
let end = tmp[0].column
|
||
if (start > end) {
|
||
start = tmp[0].column
|
||
end = column
|
||
}
|
||
for (let i = start; i<=end; i++) cols.push(i)
|
||
} else {
|
||
cols.push(column)
|
||
}
|
||
edata = this.trigger('columnSelect', { target: this.name, columns: cols })
|
||
if (edata.isCancelled !== true) {
|
||
for (let i = 0; i < this.records.length; i++) {
|
||
sel.push({ recid: this.records[i].recid, column: cols })
|
||
}
|
||
this.select(sel)
|
||
}
|
||
edata.finish()
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
columnDblClick(field, event) {
|
||
// event before
|
||
let edata = this.trigger('columnDblClick', { target: this.name, field: field, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
focus(event) {
|
||
// event before
|
||
let edata = this.trigger('focus', { target: this.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return false
|
||
// default behaviour
|
||
this.hasFocus = true
|
||
query(this.box).removeClass('w2ui-inactive').find('.w2ui-inactive').removeClass('w2ui-inactive')
|
||
setTimeout(() => {
|
||
let txt = query(this.box).find(`#grid_${this.name}_focus`).get(0)
|
||
if (txt && document.activeElement != txt) {
|
||
txt.focus()
|
||
}
|
||
}, 10)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
blur(event) {
|
||
// event before
|
||
let edata = this.trigger('blur', { target: this.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return false
|
||
// default behaviour
|
||
this.hasFocus = false
|
||
query(this.box).addClass('w2ui-inactive').find('.w2ui-selected').addClass('w2ui-inactive')
|
||
query(this.box).find('.w2ui-selection').addClass('w2ui-inactive')
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
keydown(event) {
|
||
// this method is called from w2utils
|
||
let obj = this
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (obj.keyboard !== true) return
|
||
// trigger event
|
||
let edata = obj.trigger('keydown', { target: obj.name, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
if (query(this.box).find('.w2ui-message').length > 0) {
|
||
// if there are messages
|
||
if (event.keyCode == 27) this.message()
|
||
return
|
||
}
|
||
let empty = false
|
||
let records = query(obj.box).find('#grid_'+ obj.name +'_records')
|
||
let sel = obj.getSelection()
|
||
if (sel.length === 0) empty = true
|
||
let recid = sel[0] || null
|
||
let columns = []
|
||
let recid2 = sel[sel.length-1]
|
||
if (typeof recid == 'object' && recid != null) {
|
||
recid = sel[0].recid
|
||
columns = []
|
||
let ii = 0
|
||
while (true) {
|
||
if (!sel[ii] || sel[ii].recid != recid) break
|
||
columns.push(sel[ii].column)
|
||
ii++
|
||
}
|
||
recid2 = sel[sel.length-1].recid
|
||
}
|
||
let ind = obj.get(recid, true)
|
||
let ind2 = obj.get(recid2, true)
|
||
let recEL = query(obj.box).find(`#grid_${obj.name}_rec_${(ind != null ? w2utils.escapeId(obj.records[ind].recid) : 'none')}`)
|
||
let pageSize = Math.floor(records[0].clientHeight / obj.recordHeight)
|
||
let cancel = false
|
||
let key = event.keyCode
|
||
let shiftKey = event.shiftKey
|
||
switch (key) {
|
||
case 8: // backspace
|
||
case 46: // delete
|
||
// delete if button is visible
|
||
obj.delete()
|
||
cancel = true
|
||
event.stopPropagation()
|
||
break
|
||
case 27: // escape
|
||
obj.selectNone()
|
||
cancel = true
|
||
break
|
||
case 65: // cmd + A
|
||
if (!event.metaKey && !event.ctrlKey) break
|
||
obj.selectAll()
|
||
cancel = true
|
||
break
|
||
case 13: // enter
|
||
// if expandable columns - expand it
|
||
if (this.selectType == 'row' && obj.show.expandColumn === true) {
|
||
if (recEL.length <= 0) break
|
||
obj.toggle(recid, event)
|
||
cancel = true
|
||
} else { // or enter edit
|
||
for (let c = 0; c < this.columns.length; c++) {
|
||
let edit = this.getCellEditable(ind, c)
|
||
if (edit) {
|
||
columns.push(parseInt(c))
|
||
break
|
||
}
|
||
}
|
||
// edit last column that was edited
|
||
if (this.selectType == 'row' && this.last._edit && this.last._edit.column) {
|
||
columns = [this.last._edit.column]
|
||
}
|
||
if (columns.length > 0) {
|
||
obj.editField(recid, this.last.editColumn || columns[0], null, event)
|
||
cancel = true
|
||
}
|
||
}
|
||
break
|
||
case 37: // left
|
||
moveLeft()
|
||
break
|
||
case 39: // right
|
||
moveRight()
|
||
break
|
||
case 33: // <PgUp>
|
||
moveUp(pageSize)
|
||
break
|
||
case 34: // <PgDn>
|
||
moveDown(pageSize)
|
||
break
|
||
case 35: // <End>
|
||
moveDown(-1)
|
||
break
|
||
case 36: // <Home>
|
||
moveUp(-1)
|
||
break
|
||
case 38: // up
|
||
// ctrl (or cmd) + up -> same as home
|
||
moveUp(event.metaKey || event.ctrlKey ? -1 : 1)
|
||
break
|
||
case 40: // down
|
||
// ctrl (or cmd) + up -> same as end
|
||
moveDown(event.metaKey || event.ctrlKey ? -1 : 1)
|
||
break
|
||
// copy & paste
|
||
case 17: // ctrl key
|
||
case 91: // cmd key
|
||
// SLOW: 10k records take 7.0
|
||
if (empty) break
|
||
// in Safari need to copy to buffer on cmd or ctrl key (otherwise does not work)
|
||
if (w2utils.isSafari) {
|
||
obj.last.copy_event = obj.copy(false, event)
|
||
let focus = query(obj.box).find('#grid_'+ obj.name + '_focus')
|
||
focus.val(obj.last.copy_event.detail.text)
|
||
focus[0].select()
|
||
}
|
||
break
|
||
case 67: // - c
|
||
// this fill trigger event.onComplete
|
||
if (event.metaKey || event.ctrlKey) {
|
||
if (w2utils.isSafari) {
|
||
obj.copy(obj.last.copy_event, event)
|
||
} else {
|
||
obj.last.copy_event = obj.copy(false, event)
|
||
let focus = query(obj.box).find('#grid_'+ obj.name + '_focus')
|
||
focus.val(obj.last.copy_event.detail.text)
|
||
focus[0].select()
|
||
obj.copy(obj.last.copy_event, event)
|
||
}
|
||
}
|
||
break
|
||
case 88: // x - cut
|
||
if (empty) break
|
||
if (event.ctrlKey || event.metaKey) {
|
||
if (w2utils.isSafari) {
|
||
obj.copy(obj.last.copy_event, event)
|
||
} else {
|
||
obj.last.copy_event = obj.copy(false, event)
|
||
let focus = query(obj.box).find('#grid_'+ obj.name + '_focus')
|
||
focus.val(obj.last.copy_event.detail.text)
|
||
focus[0].select()
|
||
obj.copy(obj.last.copy_event, event)
|
||
}
|
||
}
|
||
break
|
||
}
|
||
let tmp = [32, 187, 189, 192, 219, 220, 221, 186, 222, 188, 190, 191] // other typeable chars
|
||
for (let i = 48; i <= 111; i++) tmp.push(i) // 0-9,a-z,A-Z,numpad
|
||
if (tmp.indexOf(key) != -1 && !event.ctrlKey && !event.metaKey && !cancel) {
|
||
if (columns.length === 0) columns.push(0)
|
||
cancel = false
|
||
// move typed key into edit
|
||
setTimeout(() => {
|
||
let focus = query(obj.box).find('#grid_'+ obj.name + '_focus')
|
||
let key = focus.val()
|
||
focus.val('')
|
||
obj.editField(recid, columns[0], key, event)
|
||
}, 1)
|
||
}
|
||
if (cancel) { // cancel default behaviour
|
||
if (event.preventDefault) event.preventDefault()
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
function moveLeft() {
|
||
if (empty) { // no selection
|
||
selectTopRecord()
|
||
return
|
||
}
|
||
if (obj.selectType == 'row') {
|
||
if (recEL.length <= 0) return
|
||
let tmp = obj.records[ind].w2ui || {}
|
||
if (tmp && tmp.parent_recid != null && (!Array.isArray(tmp.children) || tmp.children.length === 0 || !tmp.expanded)) {
|
||
obj.unselect(recid)
|
||
obj.collapse(tmp.parent_recid, event)
|
||
obj.select(tmp.parent_recid)
|
||
} else {
|
||
obj.collapse(recid, event)
|
||
}
|
||
} else {
|
||
let prev = obj.prevCell(ind, columns[0])
|
||
if (prev?.index != ind) {
|
||
prev = null
|
||
} else {
|
||
prev = prev?.colIndex
|
||
}
|
||
if (!shiftKey && prev == null) {
|
||
obj.selectNone(true)
|
||
prev = 0
|
||
}
|
||
if (prev != null) {
|
||
if (shiftKey && obj.multiSelect) {
|
||
if (tmpUnselect()) return
|
||
let tmp = []
|
||
let newSel = []
|
||
let unSel = []
|
||
if (columns.indexOf(obj.last.sel_col) === 0 && columns.length > 1) {
|
||
for (let i = 0; i < sel.length; i++) {
|
||
if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid)
|
||
unSel.push({ recid: sel[i].recid, column: columns[columns.length-1] })
|
||
}
|
||
obj.unselect(unSel)
|
||
obj.scrollIntoView(ind, columns[columns.length-1], true)
|
||
} else {
|
||
for (let i = 0; i < sel.length; i++) {
|
||
if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid)
|
||
newSel.push({ recid: sel[i].recid, column: prev })
|
||
}
|
||
obj.select(newSel)
|
||
obj.scrollIntoView(ind, prev, true)
|
||
}
|
||
} else {
|
||
obj.click({ recid: recid, column: prev }, event)
|
||
obj.scrollIntoView(ind, prev, true)
|
||
}
|
||
} else {
|
||
// if selected more then one, then select first
|
||
if (!shiftKey) {
|
||
obj.selectNone(true)
|
||
}
|
||
}
|
||
}
|
||
cancel = true
|
||
}
|
||
function moveRight() {
|
||
if (empty) {
|
||
selectTopRecord()
|
||
return
|
||
}
|
||
if (obj.selectType == 'row') {
|
||
if (recEL.length <= 0) return
|
||
obj.expand(recid, event)
|
||
} else {
|
||
let next = obj.nextCell(ind, columns[columns.length-1]) // columns is an array of selected columns
|
||
if (next.index != ind) {
|
||
next = null
|
||
} else {
|
||
next = next.colIndex
|
||
}
|
||
if (!shiftKey && next == null) {
|
||
obj.selectNone(true)
|
||
next = obj.columns.length-1
|
||
}
|
||
if (next != null) {
|
||
if (shiftKey && key == 39 && obj.multiSelect) {
|
||
if (tmpUnselect()) return
|
||
let tmp = []
|
||
let newSel = []
|
||
let unSel = []
|
||
if (columns.indexOf(obj.last.sel_col) == columns.length-1 && columns.length > 1) {
|
||
for (let i = 0; i < sel.length; i++) {
|
||
if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid)
|
||
unSel.push({ recid: sel[i].recid, column: columns[0] })
|
||
}
|
||
obj.unselect(unSel)
|
||
obj.scrollIntoView(ind, columns[0], true)
|
||
} else {
|
||
for (let i = 0; i < sel.length; i++) {
|
||
if (tmp.indexOf(sel[i].recid) == -1) tmp.push(sel[i].recid)
|
||
newSel.push({ recid: sel[i].recid, column: next })
|
||
}
|
||
obj.select(newSel)
|
||
obj.scrollIntoView(ind, next, true)
|
||
}
|
||
} else {
|
||
obj.click({ recid: recid, column: next }, event)
|
||
obj.scrollIntoView(ind, next, true)
|
||
}
|
||
} else {
|
||
// if selected more then one, then select first
|
||
if (!shiftKey) {
|
||
obj.selectNone(true)
|
||
}
|
||
}
|
||
}
|
||
cancel = true
|
||
}
|
||
function moveUp(numRows) {
|
||
if (empty) selectTopRecord()
|
||
if (recEL.length <= 0) return
|
||
// move to the previous record
|
||
let prev = obj.prevRow(ind, obj.selectType == 'row' ? 0 : sel[0].column, numRows)
|
||
if (!shiftKey && prev == null) {
|
||
if (obj.searchData.length != 0 && !url) {
|
||
prev = obj.last.searchIds[0]
|
||
} else {
|
||
prev = 0
|
||
}
|
||
}
|
||
if (prev != null) {
|
||
if (shiftKey && obj.multiSelect) { // expand selection
|
||
if (tmpUnselect()) return
|
||
if (obj.selectType == 'row') {
|
||
if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) {
|
||
obj.unselect(obj.records[ind2].recid)
|
||
} else {
|
||
obj.select(obj.records[prev].recid)
|
||
}
|
||
} else {
|
||
if (obj.last.sel_ind > prev && obj.last.sel_ind != ind2) {
|
||
prev = ind2
|
||
let tmp = []
|
||
for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[prev].recid, column: columns[c] })
|
||
obj.unselect(tmp)
|
||
} else {
|
||
let tmp = []
|
||
for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[prev].recid, column: columns[c] })
|
||
obj.select(tmp)
|
||
}
|
||
}
|
||
} else { // move selected record
|
||
obj.selectNone(true) // no need to trigger select event
|
||
obj.click({ recid: obj.records[prev].recid, column: columns[0] }, event)
|
||
}
|
||
obj.scrollIntoView(prev, null, true, numRows != 1) // top align record
|
||
if (event.preventDefault) event.preventDefault()
|
||
} else {
|
||
// if selected more then one, then select first
|
||
if (!shiftKey) {
|
||
obj.selectNone(true)
|
||
}
|
||
}
|
||
}
|
||
function moveDown(numRows) {
|
||
if (empty) selectTopRecord()
|
||
if (recEL.length <= 0) return
|
||
// move to the next record
|
||
let next = obj.nextRow(ind2, obj.selectType == 'row' ? 0 : sel[0].column, numRows)
|
||
if (!shiftKey && next == null) {
|
||
if (obj.searchData.length != 0 && !url) {
|
||
next = obj.last.searchIds[obj.last.searchIds.length - 1]
|
||
} else {
|
||
next = obj.records.length - 1
|
||
}
|
||
}
|
||
if (next != null) {
|
||
if (shiftKey && obj.multiSelect) { // expand selection
|
||
if (tmpUnselect()) return
|
||
if (obj.selectType == 'row') {
|
||
if (obj.last.sel_ind < next && obj.last.sel_ind != ind) {
|
||
obj.unselect(obj.records[ind].recid)
|
||
} else {
|
||
obj.select(obj.records[next].recid)
|
||
}
|
||
} else {
|
||
if (obj.last.sel_ind < next && obj.last.sel_ind != ind) {
|
||
next = ind
|
||
let tmp = []
|
||
for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[next].recid, column: columns[c] })
|
||
obj.unselect(tmp)
|
||
} else {
|
||
let tmp = []
|
||
for (let c = 0; c < columns.length; c++) tmp.push({ recid: obj.records[next].recid, column: columns[c] })
|
||
obj.select(tmp)
|
||
}
|
||
}
|
||
} else { // move selected record
|
||
obj.selectNone(true) // no need to trigger select event
|
||
obj.click({ recid: obj.records[next].recid, column: columns[0] }, event)
|
||
}
|
||
obj.scrollIntoView(next, null, true, numRows != 1) // top align record
|
||
cancel = true
|
||
} else {
|
||
// if selected more then one, then select first
|
||
if (!shiftKey) {
|
||
obj.selectNone(true) // no need to trigger select event
|
||
}
|
||
}
|
||
}
|
||
function selectTopRecord() {
|
||
if (!obj.records || obj.records.length === 0) return
|
||
let ind = Math.floor(records[0].scrollTop / obj.recordHeight) + 1
|
||
if (!obj.records[ind] || ind < 2) ind = 0
|
||
if (typeof obj.records[ind] === 'undefined') return
|
||
obj.select({ recid: obj.records[ind].recid, column: 0})
|
||
}
|
||
function tmpUnselect () {
|
||
if (obj.last.sel_type != 'click') return false
|
||
if (obj.selectType != 'row') {
|
||
obj.last.sel_type = 'key'
|
||
if (sel.length > 1) {
|
||
for (let s = 0; s < sel.length; s++) {
|
||
if (sel[s].recid == obj.last.sel_recid && sel[s].column == obj.last.sel_col) {
|
||
sel.splice(s, 1)
|
||
break
|
||
}
|
||
}
|
||
obj.unselect(sel)
|
||
return true
|
||
}
|
||
return false
|
||
} else {
|
||
obj.last.sel_type = 'key'
|
||
if (sel.length > 1) {
|
||
sel.splice(sel.indexOf(obj.records[obj.last.sel_ind].recid), 1)
|
||
obj.unselect(sel)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
scrollIntoView(ind, column, instant, recTop) {
|
||
let buffered = this.records.length
|
||
if (this.searchData.length != 0 && !this.url) buffered = this.last.searchIds.length
|
||
if (buffered === 0) return
|
||
if (ind == null) {
|
||
let sel = this.getSelection()
|
||
if (sel.length === 0) return
|
||
if (w2utils.isPlainObject(sel[0])) {
|
||
ind = sel[0].index
|
||
column = sel[0].column
|
||
} else {
|
||
ind = this.get(sel[0], true)
|
||
}
|
||
}
|
||
let records = query(this.box).find(`#grid_${this.name}_records`)
|
||
let recWidth = records[0].clientWidth
|
||
let recHeight = records[0].clientHeight
|
||
let recSTop = records[0].scrollTop
|
||
let recSLeft = records[0].scrollLeft
|
||
// if all records in view
|
||
let len = this.last.searchIds.length
|
||
if (len > 0) ind = this.last.searchIds.indexOf(ind) // if search is applied
|
||
// smooth or instant
|
||
records.css({ 'scroll-behavior': instant ? 'auto' : 'smooth' })
|
||
// vertical
|
||
if (recHeight < this.recordHeight * (len > 0 ? len : buffered) && records.length > 0) {
|
||
// scroll to correct one
|
||
let t1 = Math.floor(recSTop / this.recordHeight)
|
||
let t2 = t1 + Math.floor(recHeight / this.recordHeight)
|
||
if (ind == t1) {
|
||
records.prop('scrollTop', recSTop - recHeight / 1.3)
|
||
}
|
||
if (ind == t2) {
|
||
records.prop('scrollTop', recSTop + recHeight / 1.3)
|
||
}
|
||
if (ind < t1 || ind > t2) {
|
||
records.prop('scrollTop', (ind - 1) * this.recordHeight)
|
||
}
|
||
if (recTop === true) {
|
||
records.prop('scrollTop', ind * this.recordHeight)
|
||
}
|
||
}
|
||
// horizontal
|
||
if (column != null) {
|
||
let x1 = 0
|
||
let x2 = 0
|
||
let sb = w2utils.scrollBarSize()
|
||
for (let i = 0; i <= column; i++) {
|
||
let col = this.columns[i]
|
||
if (col.frozen || col.hidden) continue
|
||
x1 = x2
|
||
x2 += parseInt(col.sizeCalculated)
|
||
}
|
||
if (recWidth < x2 - recSLeft) { // right
|
||
records.prop('scrollLeft', x1 - sb)
|
||
} else if (x1 < recSLeft) { // left
|
||
records.prop('scrollLeft', x2 - recWidth + sb * 2)
|
||
}
|
||
}
|
||
}
|
||
scrollToColumn(field) {
|
||
if (field == null)
|
||
return
|
||
let sWidth = 0
|
||
let found = false
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.field == field) {
|
||
found = true
|
||
break
|
||
}
|
||
if (col.frozen || col.hidden)
|
||
continue
|
||
let cSize = parseInt(col.sizeCalculated ? col.sizeCalculated : col.size)
|
||
sWidth += cSize
|
||
}
|
||
if (!found)
|
||
return
|
||
this.last.scrollLeft = sWidth+1
|
||
this.scroll()
|
||
}
|
||
|
||
dblClick(recid, event) {
|
||
// find columns
|
||
let column = null
|
||
if ((typeof recid == 'object') && (recid !== null)) {
|
||
column = recid.column
|
||
recid = recid.recid
|
||
}
|
||
if (event == null) event = {}
|
||
// column user clicked on
|
||
if (column == null && event.target) {
|
||
let tmp = event.target
|
||
if (tmp.tagName.toUpperCase() != 'TD') tmp = query(tmp).closest('td')[0]
|
||
column = parseInt(query(tmp).attr('col'))
|
||
}
|
||
let index = this.get(recid, true)
|
||
let rec = this.records[index]
|
||
// event before
|
||
let edata = this.trigger('dblClick', { target: this.name, recid: recid, column: column, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
this.selectNone(true) // no need to trigger select event
|
||
let edit = this.getCellEditable(index, column)
|
||
if (edit) {
|
||
this.editField(recid, column, null, event)
|
||
} else {
|
||
this.select({ recid: recid, column: column })
|
||
if (this.show.expandColumn || (rec && rec.w2ui && Array.isArray(rec.w2ui.children))) this.toggle(recid)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
showContextMenu(recid, column, event) {
|
||
if (this.last.userSelect == 'text') return
|
||
if (event == null) {
|
||
event = { offsetX: 0, offsetY: 0, target: query(this.box).find(`#grid_${this.name}_rec_${recid}`)[0] }
|
||
}
|
||
if (event.offsetX == null) {
|
||
event.offsetX = event.layerX - event.target.offsetLeft
|
||
event.offsetY = event.layerY - event.target.offsetTop
|
||
}
|
||
if (w2utils.isFloat(recid)) recid = parseFloat(recid)
|
||
let sel = this.getSelection()
|
||
if (this.selectType == 'row') {
|
||
if (sel.indexOf(recid) == -1) this.click(recid)
|
||
} else {
|
||
let selected = false
|
||
// check if any selected sel in the right row/column
|
||
for (let i = 0; i < sel.length; i++) {
|
||
if (sel[i].recid == recid || sel[i].column == column) selected = true
|
||
}
|
||
if (!selected && recid != null) this.click({ recid: recid, column: column })
|
||
if (!selected && column != null) this.columnClick(this.columns[column].field, event)
|
||
}
|
||
// event before
|
||
let edata = this.trigger('contextMenu', { target: this.name, originalEvent: event, recid, column })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (this.contextMenu.length > 0) {
|
||
w2menu.show({
|
||
anchor: document.body,
|
||
originalEvent: event,
|
||
items: this.contextMenu
|
||
})
|
||
.select((event) => {
|
||
clearTimeout(this.last.kbd_timer) // keep grid in focus
|
||
this.contextMenuClick(recid, event)
|
||
})
|
||
clearTimeout(this.last.kbd_timer) // keep grid in focus
|
||
}
|
||
// cancel browser context menu
|
||
event.preventDefault()
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
contextMenuClick(recid, event) {
|
||
// event before
|
||
let edata = this.trigger('contextMenuClick', { target: this.name, recid, originalEvent: event.detail.originalEvent,
|
||
menuEvent: event, menuIndex: event.detail.index, menuItem: event.detail.item
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// no default action
|
||
edata.finish()
|
||
}
|
||
toggle(recid) {
|
||
let rec = this.get(recid)
|
||
if (rec == null) return
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
if (rec.w2ui.expanded === true) return this.collapse(recid); else return this.expand(recid)
|
||
}
|
||
expand(recid, noRefresh) {
|
||
let ind = this.get(recid, true)
|
||
let rec = this.records[ind]
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
let id = w2utils.escapeId(recid)
|
||
let children = rec.w2ui.children
|
||
let edata
|
||
if (Array.isArray(children)) {
|
||
if (rec.w2ui.expanded === true || children.length === 0) return false // already shown
|
||
edata = this.trigger('expand', { target: this.name, recid: recid })
|
||
if (edata.isCancelled === true) return false
|
||
rec.w2ui.expanded = true
|
||
children.forEach((child) => {
|
||
child.w2ui = child.w2ui ?? {}
|
||
child.w2ui.parent_recid = rec.recid
|
||
if (child.w2ui.children == null) child.w2ui.children = []
|
||
})
|
||
this.records.splice.apply(this.records, [ind + 1, 0].concat(children))
|
||
if (this.total !== -1) {
|
||
this.total += children.length
|
||
}
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (!url) {
|
||
this.localSort(true, true)
|
||
if (this.searchData.length > 0) {
|
||
this.localSearch(true)
|
||
}
|
||
}
|
||
if (noRefresh !== true) this.refresh()
|
||
edata.finish()
|
||
} else {
|
||
if (query(this.box).find('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').length > 0 || this.show.expandColumn !== true) return false
|
||
if (rec.w2ui.expanded == 'none') return false
|
||
// insert expand row
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id).after(
|
||
`<tr id="grid_${this.name}_rec_${recid}_expanded_row" class="w2ui-expanded-row">
|
||
<td colspan="100" class="w2ui-expanded2">
|
||
<div id="grid_${this.name}_rec_${recid}_expanded"></div>
|
||
</td>
|
||
<td class="w2ui-grid-data-last"></td>
|
||
</tr>`)
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id).after(
|
||
`<tr id="grid_${this.name}_frec_${recid}_expanded_row" class="w2ui-expanded-row">
|
||
${this.show.lineNumbers ? '<td class="w2ui-col-number"></td>' : ''}
|
||
<td class="w2ui-grid-data w2ui-expanded1" colspan="100">
|
||
<div id="grid_${this.name}_frec_${recid}_expanded"></div>
|
||
</td>
|
||
</tr>`)
|
||
// event before
|
||
edata = this.trigger('expand', { target: this.name, recid: recid,
|
||
box_id: 'grid_'+ this.name +'_rec_'+ recid +'_expanded', fbox_id: 'grid_'+ this.name +'_frec_'+ recid +'_expanded' })
|
||
if (edata.isCancelled === true) {
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').remove()
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id +'_expanded_row').remove()
|
||
return false
|
||
}
|
||
// expand column
|
||
let row1 = query(this.box).find('#grid_'+ this.name +'_rec_'+ recid +'_expanded')
|
||
let row2 = query(this.box).find('#grid_'+ this.name +'_frec_'+ recid +'_expanded')
|
||
let innerHeight = row1.find(':scope div:first-child')[0]?.clientHeight ?? 50
|
||
if (row1[0].clientHeight < innerHeight) {
|
||
row1.css({ height: innerHeight + 'px' })
|
||
}
|
||
if (row2[0].clientHeight < innerHeight) {
|
||
row2.css({ height: innerHeight + 'px' })
|
||
}
|
||
// default action
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id).attr('expanded', 'yes').addClass('w2ui-expanded')
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id).attr('expanded', 'yes').addClass('w2ui-expanded')
|
||
query(this.box).find('#grid_'+ this.name +'_cell_'+ this.get(recid, true) +'_expand div').html('-')
|
||
rec.w2ui.expanded = true
|
||
// event after
|
||
edata.finish()
|
||
this.resizeRecords()
|
||
}
|
||
return true
|
||
}
|
||
collapse(recid, noRefresh) {
|
||
let ind = this.get(recid, true)
|
||
let rec = this.records[ind]
|
||
rec.w2ui = rec.w2ui || {}
|
||
let id = w2utils.escapeId(recid)
|
||
let children = rec.w2ui.children
|
||
let edata
|
||
if (Array.isArray(children)) {
|
||
if (rec.w2ui.expanded !== true) return false // already hidden
|
||
edata = this.trigger('collapse', { target: this.name, recid: recid })
|
||
if (edata.isCancelled === true) return false
|
||
clearExpanded(rec)
|
||
let stops = []
|
||
for (let r = rec; r != null; r = this.get(r.w2ui.parent_recid))
|
||
stops.push(r.w2ui.parent_recid)
|
||
// stops contains 'undefined' plus the ID of all nodes in the path from 'rec' to the tree root
|
||
let start = ind + 1
|
||
let end = start
|
||
while (true) {
|
||
if (this.records.length <= end + 1 || this.records[end+1].w2ui == null ||
|
||
stops.indexOf(this.records[end+1].w2ui.parent_recid) >= 0) {
|
||
break
|
||
}
|
||
end++
|
||
}
|
||
this.records.splice(start, end - start + 1)
|
||
if (this.total !== -1) {
|
||
this.total -= end - start + 1
|
||
}
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (!url) {
|
||
if (this.searchData.length > 0) {
|
||
this.localSearch(true)
|
||
}
|
||
}
|
||
if (noRefresh !== true) this.refresh()
|
||
edata.finish()
|
||
} else {
|
||
if (query(this.box).find('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').length === 0 || this.show.expandColumn !== true) return false
|
||
// event before
|
||
edata = this.trigger('collapse', { target: this.name, recid: recid,
|
||
box_id: 'grid_'+ this.name +'_rec_'+ recid +'_expanded', fbox_id: 'grid_'+ this.name +'_frec_'+ recid +'_expanded' })
|
||
if (edata.isCancelled === true) return false
|
||
// default action
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id).removeAttr('expanded').removeClass('w2ui-expanded')
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id).removeAttr('expanded').removeClass('w2ui-expanded')
|
||
query(this.box).find('#grid_'+ this.name +'_cell_'+ this.get(recid, true) +'_expand div').html('+')
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id +'_expanded').css('height', '0px')
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id +'_expanded').css('height', '0px')
|
||
setTimeout(() => {
|
||
query(this.box).find('#grid_'+ this.name +'_rec_'+ id +'_expanded_row').remove()
|
||
query(this.box).find('#grid_'+ this.name +'_frec_'+ id +'_expanded_row').remove()
|
||
rec.w2ui.expanded = false
|
||
// event after
|
||
edata.finish()
|
||
this.resizeRecords()
|
||
}, 300)
|
||
}
|
||
return true
|
||
function clearExpanded(rec) {
|
||
rec.w2ui.expanded = false
|
||
for (let i = 0; i < rec.w2ui.children.length; i++) {
|
||
let subRec = rec.w2ui.children[i]
|
||
if (subRec.w2ui.expanded) {
|
||
clearExpanded(subRec)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
sort(field, direction, multiField) { // if no params - clears sort
|
||
// event before
|
||
let edata = this.trigger('sort', { target: this.name, field: field, direction: direction, multiField: multiField })
|
||
if (edata.isCancelled === true) return
|
||
// check if needed to quit
|
||
if (field != null) {
|
||
// default action
|
||
let sortIndex = this.sortData.length
|
||
for (let s = 0; s < this.sortData.length; s++) {
|
||
if (this.sortData[s].field == field) { sortIndex = s; break }
|
||
}
|
||
if (direction == null) {
|
||
if (this.sortData[sortIndex] == null) {
|
||
direction = 'asc'
|
||
} else {
|
||
if (this.sortData[sortIndex].direction == null) {
|
||
this.sortData[sortIndex].direction = ''
|
||
}
|
||
switch (this.sortData[sortIndex].direction.toLowerCase()) {
|
||
case 'asc' : direction = 'desc'; break
|
||
case 'desc' : direction = 'asc'; break
|
||
default : direction = 'asc'; break
|
||
}
|
||
}
|
||
}
|
||
if (this.multiSort === false) { this.sortData = []; sortIndex = 0 }
|
||
if (multiField != true) { this.sortData = []; sortIndex = 0 }
|
||
// set new sort
|
||
if (this.sortData[sortIndex] == null) this.sortData[sortIndex] = {}
|
||
this.sortData[sortIndex].field = field
|
||
this.sortData[sortIndex].direction = direction
|
||
} else {
|
||
this.sortData = []
|
||
}
|
||
// if local
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (!url) {
|
||
this.localSort(false, true)
|
||
if (this.searchData.length > 0) this.localSearch(true)
|
||
// reset vertical scroll
|
||
this.last.scrollTop = 0
|
||
query(this.box).find(`#grid_${this.name}_records`).prop('scrollTop', 0)
|
||
// event after
|
||
edata.finish({ direction })
|
||
this.refresh()
|
||
} else {
|
||
// event after
|
||
edata.finish({ direction })
|
||
this.last.fetch.offset = 0
|
||
this.reload()
|
||
}
|
||
}
|
||
copy(flag, oEvent) {
|
||
if (w2utils.isPlainObject(flag)) {
|
||
// event after
|
||
flag.finish()
|
||
return flag.text
|
||
}
|
||
// generate text to copy
|
||
let sel = this.getSelection()
|
||
if (sel.length === 0) return ''
|
||
let text = ''
|
||
if (typeof sel[0] == 'object') { // cell copy
|
||
// find min/max column
|
||
let minCol = sel[0].column
|
||
let maxCol = sel[0].column
|
||
let recs = []
|
||
for (let s = 0; s < sel.length; s++) {
|
||
if (sel[s].column < minCol) minCol = sel[s].column
|
||
if (sel[s].column > maxCol) maxCol = sel[s].column
|
||
if (recs.indexOf(sel[s].index) == -1) recs.push(sel[s].index)
|
||
}
|
||
recs.sort((a, b) => { return a-b }) // sort function must be for numerical sort
|
||
for (let r = 0 ; r < recs.length; r++) {
|
||
let ind = recs[r]
|
||
for (let c = minCol; c <= maxCol; c++) {
|
||
let col = this.columns[c]
|
||
if (col.hidden === true) continue
|
||
text += this.getCellCopy(ind, c) + '\t'
|
||
}
|
||
text = text.substr(0, text.length-1) // remove last \t
|
||
text += '\n'
|
||
}
|
||
} else { // row copy
|
||
// copy headers
|
||
for (let c = 0; c < this.columns.length; c++) {
|
||
let col = this.columns[c]
|
||
if (col.hidden === true) continue
|
||
let colName = (col.text ? col.text : col.field)
|
||
if (col.text && col.text.length < 3 && col.tooltip) colName = col.tooltip // if column name is less then 3 char and there is tooltip - use it
|
||
text += '"' + w2utils.stripTags(colName) + '"\t'
|
||
}
|
||
text = text.substr(0, text.length-1) // remove last \t
|
||
text += '\n'
|
||
// copy selected text
|
||
for (let s = 0; s < sel.length; s++) {
|
||
let ind = this.get(sel[s], true)
|
||
for (let c = 0; c < this.columns.length; c++) {
|
||
let col = this.columns[c]
|
||
if (col.hidden === true) continue
|
||
text += '"' + this.getCellCopy(ind, c) + '"\t'
|
||
}
|
||
text = text.substr(0, text.length-1) // remove last \t
|
||
text += '\n'
|
||
}
|
||
}
|
||
text = text.substr(0, text.length - 1)
|
||
// if called without params
|
||
let edata
|
||
if (flag == null) {
|
||
// before event
|
||
edata = this.trigger('copy', { target: this.name, text: text,
|
||
cut: (oEvent.keyCode == 88 ? true : false), originalEvent: oEvent })
|
||
if (edata.isCancelled === true) return ''
|
||
text = edata.detail.text
|
||
// event after
|
||
edata.finish()
|
||
return text
|
||
} else if (flag === false) { // only before event
|
||
// before event
|
||
edata = this.trigger('copy', { target: this.name, text: text,
|
||
cut: (oEvent.keyCode == 88 ? true : false), originalEvent: oEvent })
|
||
if (edata.isCancelled === true) return ''
|
||
text = edata.detail.text
|
||
return edata
|
||
}
|
||
}
|
||
/**
|
||
* Gets value to be copied to the clipboard
|
||
* @param ind index of the record
|
||
* @param col_ind index of the column
|
||
* @returns the displayed value of the field's record associated with the cell
|
||
*/
|
||
getCellCopy(ind, col_ind) {
|
||
return w2utils.stripTags(this.getCellHTML(ind, col_ind))
|
||
}
|
||
paste(text, event) {
|
||
let sel = this.getSelection()
|
||
let ind = this.get(sel[0].recid, true)
|
||
let col = sel[0].column
|
||
// before event
|
||
let edata = this.trigger('paste', { target: this.name, text: text, index: ind, column: col, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
text = edata.detail.text
|
||
// default action
|
||
if (this.selectType == 'row' || sel.length === 0) {
|
||
console.log('ERROR: You can paste only if grid.selectType = \'cell\' and when at least one cell selected.')
|
||
// event after
|
||
edata.finish()
|
||
return
|
||
}
|
||
if (typeof text !== 'object') {
|
||
let newSel = []
|
||
text = text.split('\n')
|
||
for (let t = 0; t < text.length; t++) {
|
||
let tmp = text[t].split('\t')
|
||
let cnt = 0
|
||
let rec = this.records[ind]
|
||
let cols = []
|
||
if (rec == null) continue
|
||
for (let dt = 0; dt < tmp.length; dt++) {
|
||
if (!this.columns[col + cnt]) continue
|
||
setCellPaste(rec, this.columns[col + cnt].field, tmp[dt])
|
||
cols.push(col + cnt)
|
||
cnt++
|
||
}
|
||
for (let c = 0; c < cols.length; c++) newSel.push({ recid: rec.recid, column: cols[c] })
|
||
ind++
|
||
}
|
||
this.selectNone(true) // no need to trigger select event
|
||
this.select(newSel)
|
||
} else {
|
||
this.selectNone(true) // no need to trigger select event
|
||
this.select([{ recid: this.records[ind], column: col }])
|
||
}
|
||
this.refresh()
|
||
// event after
|
||
edata.finish()
|
||
function setCellPaste(rec, field, paste) {
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
rec.w2ui.changes = rec.w2ui.changes || {}
|
||
rec.w2ui.changes[field] = paste
|
||
}
|
||
}
|
||
// ==================================================
|
||
// --- Common functions
|
||
resize() {
|
||
let time = Date.now()
|
||
// make sure the box is right
|
||
if (!this.box || query(this.box).attr('name') != this.name) return
|
||
// event before
|
||
let edata = this.trigger('resize', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// resize
|
||
this.resizeBoxes()
|
||
this.resizeRecords()
|
||
// event after
|
||
edata.finish()
|
||
return Date.now() - time
|
||
}
|
||
update({ cells, fullCellRefresh, ignoreColumns } = {}) {
|
||
let time = Date.now()
|
||
let self = this
|
||
if (this.box == null) return 0
|
||
if (Array.isArray(cells)) {
|
||
for (let i = 0; i < cells.length; i++) {
|
||
let index = cells[i].index
|
||
let column = cells[i].column
|
||
if (index < 0) continue
|
||
if (index == null || column == null) {
|
||
console.log('ERROR: Wrong argument for grid.update({ cells }), cells should be [{ index: X, column: Y }, ...]')
|
||
continue
|
||
}
|
||
let rec = this.records[index] ?? {}
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
rec.w2ui._update = rec.w2ui._update ?? { cells: [] }
|
||
let row1 = rec.w2ui._update.row1
|
||
let row2 = rec.w2ui._update.row2
|
||
if (row1 == null || !row1.isConnected || row2 == null || !row2.isColSelected) {
|
||
row1 = this.box.querySelector(`#grid_${this.name}_rec_${w2utils.escapeId(rec.recid)}`)
|
||
row2 = this.box.querySelector(`#grid_${this.name}_frec_${w2utils.escapeId(rec.recid)}`)
|
||
rec.w2ui._update.row1 = row1
|
||
rec.w2ui._update.row2 = row2
|
||
}
|
||
_update(rec, row1, row2, index, column)
|
||
}
|
||
} else {
|
||
for (let i = this.last.range_start-1; i <= this.last.range_end; i++) {
|
||
let index = i
|
||
if (this.last.searchIds.length > 0) { // if search is applied
|
||
index = this.last.searchIds[i]
|
||
} else {
|
||
index = i
|
||
}
|
||
let rec = this.records[index]
|
||
if (index < 0 || rec == null) continue
|
||
rec.w2ui = rec.w2ui ?? {}
|
||
rec.w2ui._update = rec.w2ui._update ?? { cells: [] }
|
||
let row1 = rec.w2ui._update.row1
|
||
let row2 = rec.w2ui._update.row2
|
||
if (row1 == null || !row1.isConnected || row2 == null || !row2.isColSelected) {
|
||
row1 = this.box.querySelector(`#grid_${this.name}_rec_${w2utils.escapeId(rec.recid)}`)
|
||
row2 = this.box.querySelector(`#grid_${this.name}_frec_${w2utils.escapeId(rec.recid)}`)
|
||
rec.w2ui._update.row1 = row1
|
||
rec.w2ui._update.row2 = row2
|
||
}
|
||
for (let column = 0; column < this.columns.length; column++) {
|
||
_update(rec, row1, row2, index, column)
|
||
}
|
||
}
|
||
}
|
||
return Date.now() - time
|
||
function _update(rec, row1, row2, index, column) {
|
||
let pcol = self.columns[column]
|
||
if (Array.isArray(ignoreColumns) && (ignoreColumns.includes(column) || ignoreColumns.includes(pcol.field))) {
|
||
return
|
||
}
|
||
let cell = rec.w2ui._update.cells[column]
|
||
if (cell == null || !cell.isConnected) {
|
||
cell = self.box.querySelector(`#grid_${self.name}_data_${index}_${column}`)
|
||
rec.w2ui._update.cells[column] = cell
|
||
}
|
||
if (cell == null) return
|
||
if (fullCellRefresh) {
|
||
query(cell).replace(self.getCellHTML(index, column, false))
|
||
// need to reselect as it was replaced
|
||
cell = self.box.querySelector(`#grid_${self.name}_data_${index}_${column}`)
|
||
rec.w2ui._update.cells[column] = cell
|
||
} else {
|
||
let div = cell.children[0] // there is always a div inside a cell
|
||
// value, attr, style, className, divAttr -- all on TD level except divAttr
|
||
let { value, style, className } = self.getCellValue(index, column, false, true)
|
||
if (div.innerHTML != value) {
|
||
div.innerHTML = value
|
||
}
|
||
if (style != '' && cell.style.cssText != style) {
|
||
cell.style.cssText = style
|
||
}
|
||
if (className != '') {
|
||
let ignore = ['w2ui-grid-data']
|
||
let remove = []
|
||
let add = className.split(' ').filter(cl => !!cl) // remove empty
|
||
cell.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl)})
|
||
cell.classList.remove(...remove)
|
||
cell.classList.add(...add)
|
||
}
|
||
}
|
||
// column styles if any (lower priority)
|
||
if (self.columns[column].style && self.columns[column].style != cell.style.cssText) {
|
||
cell.style.cssText = self.columns[column].style ?? ''
|
||
}
|
||
// record class if any
|
||
if (rec.w2ui.class != null) {
|
||
if (typeof rec.w2ui.class == 'string') {
|
||
let ignore = ['w2ui-odd', 'w2ui-even', 'w2ui-record']
|
||
let remove = []
|
||
let add = rec.w2ui.class.split(' ').filter(cl => !!cl) // remove empty
|
||
if (row1 && row2) {
|
||
row1.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl)})
|
||
row1.classList.remove(...remove)
|
||
row1.classList.add(...add)
|
||
row2.classList.remove(...remove)
|
||
row2.classList.add(...add)
|
||
}
|
||
}
|
||
if (w2utils.isPlainObject(rec.w2ui.class) && typeof rec.w2ui.class[pcol.field] == 'string') {
|
||
let ignore = ['w2ui-grid-data']
|
||
let remove = []
|
||
let add = rec.w2ui.class[pcol.field].split(' ').filter(cl => !!cl)
|
||
cell.classList.forEach(cl => { if (!ignore.includes(cl)) remove.push(cl)})
|
||
cell.classList.remove(...remove)
|
||
cell.classList.add(...add)
|
||
}
|
||
}
|
||
// record styles if any
|
||
if (rec.w2ui.style != null) {
|
||
if (row1 && row2 && typeof rec.w2ui.style == 'string' && row1.style.cssText !== rec.w2ui.style) {
|
||
row1.style.cssText = 'height: '+ self.recordHeight + 'px;' + rec.w2ui.style
|
||
row1.setAttribute('custom_style', rec.w2ui.style)
|
||
row2.style.cssText = 'height: '+ self.recordHeight + 'px;' + rec.w2ui.style
|
||
row2.setAttribute('custom_style', rec.w2ui.style)
|
||
}
|
||
if (w2utils.isPlainObject(rec.w2ui.style) && typeof rec.w2ui.style[pcol.field] == 'string'
|
||
&& cell.style.cssText !== rec.w2ui.style[pcol.field]) {
|
||
cell.style.cssText = rec.w2ui.style[pcol.field]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
refreshCell(recid, field) {
|
||
let index = this.get(recid, true)
|
||
let col_ind = this.getColumn(field, true)
|
||
let isSummary = (this.records[index] && this.records[index].recid == recid ? false : true)
|
||
let cell = query(this.box).find(`${isSummary ? '.w2ui-grid-summary ' : ''}#grid_${this.name}_data_${index}_${col_ind}`)
|
||
if (cell.length == 0) return false
|
||
// set cell html and changed flag
|
||
cell.replace(this.getCellHTML(index, col_ind, isSummary))
|
||
return true
|
||
}
|
||
refreshRow(recid, ind = null) {
|
||
let tr1 = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid))
|
||
let tr2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid))
|
||
if (tr1.length > 0) {
|
||
if (ind == null) ind = this.get(recid, true)
|
||
let line = tr1.attr('line')
|
||
let isSummary = (this.records[ind] && this.records[ind].recid == recid ? false : true)
|
||
// if it is searched, find index in search array
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (this.searchData.length > 0 && !url) for (let s = 0; s < this.last.searchIds.length; s++) if (this.last.searchIds[s] == ind) ind = s
|
||
let rec_html = this.getRecordHTML(ind, line, isSummary)
|
||
tr1.replace(rec_html[0])
|
||
tr2.replace(rec_html[1])
|
||
// apply style to row if it was changed in render functions
|
||
let st = (this.records[ind].w2ui ? this.records[ind].w2ui.style : '')
|
||
if (typeof st == 'string') {
|
||
tr1 = query(this.box).find('#grid_'+ this.name +'_frec_'+ w2utils.escapeId(recid))
|
||
tr2 = query(this.box).find('#grid_'+ this.name +'_rec_'+ w2utils.escapeId(recid))
|
||
tr1.attr('custom_style', st)
|
||
tr2.attr('custom_style', st)
|
||
if (tr1.hasClass('w2ui-selected')) {
|
||
st = st.replace('background-color', 'none')
|
||
}
|
||
tr1[0].style.cssText = 'height: '+ this.recordHeight + 'px;' + st
|
||
tr2[0].style.cssText = 'height: '+ this.recordHeight + 'px;' + st
|
||
}
|
||
if (isSummary) {
|
||
this.resize()
|
||
}
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
refresh() {
|
||
let time = Date.now()
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (this.total <= 0 && !url && this.searchData.length === 0) {
|
||
this.total = this.records.length
|
||
}
|
||
if (!this.box) return
|
||
// event before
|
||
let edata = this.trigger('refresh', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// -- header
|
||
if (this.show.header) {
|
||
query(this.box).find(`#grid_${this.name}_header`).html(w2utils.lang(this.header) +' ').show()
|
||
} else {
|
||
query(this.box).find(`#grid_${this.name}_header`).hide()
|
||
}
|
||
// -- toolbar
|
||
if (this.show.toolbar) {
|
||
query(this.box).find('#grid_'+ this.name +'_toolbar').show()
|
||
} else {
|
||
query(this.box).find('#grid_'+ this.name +'_toolbar').hide()
|
||
}
|
||
// -- make sure search is closed
|
||
this.searchClose()
|
||
// search placeholder
|
||
let sInput = query(this.box).find('#grid_'+ this.name +'_search_all')
|
||
if (!this.multiSearch && this.last.field == 'all' && this.searches.length > 0) {
|
||
this.last.field = this.searches[0].field
|
||
this.last.label = this.searches[0].label
|
||
}
|
||
for (let s = 0; s < this.searches.length; s++) {
|
||
if (this.searches[s].field == this.last.field) this.last.label = this.searches[s].label
|
||
}
|
||
if (this.last.multi) {
|
||
sInput.attr('placeholder', '[' + w2utils.lang('Multiple Fields') + ']')
|
||
} else {
|
||
sInput.attr('placeholder', w2utils.lang('Search') + ' ' + w2utils.lang(this.last.label, true))
|
||
}
|
||
if (sInput.val() != this.last.search) {
|
||
let val = this.last.search
|
||
let tmp = sInput._w2field
|
||
if (tmp) val = tmp.format(val)
|
||
sInput.val(val)
|
||
}
|
||
this.refreshSearch()
|
||
this.refreshBody()
|
||
// -- footer
|
||
if (this.show.footer) {
|
||
query(this.box).find(`#grid_${this.name}_footer`).html(this.getFooterHTML()).show()
|
||
} else {
|
||
query(this.box).find(`#grid_${this.name}_footer`).hide()
|
||
}
|
||
// all selected?
|
||
let sel = this.last.selection,
|
||
areAllSelected = (this.records.length > 0 && sel.indexes.length == this.records.length),
|
||
areAllSearchedSelected = (sel.indexes.length > 0 && this.searchData.length !== 0 && sel.indexes.length == this.last.searchIds.length)
|
||
if (areAllSelected || areAllSearchedSelected) {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', true)
|
||
} else {
|
||
query(this.box).find('#grid_'+ this.name +'_check_all').prop('checked', false)
|
||
}
|
||
// show number of selected
|
||
this.status()
|
||
// collapse all records
|
||
let rows = this.find({ 'w2ui.expanded': true }, true, true)
|
||
for (let r = 0; r < rows.length; r++) {
|
||
let tmp = this.records[rows[r]].w2ui
|
||
if (tmp && !Array.isArray(tmp.children)) {
|
||
tmp.expanded = false
|
||
}
|
||
}
|
||
// mark selection
|
||
if (this.markSearch) {
|
||
setTimeout(() => {
|
||
// mark all search strings
|
||
let search = []
|
||
for (let s = 0; s < this.searchData.length; s++) {
|
||
let sdata = this.searchData[s]
|
||
let fld = this.getSearch(sdata.field)
|
||
if (!fld || fld.hidden) continue
|
||
let ind = this.getColumn(sdata.field, true)
|
||
search.push({ field: sdata.field, search: sdata.value, col: ind })
|
||
}
|
||
if (search.length > 0) {
|
||
search.forEach((item) => {
|
||
let el = query(this.box).find('td[col="'+ item.col +'"]:not(.w2ui-head)')
|
||
w2utils.marker(el, item.search)
|
||
})
|
||
}
|
||
}, 50)
|
||
}
|
||
this.updateToolbar()
|
||
// event after
|
||
edata.finish()
|
||
this.resize()
|
||
this.addRange('selection')
|
||
setTimeout(() => { // allow to render first
|
||
this.resize() // needed for horizontal scroll to show (do not remove)
|
||
this.scroll()
|
||
}, 1)
|
||
if (this.reorderColumns && !this.last.columnDrag) {
|
||
this.last.columnDrag = this.initColumnDrag()
|
||
} else if (!this.reorderColumns && this.last.columnDrag) {
|
||
this.last.columnDrag.remove()
|
||
}
|
||
return Date.now() - time
|
||
}
|
||
refreshSearch() {
|
||
if (this.multiSearch && this.searchData.length > 0) {
|
||
if (query(this.box).find('.w2ui-grid-searches').length == 0) {
|
||
query(this.box).find('.w2ui-grid-toolbar')
|
||
.css('height', (this.last.toolbar_height + 35) + 'px')
|
||
.append(`<div id="grid_${this.name}_searches" class="w2ui-grid-searches"></div>`)
|
||
}
|
||
let searches = `
|
||
<span id="grid_${this.name}_search_logic" class="w2ui-grid-search-logic"></span>
|
||
<div class="grid-search-line"></div>`
|
||
this.searchData.forEach((sd, sd_ind) => {
|
||
let ind = this.getSearch(sd.field, true)
|
||
let sf = this.searches[ind]
|
||
let display
|
||
if (Array.isArray(sd.value)) {
|
||
display = `<span class="grid-search-count">${sd.value.length}</span>`
|
||
} else {
|
||
display = `: ${sd.value}`
|
||
}
|
||
if (sf && sf.type == 'date') {
|
||
if (sd.operator == 'between') {
|
||
let dsp1 = sd.value[0]
|
||
let dsp2 = sd.value[1]
|
||
if (Number(dsp1) === dsp1) {
|
||
dsp1 = w2utils.formatDate(dsp1)
|
||
}
|
||
if (Number(dsp2) === dsp2) {
|
||
dsp2 = w2utils.formatDate(dsp2)
|
||
}
|
||
display = `: ${dsp1} - ${dsp2}`
|
||
} else {
|
||
let dsp = sd.value
|
||
if (Number(dsp) == dsp) {
|
||
dsp = w2utils.formatDate(dsp)
|
||
}
|
||
let oper = sd.operator
|
||
if (oper == 'more') oper = 'since'
|
||
if (oper == 'less') oper = 'before'
|
||
if (oper.substr(0, 5) == 'more:') {
|
||
oper = 'since'
|
||
}
|
||
display = `: ${oper} ${dsp}`
|
||
}
|
||
}
|
||
searches += `<span class="w2ui-action" data-click="searchFieldTooltip|${ind}|${sd_ind}|this">
|
||
${sf ? sf.label : ''}
|
||
${display}
|
||
<span class="icon-chevron-down"></span>
|
||
</span>`
|
||
})
|
||
// clear and save
|
||
searches += `
|
||
${this.show.searchSave
|
||
? `<div class="grid-search-line"></div>
|
||
<button class="w2ui-btn grid-search-btn" data-click="searchSave">${w2utils.lang('Save')}</button>
|
||
`
|
||
: ''
|
||
}
|
||
<button class="w2ui-btn grid-search-btn btn-remove"
|
||
data-click="searchReset">X</button>
|
||
`
|
||
query(this.box).find(`#grid_${this.name}_searches`).html(searches)
|
||
query(this.box).find(`#grid_${this.name}_search_logic`).html(w2utils.lang(this.last.logic == 'AND' ? 'All' : 'Any'))
|
||
} else {
|
||
query(this.box).find('.w2ui-grid-toolbar')
|
||
.css('height', this.last.toolbar_height + 'px')
|
||
.find('.w2ui-grid-searches')
|
||
.remove()
|
||
}
|
||
if (this.searchSelected) {
|
||
query(this.box).find(`#grid_${this.name}_search_all`).val(' ').prop('readOnly', true)
|
||
query(this.box).find(`#grid_${this.name}_search_name`).show().find('.name-text').html(this.searchSelected.text)
|
||
} else {
|
||
query(this.box).find(`#grid_${this.name}_search_all`).prop('readOnly', false)
|
||
query(this.box).find(`#grid_${this.name}_search_name`).hide().find('.name-text').html('')
|
||
}
|
||
w2utils.bindEvents(query(this.box).find(`#grid_${this.name}_searches .w2ui-action, #grid_${this.name}_searches button`), this)
|
||
}
|
||
refreshBody() {
|
||
this.scroll() // need to calculate virtual scrolling for columns
|
||
let recHTML = this.getRecordsHTML()
|
||
let colHTML = this.getColumnsHTML()
|
||
let bodyHTML =
|
||
'<div id="grid_'+ this.name +'_frecords" class="w2ui-grid-frecords" style="margin-bottom: '+ (w2utils.scrollBarSize() - 1) +'px;">'+
|
||
recHTML[0] +
|
||
'</div>'+
|
||
'<div id="grid_'+ this.name +'_records" class="w2ui-grid-records">' +
|
||
recHTML[1] +
|
||
'</div>'+
|
||
'<div id="grid_'+ this.name +'_scroll1" class="w2ui-grid-scroll1" style="height: '+ w2utils.scrollBarSize() +'px"></div>'+
|
||
// Columns need to be after to be able to overlap
|
||
'<div id="grid_'+ this.name +'_fcolumns" class="w2ui-grid-fcolumns">'+
|
||
' <table><tbody>'+ colHTML[0] +'</tbody></table>'+
|
||
'</div>'+
|
||
'<div id="grid_'+ this.name +'_columns" class="w2ui-grid-columns">'+
|
||
' <table><tbody>'+ colHTML[1] +'</tbody></table>'+
|
||
'</div>'+
|
||
`<div class="w2ui-intersection-marker" style="display: none; height: ${this.recordHeight-5}px">
|
||
<div class="top-marker"></div>
|
||
<div class="bottom-marker"></div>
|
||
</div>`
|
||
let gridBody = query(this.box).find(`#grid_${this.name}_body`, this.box).html(bodyHTML)
|
||
let records = query(this.box).find(`#grid_${this.name}_records`, this.box)
|
||
let frecords = query(this.box).find(`#grid_${this.name}_frecords`, this.box)
|
||
if (this.selectType == 'row') {
|
||
records.on('mouseover mouseout', { delegate: 'tr' }, (event) => {
|
||
let recid = query(event.delegate).attr('recid')
|
||
query(this.box).find(`#grid_${this.name}_frec_${w2utils.escapeId(recid)}`)
|
||
.toggleClass('w2ui-record-hover', event.type == 'mouseover')
|
||
})
|
||
frecords.on('mouseover mouseout', { delegate: 'tr' }, (event) => {
|
||
let recid = query(event.delegate).attr('recid')
|
||
query(this.box).find(`#grid_${this.name}_rec_${w2utils.escapeId(recid)}`)
|
||
.toggleClass('w2ui-record-hover', event.type == 'mouseover')
|
||
})
|
||
}
|
||
if (w2utils.isIOS) {
|
||
records.append(frecords)
|
||
.on('click', { delegate: 'tr' }, (event) => {
|
||
let recid = query(event.delegate).attr('recid')
|
||
this.dblClick(recid, event)
|
||
})
|
||
} else {
|
||
records.add(frecords)
|
||
.on('click', { delegate: 'tr' }, (event) => {
|
||
let recid = query(event.delegate).attr('recid')
|
||
// do not generate click if empty record is clicked
|
||
if (recid != '-none-') {
|
||
this.click(recid, event)
|
||
}
|
||
})
|
||
.on('contextmenu', { delegate: 'tr' }, (event) => {
|
||
let recid = query(event.delegate).attr('recid')
|
||
let td = query(event.target).closest('td')
|
||
let column = parseInt(td.attr('col') ?? -1)
|
||
this.showContextMenu(recid, column, event)
|
||
})
|
||
.on('mouseover', { delegate: 'tr' }, (event) => {
|
||
this.last.rec_out = false
|
||
let index = query(event.delegate).attr('index')
|
||
let recid = query(event.delegate).attr('recid')
|
||
if (index !== this.last.rec_over) {
|
||
this.last.rec_over = index
|
||
// setTimeout is needed for correct event order enter/leave
|
||
setTimeout(() => {
|
||
delete this.last.rec_out
|
||
let edata = this.trigger('mouseEnter', { target: this.name, originalEvent: event, index, recid })
|
||
edata.finish()
|
||
})
|
||
}
|
||
})
|
||
.on('mouseout', { delegate: 'tr' }, (event) => {
|
||
let index = query(event.delegate).attr('index')
|
||
let recid = query(event.delegate).attr('recid')
|
||
this.last.rec_out = true
|
||
// setTimeouts are needed for correct event order enter/leave
|
||
setTimeout(() => {
|
||
let recLeave = () => {
|
||
let edata = this.trigger('mouseLeave', { target: this.name, originalEvent: event, index, recid })
|
||
edata.finish()
|
||
}
|
||
if (index !== this.last.rec_over) {
|
||
recLeave()
|
||
}
|
||
setTimeout(() => {
|
||
if (this.last.rec_out) {
|
||
delete this.last.rec_out
|
||
delete this.last.rec_over
|
||
recLeave()
|
||
}
|
||
})
|
||
})
|
||
})
|
||
}
|
||
// enable scrolling on frozen records,
|
||
gridBody
|
||
.data('scroll', { lastDelta: 0, lastTime: 0 })
|
||
.find('.w2ui-grid-frecords')
|
||
.on('mousewheel DOMMouseScroll ', (event) => {
|
||
event.preventDefault()
|
||
// TODO: improve, scroll is not smooth, if scrolled to the end, it takes a while to return
|
||
let scroll = gridBody.data('scroll')
|
||
let container = gridBody.find('.w2ui-grid-records')
|
||
let amount = typeof event.wheelDelta != null ? -event.wheelDelta : (event.detail || event.deltaY)
|
||
let newScrollTop = container.prop('scrollTop')
|
||
scroll.lastDelta += amount
|
||
amount = Math.round(scroll.lastDelta)
|
||
gridBody.data('scroll', scroll)
|
||
// make scroll amount dependent on visible rows
|
||
// amount *= (Math.round(records.prop('clientHeight') / self.recordHeight) - 1) * self.recordHeight / 4
|
||
container.get(0).scroll({ top: newScrollTop + amount, behavior: 'smooth' })
|
||
})
|
||
// scroll on records (and frozen records)
|
||
records.off('.body-global')
|
||
.on('scroll.body-global', { delegate: '.w2ui-grid-records' }, event => {
|
||
this.scroll(event)
|
||
})
|
||
query(this.box).find('.w2ui-grid-body') // gridBody
|
||
.off('.body-global')
|
||
// header column click
|
||
.on('click.body-global dblclick.body-global contextmenu.body-global', { delegate: 'td.w2ui-head' }, event => {
|
||
let col_ind = query(event.delegate).attr('col')
|
||
let col = this.columns[col_ind] ?? { field: col_ind } // it could be line number
|
||
switch (event.type) {
|
||
case 'click':
|
||
this.columnClick(col.field, event)
|
||
break
|
||
case 'dblclick':
|
||
this.columnDblClick(col.field, event)
|
||
break
|
||
case 'contextmenu':
|
||
if (this.show.columnMenu) {
|
||
w2menu.show({
|
||
type: 'check',
|
||
anchor: document.body,
|
||
originalEvent: event,
|
||
items: this.initColumnOnOff()
|
||
})
|
||
.then(() => {
|
||
query('#w2overlay-context-menu .w2ui-grid-skip')
|
||
.off('.w2ui-grid')
|
||
.on('click.w2ui-grid', evt => {
|
||
evt.stopPropagation()
|
||
})
|
||
.on('keypress', evt => {
|
||
if (evt.keyCode == 13) {
|
||
this.skip(evt.target.value)
|
||
this.toolbar.click('w2ui-column-on-off') // close menu
|
||
}
|
||
})
|
||
})
|
||
.select((event) => {
|
||
let id = event.detail.item.id
|
||
if (['w2ui-stateSave', 'w2ui-stateReset'].includes(id)) {
|
||
this[id.substring(5)]()
|
||
} else if (id == 'w2ui-skip') {
|
||
// empty
|
||
} else {
|
||
this.columnOnOff(event, event.detail.item.id)
|
||
}
|
||
clearTimeout(this.last.kbd_timer) // keep grid in focus
|
||
})
|
||
clearTimeout(this.last.kbd_timer) // keep grid in focus
|
||
}
|
||
event.preventDefault()
|
||
break
|
||
}
|
||
})
|
||
.on('mouseover.body-global', { delegate: '.w2ui-col-header' }, event => {
|
||
let col = query(event.delegate).parent().attr('col')
|
||
this.columnTooltipShow(col, event)
|
||
query(event.delegate)
|
||
.off('.tooltip')
|
||
.on('mouseleave.tooltip', () => {
|
||
this.columnTooltipHide(col, event)
|
||
})
|
||
})
|
||
// select all
|
||
.on('click.body-global', { delegate: 'input.w2ui-select-all' }, event => {
|
||
if (event.delegate.checked) { this.selectAll() } else { this.selectNone() }
|
||
event.stopPropagation()
|
||
clearTimeout(this.last.kbd_timer) // keep grid in focus
|
||
})
|
||
// tree-like grid (or expandable column) expand/collapse
|
||
.on('click.body-global', { delegate: '.w2ui-show-children, .w2ui-col-expand' }, event => {
|
||
event.stopPropagation()
|
||
this.toggle(query(event.target).parents('tr').attr('recid'))
|
||
})
|
||
// info bubbles
|
||
.on('click.body-global mouseover.body-global', { delegate: '.w2ui-info' }, event => {
|
||
let td = query(event.delegate).closest('td')
|
||
let tr = td.parent()
|
||
let col = this.columns[td.attr('col')]
|
||
let isSummary = tr.parents('.w2ui-grid-body').hasClass('w2ui-grid-summary')
|
||
if (['mouseenter', 'mouseover'].includes(col.info?.showOn?.toLowerCase()) && event.type == 'mouseover') {
|
||
this.showBubble(tr.attr('index'), td.attr('col'), isSummary)
|
||
.then(() => {
|
||
query(event.delegate)
|
||
.off('.tooltip')
|
||
.on('mouseleave.tooltip', () => { w2tooltip.hide(this.name + '-bubble') })
|
||
})
|
||
} else if (event.type == 'click') {
|
||
w2tooltip.hide(this.name + '-bubble')
|
||
this.showBubble(tr.attr('index'), td.attr('col'), isSummary)
|
||
}
|
||
})
|
||
// clipborad copy icon
|
||
.on('mouseover.body-global', { delegate: '.w2ui-clipboard-copy' }, event => {
|
||
if (event.delegate._tooltipShow) return
|
||
let td = query(event.delegate).parent()
|
||
let tr = td.parent()
|
||
let col = this.columns[td.attr('col')]
|
||
let isSummary = tr.parents('.w2ui-grid-body').hasClass('w2ui-grid-summary')
|
||
w2tooltip.show({
|
||
name: this.name + '-bubble',
|
||
anchor: event.delegate,
|
||
html: w2utils.lang(typeof col.clipboardCopy == 'string' ? col.clipboardCopy : 'Copy to clipboard'),
|
||
position: 'top|bottom',
|
||
offsetY: -2
|
||
})
|
||
.hide(evt => {
|
||
event.delegate._tooltipShow = false
|
||
query(event.delegate).off('.tooltip')
|
||
})
|
||
query(event.delegate)
|
||
.off('.tooltip')
|
||
.on('mouseleave.tooltip', evt => {
|
||
w2tooltip.hide(this.name + '-bubble')
|
||
})
|
||
.on('click.tooltip', evt => {
|
||
evt.stopPropagation()
|
||
w2tooltip.update(this.name + '-bubble', w2utils.lang('Copied'))
|
||
this.clipboardCopy(tr.attr('index'), td.attr('col'), isSummary)
|
||
})
|
||
event.delegate._tooltipShow = true
|
||
})
|
||
.on('click.body-global', { delegate: '.w2ui-editable-checkbox' }, event => {
|
||
let dt = query(event.delegate).data()
|
||
this.editChange.call(this, event.delegate, dt.changeind, dt.colind, event)
|
||
this.updateToolbar()
|
||
})
|
||
// show empty message
|
||
if (this.records.length === 0 && this.msgEmpty) {
|
||
query(this.box).find(`#grid_${this.name}_body`)
|
||
.append(`<div id="grid_${this.name}_empty_msg" class="w2ui-grid-empty-msg"><div>${w2utils.lang(this.msgEmpty)}</div></div>`)
|
||
} else if (query(this.box).find(`#grid_${this.name}_empty_msg`).length > 0) {
|
||
query(this.box).find(`#grid_${this.name}_empty_msg`).remove()
|
||
}
|
||
// show summary records
|
||
if (this.summary.length > 0) {
|
||
let sumHTML = this.getSummaryHTML()
|
||
query(this.box).find(`#grid_${this.name}_fsummary`).html(sumHTML[0]).show()
|
||
query(this.box).find(`#grid_${this.name}_summary`).html(sumHTML[1]).show()
|
||
} else {
|
||
query(this.box).find(`#grid_${this.name}_fsummary`).hide()
|
||
query(this.box).find(`#grid_${this.name}_summary`).hide()
|
||
}
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
let obj = this
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find(`#grid_${this.name}_body`).length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-grid w2ui-inactive')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.box) return
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
// reset needed if grid existed
|
||
this.reset(true)
|
||
// --- default search field
|
||
if (!this.last.field) {
|
||
if (!this.multiSearch || !this.show.searchAll) {
|
||
let tmp = 0
|
||
while (tmp < this.searches.length && (this.searches[tmp].hidden || this.searches[tmp].simple === false)) tmp++
|
||
if (tmp >= this.searches.length) {
|
||
// all searches are hidden
|
||
this.last.field = ''
|
||
this.last.label = ''
|
||
} else {
|
||
this.last.field = this.searches[tmp].field
|
||
this.last.label = this.searches[tmp].label
|
||
}
|
||
} else {
|
||
this.last.field = 'all'
|
||
this.last.label = 'All Fields'
|
||
}
|
||
}
|
||
// insert elements
|
||
query(this.box)
|
||
.attr('name', this.name)
|
||
.addClass('w2ui-reset w2ui-grid w2ui-inactive')
|
||
.html('<div class="w2ui-grid-box">'+
|
||
' <div id="grid_'+ this.name +'_header" class="w2ui-grid-header"></div>'+
|
||
' <div id="grid_'+ this.name +'_toolbar" class="w2ui-grid-toolbar"></div>'+
|
||
' <div id="grid_'+ this.name +'_body" class="w2ui-grid-body"></div>'+
|
||
' <div id="grid_'+ this.name +'_fsummary" class="w2ui-grid-body w2ui-grid-summary"></div>'+
|
||
' <div id="grid_'+ this.name +'_summary" class="w2ui-grid-body w2ui-grid-summary"></div>'+
|
||
' <div id="grid_'+ this.name +'_footer" class="w2ui-grid-footer"></div>'+
|
||
' <textarea id="grid_'+ this.name +'_focus" class="w2ui-grid-focus-input" '+
|
||
(this.tabIndex ? 'tabindex="' + this.tabIndex + '"' : '')+
|
||
(w2utils.isIOS ? 'readonly' : '') +'></textarea>'+ // readonly needed on android not to open keyboard
|
||
'</div>')
|
||
if (this.selectType != 'row') query(this.box).addClass('w2ui-ss')
|
||
if (query(this.box).length > 0) query(this.box)[0].style.cssText += this.style
|
||
// init toolbar
|
||
this.initToolbar()
|
||
if (this.toolbar != null) this.toolbar.render(query(this.box).find('#grid_'+ this.name +'_toolbar')[0])
|
||
this.last.toolbar_height = query(this.box).find(`#grid_${this.name}_toolbar`).prop('offsetHeight')
|
||
// re-init search_all
|
||
if (this.last.field && this.last.field != 'all') {
|
||
let sd = this.searchData
|
||
setTimeout(() => { this.searchInitInput(this.last.field, (sd.length == 1 ? sd[0].value : null)) }, 1)
|
||
}
|
||
// init footer
|
||
query(this.box).find(`#grid_${this.name}_footer`).html(this.getFooterHTML())
|
||
// refresh
|
||
if (!this.last.state) this.last.state = this.stateSave(true) // initial default state
|
||
this.stateRestore()
|
||
if (url) { this.clear(); this.refresh() } // show empty grid (need it) - should it be only for remote data source
|
||
// if hidden searches - apply it
|
||
let hasHiddenSearches = false
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
if (this.searches[i].hidden) { hasHiddenSearches = true; break }
|
||
}
|
||
if (hasHiddenSearches) {
|
||
this.searchReset(false) // will call reload
|
||
if (!url) setTimeout(() => { this.searchReset() }, 1)
|
||
} else {
|
||
this.reload()
|
||
}
|
||
// focus
|
||
query(this.box).find(`#grid_${this.name}_focus`)
|
||
.on('focus', (event) => {
|
||
clearTimeout(this.last.kbd_timer)
|
||
if (!this.hasFocus) this.focus()
|
||
})
|
||
.on('blur', (event) => {
|
||
clearTimeout(this.last.kbd_timer)
|
||
this.last.kbd_timer = setTimeout(() => {
|
||
if (this.hasFocus) { this.blur() }
|
||
}, 100) // need this timer to be 100 ms
|
||
})
|
||
.on('paste', (event) => {
|
||
let cd = (event.clipboardData ? event.clipboardData : null)
|
||
if (cd) {
|
||
let items = cd.items
|
||
if (items.length == 2) {
|
||
if (items.length == 2 && items[1].kind == 'file') {
|
||
items = [items[1]]
|
||
}
|
||
if (items.length == 2 && items[0].type == 'text/plain' && items[1].type == 'text/html') {
|
||
items = [items[1]]
|
||
}
|
||
}
|
||
let items2send = []
|
||
// might contain data in different formats, but it is a single paste
|
||
for (let index in items) {
|
||
let item = items[index]
|
||
if (item.kind === 'file') {
|
||
let file = item.getAsFile()
|
||
items2send.push({ kind: 'file', data: file })
|
||
} else if (item.kind === 'string' && (item.type === 'text/plain' || item.type === 'text/html')) {
|
||
event.preventDefault()
|
||
let text = cd.getData('text/plain')
|
||
if (text.indexOf('\r') != -1 && text.indexOf('\n') == -1) {
|
||
text = text.replace(/\r/g, '\n')
|
||
}
|
||
items2send.push({ kind: (item.type == 'text/html' ? 'html' : 'text'), data: text })
|
||
}
|
||
}
|
||
if (items2send.length === 1 && items2send[0].kind != 'file') {
|
||
items2send = items2send[0].data
|
||
}
|
||
w2ui[this.name].paste(items2send, event)
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
.on('keydown', function (event) {
|
||
w2ui[obj.name].keydown.call(w2ui[obj.name], event)
|
||
})
|
||
// init mouse events for mouse selection
|
||
let edataCol // event for column select
|
||
query(this.box).off('mousedown.mouseStart').on('mousedown.mouseStart', mouseStart)
|
||
this.updateToolbar()
|
||
// event after
|
||
edata.finish()
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
return Date.now() - time
|
||
function mouseStart (event) {
|
||
if (event.which != 1) return // if not left mouse button
|
||
// restore css user-select
|
||
if (obj.last.userSelect == 'text') {
|
||
obj.last.userSelect = ''
|
||
query(obj.box).find('.w2ui-grid-body').css('user-select', 'none')
|
||
}
|
||
// regular record select
|
||
if (obj.selectType == 'row' && (query(event.target).parents().hasClass('w2ui-head') || query(event.target).hasClass('w2ui-head'))) return
|
||
if (obj.last.move && obj.last.move.type == 'expand') return
|
||
// if altKey - alow text selection
|
||
if (event.altKey) {
|
||
query(obj.box).find('.w2ui-grid-body').css('user-select', 'text')
|
||
obj.selectNone()
|
||
obj.last.move = { type: 'text-select' }
|
||
obj.last.userSelect = 'text'
|
||
} else {
|
||
let tmp = event.target
|
||
let pos = {
|
||
x: event.offsetX - 10,
|
||
y: event.offsetY - 10
|
||
}
|
||
let tmps = false
|
||
while (tmp) {
|
||
if (tmp.classList && tmp.classList.contains('w2ui-grid')) break
|
||
if (tmp.tagName && tmp.tagName.toUpperCase() == 'TD') tmps = true
|
||
if (tmp.tagName && tmp.tagName.toUpperCase() != 'TR' && tmps == true) {
|
||
pos.x += tmp.offsetLeft
|
||
pos.y += tmp.offsetTop
|
||
}
|
||
tmp = tmp.parentNode
|
||
}
|
||
obj.last.move = {
|
||
x : event.screenX,
|
||
y : event.screenY,
|
||
divX : 0,
|
||
divY : 0,
|
||
focusX : pos.x,
|
||
focusY : pos.y,
|
||
recid : query(event.target).parents('tr').attr('recid'),
|
||
column : parseInt(event.target.tagName.toUpperCase() == 'TD' ? query(event.target).attr('col') : query(event.target).parents('td').attr('col')),
|
||
type : 'select',
|
||
ghost : false,
|
||
start : true
|
||
}
|
||
if (obj.last.move.recid == null) obj.last.move.type = 'select-column'
|
||
// set focus to grid
|
||
let target = event.target
|
||
let $input = query(obj.box).find('#grid_'+ obj.name + '_focus')
|
||
// move input next to cursor so screen does not jump
|
||
if (obj.last.move) {
|
||
let sLeft = obj.last.move.focusX
|
||
let sTop = obj.last.move.focusY
|
||
let $owner = query(target).parents('table').parent()
|
||
if ($owner.hasClass('w2ui-grid-records') || $owner.hasClass('w2ui-grid-frecords')
|
||
|| $owner.hasClass('w2ui-grid-columns') || $owner.hasClass('w2ui-grid-fcolumns')
|
||
|| $owner.hasClass('w2ui-grid-summary')) {
|
||
sLeft = obj.last.move.focusX - query(obj.box).find('#grid_'+ obj.name +'_records').prop('scrollLeft')
|
||
sTop = obj.last.move.focusY - query(obj.box).find('#grid_'+ obj.name +'_records').prop('scrollTop')
|
||
}
|
||
if (query(target).hasClass('w2ui-grid-footer') || query(target).parents('div.w2ui-grid-footer').length > 0) {
|
||
sTop = query(obj.box).find('#grid_'+ obj.name +'_footer').get(0).offsetTop
|
||
}
|
||
// if clicked on toolbar
|
||
if ($owner.hasClass('w2ui-scroll-wrapper') && $owner.parent().hasClass('w2ui-toolbar')) {
|
||
sLeft = obj.last.move.focusX - $owner.prop('scrollLeft')
|
||
}
|
||
$input.css({
|
||
left: sLeft - 10,
|
||
top : sTop
|
||
})
|
||
}
|
||
// if toolbar input is clicked
|
||
setTimeout(() => {
|
||
if (!obj.last.inEditMode) {
|
||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) {
|
||
target.focus()
|
||
} else {
|
||
if ($input.get(0) !== document.active) $input.get(0)?.focus({ preventScroll: true })
|
||
}
|
||
}
|
||
}, 50)
|
||
// disable click select for this condition
|
||
if (!obj.multiSelect && !obj.reorderRows && obj.last.move.type == 'drag') {
|
||
delete obj.last.move
|
||
}
|
||
}
|
||
if (obj.reorderRows == true) {
|
||
let el = event.target
|
||
if (el.tagName.toUpperCase() != 'TD') el = query(el).parents('td')[0]
|
||
if (query(el).hasClass('w2ui-col-number') || query(el).hasClass('w2ui-col-order')) {
|
||
obj.selectNone()
|
||
obj.last.move.reorder = true
|
||
// suppress hover
|
||
let eColor = query(obj.box).find('.w2ui-even.w2ui-empty-record').css('background-color')
|
||
let oColor = query(obj.box).find('.w2ui-odd.w2ui-empty-record').css('background-color')
|
||
query(obj.box).find('.w2ui-even td').filter(':not(.w2ui-col-number)').css('background-color', eColor)
|
||
query(obj.box).find('.w2ui-odd td').filter(':not(.w2ui-col-number)').css('background-color', oColor)
|
||
// display empty record and ghost record
|
||
let mv = obj.last.move
|
||
let recs = query(obj.box).find('.w2ui-grid-records')
|
||
if (!mv.ghost) {
|
||
let row = query(obj.box).find(`#grid_${obj.name}_rec_${mv.recid}`)
|
||
let tmp = row.parents('table').find('tr:first-child').get(0).cloneNode(true)
|
||
mv.offsetY = event.offsetY
|
||
mv.from = mv.recid
|
||
mv.pos = { top: row.get(0).offsetTop-1, left: row.get(0).offsetLeft }
|
||
mv.ghost = query(row.get(0).cloneNode(true))
|
||
mv.ghost.removeAttr('id')
|
||
mv.ghost.find('td').css({
|
||
'border-top': '1px solid silver',
|
||
'border-bottom': '1px solid silver'
|
||
})
|
||
row.find('td').remove()
|
||
row.append(`<td colspan="1000"><div class="w2ui-reorder-empty" style="height: ${(obj.recordHeight - 2)}px"></div></td>`)
|
||
recs.append('<div id="grid_'+ obj.name + '_ghost_line" style="position: absolute; z-index: 999999; pointer-events: none; width: 100%;"></div>')
|
||
recs.append('<table id="grid_'+ obj.name + '_ghost" style="position: absolute; z-index: 999998; opacity: 0.9; pointer-events: none;"></table>')
|
||
query(obj.box).find('#grid_'+ obj.name + '_ghost').append(tmp).append(mv.ghost)
|
||
}
|
||
let ghost = query(obj.box).find('#grid_'+ obj.name + '_ghost')
|
||
ghost.css({
|
||
top : mv.pos.top + 'px',
|
||
left : mv.pos.left + 'px'
|
||
})
|
||
} else {
|
||
obj.last.move.reorder = false
|
||
}
|
||
}
|
||
query(document)
|
||
.on('mousemove.w2ui-' + obj.name, mouseMove)
|
||
.on('mouseup.w2ui-' + obj.name, mouseStop)
|
||
// needed when grid grids are nested, see issue #1275
|
||
event.stopPropagation()
|
||
}
|
||
function mouseMove(event) {
|
||
if (!event.target.tagName) {
|
||
// element has no tagName - most likely the target is the #document itself
|
||
// this can happen is you click+drag and move the mouse out of the DOM area,
|
||
// e.g. into the browser's toolbar area
|
||
return
|
||
}
|
||
let mv = obj.last.move
|
||
if (!mv || ['select', 'select-column'].indexOf(mv.type) == -1) return
|
||
mv.divX = (event.screenX - mv.x)
|
||
mv.divY = (event.screenY - mv.y)
|
||
if (Math.abs(mv.divX) <= 1 && Math.abs(mv.divY) <= 1) return // only if moved more then 1px
|
||
obj.last.cancelClick = true
|
||
if (obj.reorderRows == true && obj.last.move.reorder) {
|
||
let tmp = query(event.target).parents('tr')
|
||
let recid = tmp.attr('recid')
|
||
if (recid == '-none-') recid = 'bottom'
|
||
if (recid != mv.from) {
|
||
// let row1 = query(obj.box).find('#grid_'+ obj.name + '_rec_'+ mv.recid)
|
||
let row2 = query(obj.box).find('#grid_'+ obj.name + '_rec_'+ recid)
|
||
query(obj.box).find('.insert-before')
|
||
row2.addClass('insert-before')
|
||
// MOVABLE GHOST
|
||
// if (event.screenY - mv.lastY < 0) row1.after(row2); else row2.after(row1);
|
||
mv.lastY = event.screenY
|
||
mv.to = recid
|
||
// line to insert before
|
||
let pos = { top: row2.get(0)?.offsetTop, left: row2.get(0)?.offsetLeft }
|
||
let ghost_line = query(obj.box).find('#grid_'+ obj.name + '_ghost_line')
|
||
if (pos) {
|
||
ghost_line.css({
|
||
top : pos.top + 'px',
|
||
left : mv.pos.left + 'px',
|
||
'border-top': '2px solid #769EFC'
|
||
})
|
||
} else {
|
||
ghost_line.css({
|
||
'border-top': '2px solid transparent'
|
||
})
|
||
}
|
||
}
|
||
let ghost = query(obj.box).find('#grid_'+ obj.name + '_ghost')
|
||
ghost.css({
|
||
top : (mv.pos.top + mv.divY) + 'px',
|
||
left : mv.pos.left + 'px'
|
||
})
|
||
return
|
||
}
|
||
if (mv.start && mv.recid) {
|
||
obj.selectNone()
|
||
mv.start = false
|
||
}
|
||
let newSel = []
|
||
let recid = (event.target.tagName.toUpperCase() == 'TR' ? query(event.target).attr('recid') : query(event.target).parents('tr').attr('recid'))
|
||
if (recid == null) {
|
||
// select by dragging columns
|
||
if (obj.selectType == 'row') return
|
||
if (obj.last.move && obj.last.move.type == 'select') return
|
||
let col = parseInt(query(event.target).parents('td').attr('col'))
|
||
if (isNaN(col)) {
|
||
obj.removeRange('column-selection')
|
||
query(obj.box).find('.w2ui-grid-columns .w2ui-col-header, .w2ui-grid-fcolumns .w2ui-col-header').removeClass('w2ui-col-selected')
|
||
query(obj.box).find('.w2ui-col-number').removeClass('w2ui-row-selected')
|
||
delete mv.colRange
|
||
} else {
|
||
// add all columns in between
|
||
let newRange = col + '-' + col
|
||
if (mv.column < col) newRange = mv.column + '-' + col
|
||
if (mv.column > col) newRange = col + '-' + mv.column
|
||
// array of selected columns
|
||
let cols = []
|
||
let tmp = newRange.split('-')
|
||
for (let ii = parseInt(tmp[0]); ii <= parseInt(tmp[1]); ii++) {
|
||
cols.push(ii)
|
||
}
|
||
if (mv.colRange != newRange) {
|
||
edataCol = obj.trigger('columnSelect', { target: obj.name, columns: cols })
|
||
if (edataCol.isCancelled !== true) {
|
||
if (mv.colRange == null) obj.selectNone()
|
||
// highlight columns
|
||
let tmp = newRange.split('-')
|
||
query(obj.box).find('.w2ui-grid-columns .w2ui-col-header, .w2ui-grid-fcolumns .w2ui-col-header').removeClass('w2ui-col-selected')
|
||
for (let j = parseInt(tmp[0]); j <= parseInt(tmp[1]); j++) {
|
||
query(obj.box).find('#grid_'+ obj.name +'_column_' + j + ' .w2ui-col-header').addClass('w2ui-col-selected')
|
||
}
|
||
query(obj.box).find('.w2ui-col-number').not('.w2ui-head').addClass('w2ui-row-selected')
|
||
// show new range
|
||
mv.colRange = newRange
|
||
obj.removeRange('column-selection')
|
||
obj.addRange({
|
||
name : 'column-selection',
|
||
range : [{ recid: obj.records[0].recid, column: tmp[0] }, { recid: obj.records[obj.records.length-1].recid, column: tmp[1] }],
|
||
style : 'background-color: rgba(90, 145, 234, 0.1)'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
} else { // regular selection
|
||
let ind1 = obj.get(mv.recid, true)
|
||
// this happens when selection is started on summary row
|
||
if (ind1 == null || (obj.records[ind1] && obj.records[ind1].recid != mv.recid)) return
|
||
let ind2 = obj.get(recid, true)
|
||
// this happens when selection is extended into summary row (a good place to implement scrolling)
|
||
if (ind2 == null) return
|
||
let col1 = parseInt(mv.column)
|
||
let col2 = parseInt(event.target.tagName.toUpperCase() == 'TD' ? query(event.target).attr('col') : query(event.target).parents('td').attr('col'))
|
||
if (isNaN(col1) && isNaN(col2)) { // line number select entire record
|
||
col1 = 0
|
||
col2 = obj.columns.length-1
|
||
}
|
||
if (ind1 > ind2) { let tmp = ind1; ind1 = ind2; ind2 = tmp }
|
||
// check if need to refresh
|
||
let tmp = 'ind1:'+ ind1 +',ind2;'+ ind2 +',col1:'+ col1 +',col2:'+ col2
|
||
if (mv.range == tmp) return
|
||
mv.range = tmp
|
||
for (let i = ind1; i <= ind2; i++) {
|
||
if (obj.last.searchIds.length > 0 && obj.last.searchIds.indexOf(i) == -1) continue
|
||
if (obj.selectType != 'row') {
|
||
if (col1 > col2) { let tmp = col1; col1 = col2; col2 = tmp }
|
||
for (let c = col1; c <= col2; c++) {
|
||
if (obj.columns[c].hidden) continue
|
||
newSel.push({ recid: obj.records[i].recid, column: parseInt(c) })
|
||
}
|
||
} else {
|
||
newSel.push(obj.records[i].recid)
|
||
}
|
||
}
|
||
if (obj.selectType != 'row') {
|
||
let sel = obj.getSelection()
|
||
// add more items
|
||
let tmp = []
|
||
for (let ns = 0; ns < newSel.length; ns++) {
|
||
let flag = false
|
||
for (let s = 0; s < sel.length; s++) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true
|
||
if (!flag) tmp.push({ recid: newSel[ns].recid, column: newSel[ns].column })
|
||
}
|
||
obj.select(tmp)
|
||
// remove items
|
||
tmp = []
|
||
for (let s = 0; s < sel.length; s++) {
|
||
let flag = false
|
||
for (let ns = 0; ns < newSel.length; ns++) if (newSel[ns].recid == sel[s].recid && newSel[ns].column == sel[s].column) flag = true
|
||
if (!flag) tmp.push({ recid: sel[s].recid, column: sel[s].column })
|
||
}
|
||
obj.unselect(tmp)
|
||
} else {
|
||
if (obj.multiSelect) {
|
||
let sel = obj.getSelection()
|
||
for (let ns = 0; ns < newSel.length; ns++) {
|
||
if (sel.indexOf(newSel[ns]) == -1) obj.select(newSel[ns]) // add more items
|
||
}
|
||
for (let s = 0; s < sel.length; s++) {
|
||
if (newSel.indexOf(sel[s]) == -1) obj.unselect(sel[s]) // remove items
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function mouseStop (event) {
|
||
let mv = obj.last.move
|
||
setTimeout(() => { delete obj.last.cancelClick }, 1)
|
||
if (query(event.target).parents().hasClass('.w2ui-head') || query(event.target).hasClass('.w2ui-head')) return
|
||
if (mv && ['select', 'select-column'].indexOf(mv.type) != -1) {
|
||
if (mv.colRange != null && edataCol.isCancelled !== true) {
|
||
let tmp = mv.colRange.split('-')
|
||
let sel = []
|
||
for (let i = 0; i < obj.records.length; i++) {
|
||
let cols = []
|
||
for (let j = parseInt(tmp[0]); j <= parseInt(tmp[1]); j++) cols.push(j)
|
||
sel.push({ recid: obj.records[i].recid, column: cols })
|
||
}
|
||
obj.removeRange('column-selection')
|
||
edataCol.finish()
|
||
obj.select(sel)
|
||
}
|
||
if (obj.reorderRows == true && obj.last.move.reorder) {
|
||
if (mv.to != null) {
|
||
// event
|
||
let edata = obj.trigger('reorderRow', { target: obj.name, recid: mv.from, moveBefore: mv.to })
|
||
if (edata.isCancelled === true) {
|
||
resetRowReorder()
|
||
delete obj.last.move
|
||
return
|
||
}
|
||
// default behavior
|
||
let ind1 = obj.get(mv.from, true)
|
||
let ind2 = obj.get(mv.to, true)
|
||
if (mv.to == 'bottom') ind2 = obj.records.length // end of list
|
||
let tmp = obj.records[ind1]
|
||
// swap records
|
||
if (ind1 != null && ind2 != null) {
|
||
obj.records.splice(ind1, 1)
|
||
if (ind1 > ind2) {
|
||
obj.records.splice(ind2, 0, tmp)
|
||
} else {
|
||
obj.records.splice(ind2 - 1, 0, tmp)
|
||
}
|
||
}
|
||
// clear sortData
|
||
obj.sortData = []
|
||
query(obj.box)
|
||
.find(`#grid_${obj.name}_columns .w2ui-col-header`)
|
||
.removeClass('w2ui-col-sorted')
|
||
resetRowReorder()
|
||
// event after
|
||
edata.finish()
|
||
} else {
|
||
resetRowReorder()
|
||
}
|
||
}
|
||
}
|
||
delete obj.last.move
|
||
query(document).off('.w2ui-' + obj.name)
|
||
}
|
||
function resetRowReorder() {
|
||
query(obj.box).find(`#grid_${obj.name}_ghost`).remove()
|
||
query(obj.box).find(`#grid_${obj.name}_ghost_line`).remove()
|
||
obj.refresh()
|
||
delete obj.last.move
|
||
}
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// remove all events
|
||
query(this.box).off()
|
||
// clean up
|
||
if (typeof this.toolbar == 'object' && this.toolbar.destroy) this.toolbar.destroy()
|
||
if (query(this.box).find(`#grid_${this.name}_body`).length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-grid w2ui-inactive')
|
||
.html('')
|
||
}
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
// ===========================================
|
||
// --- Internal Functions
|
||
initColumnOnOff() {
|
||
let items = [
|
||
{ id: 'line-numbers', text: 'Line #', checked: this.show.lineNumbers }
|
||
]
|
||
// columns
|
||
for (let c = 0; c < this.columns.length; c++) {
|
||
let col = this.columns[c]
|
||
let text = this.columns[c].text
|
||
if (col.hideable === false) continue
|
||
if (!text && this.columns[c].tooltip) text = this.columns[c].tooltip
|
||
if (!text) text = '- column '+ (parseInt(c) + 1) +' -'
|
||
items.push({ id: col.field, text: w2utils.stripTags(text), checked: !col.hidden })
|
||
}
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if ((url && this.show.skipRecords) || this.show.saveRestoreState) {
|
||
items.push({ text: '--' })
|
||
}
|
||
// skip records
|
||
if (this.show.skipRecords) {
|
||
let skip = w2utils.lang('Skip') +
|
||
`<input id="${this.name}_skip" type="text" class="w2ui-input w2ui-grid-skip" value="${this.offset}">` +
|
||
w2utils.lang('records')
|
||
items.push({ id: 'w2ui-skip', text: skip, group: false, icon: 'w2ui-icon-empty' })
|
||
}
|
||
// save/restore state
|
||
if (this.show.saveRestoreState) {
|
||
items.push(
|
||
{ id: 'w2ui-stateSave', text: w2utils.lang('Save Grid State'), icon: 'w2ui-icon-empty', group: false },
|
||
{ id: 'w2ui-stateReset', text: w2utils.lang('Restore Default State'), icon: 'w2ui-icon-empty', group: false }
|
||
)
|
||
}
|
||
let selected = []
|
||
items.forEach(item => {
|
||
item.text = w2utils.lang(item.text) // translate
|
||
if (item.checked) selected.push(item.id)
|
||
})
|
||
this.toolbar.set('w2ui-column-on-off', { selected, items })
|
||
return items
|
||
}
|
||
initColumnDrag(box) {
|
||
// throw error if using column groups
|
||
if (this.columnGroups && this.columnGroups.length) {
|
||
throw 'Draggable columns are not currently supported with column groups.'
|
||
}
|
||
let self = this
|
||
let dragData = {
|
||
pressed: false,
|
||
targetPos: null,
|
||
columnHead: null
|
||
}
|
||
let hasInvalidClass = (target, lastColumn) => {
|
||
let iClass = ['w2ui-col-number', 'w2ui-col-expand', 'w2ui-col-select']
|
||
if (lastColumn !== true) iClass.push('w2ui-head-last')
|
||
for (let i = 0; i < iClass.length; i++) {
|
||
if (query(target).closest('.w2ui-head').hasClass(iClass[i])) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
// attach original event listener
|
||
query(self.box)
|
||
.off('.colDrag')
|
||
.on('mousedown.colDrag', dragColStart)
|
||
function dragColStart(event) {
|
||
if (dragData.pressed || dragData.numberPreColumnsPresent === 0 || event.button !== 0) return
|
||
let edata, columns, origColumn, origColumnNumber
|
||
let preColHeadersSelector = '.w2ui-head.w2ui-col-number, .w2ui-head.w2ui-col-expand, .w2ui-head.w2ui-col-select'
|
||
// do nothing if it is not a header
|
||
if (!query(event.target).parents().hasClass('w2ui-head') || hasInvalidClass(event.target)) return
|
||
dragData.pressed = true
|
||
dragData.initialX = event.pageX
|
||
dragData.initialY = event.pageY
|
||
dragData.numberPreColumnsPresent = query(self.box).find(preColHeadersSelector).length
|
||
// start event for drag start
|
||
dragData.columnHead = origColumn = query(event.target).closest('.w2ui-head')
|
||
dragData.originalPos = origColumnNumber = parseInt(origColumn.attr('col'), 10)
|
||
edata = self.trigger('columnDragStart', { originalEvent: event, origColumnNumber, target: origColumn[0] })
|
||
if (edata.isCancelled === true) return false
|
||
columns = dragData.columns = query(self.box).find('.w2ui-head:not(.w2ui-head-last)')
|
||
// add events
|
||
query(document).on('mouseup.colDrag', dragColEnd)
|
||
query(document).on('mousemove.colDrag', dragColOver)
|
||
let col = self.columns[dragData.originalPos]
|
||
let colText = w2utils.lang(typeof col.text == 'function' ? col.text(col) : col.text)
|
||
dragData.ghost = query.html(`<span col="${dragData.originalPos}">${colText}</span>`)[0]
|
||
query(document.body).append(dragData.ghost)
|
||
query(dragData.ghost)
|
||
.css({
|
||
display: 'none',
|
||
left: event.pageX,
|
||
top: event.pageY,
|
||
opacity: 1,
|
||
margin: '3px 0 0 20px',
|
||
padding: '3px',
|
||
'background-color': 'white',
|
||
position: 'fixed',
|
||
'z-index': 999999,
|
||
})
|
||
.addClass('.w2ui-grid-ghost')
|
||
|
||
// establish current offsets
|
||
dragData.offsets = []
|
||
for (let i = 0, l = columns.length; i < l; i++) {
|
||
let rect = columns[i].getBoundingClientRect()
|
||
dragData.offsets.push(rect.left)
|
||
}
|
||
// conclude event
|
||
edata.finish()
|
||
}
|
||
function dragColOver(event) {
|
||
if (!dragData.pressed || !dragData.columnHead) return
|
||
let cursorX = event.pageX
|
||
let cursorY = event.pageY
|
||
if (!hasInvalidClass(event.target, true)) {
|
||
markIntersection(event)
|
||
}
|
||
trackGhost(cursorX, cursorY)
|
||
}
|
||
function dragColEnd(event) {
|
||
if (!dragData.pressed || !dragData.columnHead) return
|
||
dragData.pressed = false
|
||
let edata, target, selected, columnConfig
|
||
let finish = () => {
|
||
let ghosts = query(self.box).find('.w2ui-grid-ghost')
|
||
query(self.box).find('.w2ui-intersection-marker').hide()
|
||
query(dragData.ghost).remove()
|
||
ghosts.remove()
|
||
// dragData.columns.css({ overflow: '' }).children('div').css({ overflow: '' });
|
||
query(document).off('.colDrag')
|
||
dragData = {}
|
||
}
|
||
// if no move, then click event for sorting
|
||
if (event.pageX == dragData.initialX && event.pageY == dragData.initialY) {
|
||
self.columnClick(self.columns[dragData.originalPos].field, event)
|
||
finish()
|
||
return
|
||
}
|
||
// start event for drag start
|
||
edata = self.trigger('columnDragEnd', { originalEvent: event, target: dragData.columnHead[0], dragData })
|
||
if (edata.isCancelled === true) return false
|
||
selected = self.columns[dragData.originalPos]
|
||
columnConfig = self.columns
|
||
if (dragData.originalPos != dragData.targetPos && dragData.targetPos != null) {
|
||
columnConfig.splice(dragData.targetPos, 0, w2utils.clone(selected))
|
||
columnConfig.splice(columnConfig.indexOf(selected), 1)
|
||
}
|
||
finish()
|
||
self.refresh()
|
||
edata.finish({ targetColumn: target - 1 })
|
||
}
|
||
function markIntersection(event) {
|
||
// if mouse over is not over table
|
||
if (query(event.target).closest('td').length == 0) {
|
||
return
|
||
}
|
||
// if mouse over invalid column
|
||
let rect1 = query(self.box).find('.w2ui-grid-body').get(0).getBoundingClientRect()
|
||
let rect2 = query(event.target).closest('td').get(0).getBoundingClientRect()
|
||
query(self.box).find('.w2ui-intersection-marker')
|
||
.show()
|
||
.css({
|
||
left: (rect2.left - rect1.left) + 'px'
|
||
})
|
||
let td = query(event.target).closest('td')
|
||
dragData.targetPos = td.hasClass('w2ui-head-last') ? self.columns.length : parseInt(td.attr('col'))
|
||
return
|
||
}
|
||
function trackGhost(cursorX, cursorY){
|
||
query(dragData.ghost)
|
||
.css({
|
||
left : (cursorX - 10) + 'px',
|
||
top : (cursorY - 10) + 'px'
|
||
})
|
||
.show()
|
||
}
|
||
// return an object to remove drag if it has ever been enabled
|
||
return {
|
||
remove() {
|
||
query(self.box).off('.colDrag')
|
||
self.last.columnDrag = false
|
||
}
|
||
}
|
||
}
|
||
columnOnOff(event, field) {
|
||
// event before
|
||
let edata = this.trigger('columnOnOff', { target: this.name, field: field, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// collapse expanded rows
|
||
let rows = this.find({ 'w2ui.expanded': true }, true)
|
||
for (let r = 0; r < rows.length; r++) {
|
||
let tmp = this.records[r].w2ui
|
||
if (tmp && !Array.isArray(tmp.children)) {
|
||
this.records[r].w2ui.expanded = false
|
||
}
|
||
}
|
||
// show/hide
|
||
if (field == 'line-numbers') {
|
||
this.show.lineNumbers = !this.show.lineNumbers
|
||
this.refresh()
|
||
} else {
|
||
let col = this.getColumn(field)
|
||
if (col.hidden) {
|
||
this.showColumn(col.field)
|
||
} else {
|
||
this.hideColumn(col.field)
|
||
}
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
initToolbar() {
|
||
// if it is already initiazlied
|
||
if (this.toolbar.render != null) {
|
||
return
|
||
}
|
||
let tb_items = this.toolbar.items || []
|
||
this.toolbar.items = []
|
||
this.toolbar = new w2toolbar(w2utils.extend({}, this.toolbar, { name: this.name +'_toolbar', owner: this }))
|
||
if (this.show.toolbarReload) {
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.reload))
|
||
}
|
||
if (this.show.toolbarColumns) {
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.columns))
|
||
}
|
||
if (this.show.toolbarSearch) {
|
||
let html =`
|
||
<div class="w2ui-grid-search-input">
|
||
${this.buttons.search.html}
|
||
<div id="grid_${this.name}_search_name" class="w2ui-grid-search-name">
|
||
<span class="name-icon w2ui-icon-search"></span>
|
||
<span class="name-text"></span>
|
||
<span class="name-cross w2ui-action" data-click="searchReset">x</span>
|
||
</div>
|
||
<input type="text" id="grid_${this.name}_search_all" class="w2ui-search-all" tabindex="-1"
|
||
autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
|
||
placeholder="${w2utils.lang(this.last.label, true)}" value="${this.last.search}"
|
||
data-focus="searchSuggest" data-click="stop"
|
||
>
|
||
<div class="w2ui-search-drop w2ui-action" data-click="searchOpen"
|
||
style="${this.multiSearch ? '' : 'display: none'}">
|
||
<span class="w2ui-icon-drop"></span>
|
||
</div>
|
||
</div>`
|
||
this.toolbar.items.push({
|
||
id: 'w2ui-search',
|
||
type: 'html',
|
||
html,
|
||
onRefresh: async (event) => {
|
||
await event.complete
|
||
let input = query(this.box).find(`#grid_${this.name}_search_all`)
|
||
w2utils.bindEvents(query(this.box).find(`#grid_${this.name}_search_all, .w2ui-action`), this)
|
||
// slow down live search calls
|
||
let slowSearch = w2utils.debounce((event) => {
|
||
let val = event.target.value
|
||
if (this.liveSearch && this.last.liveText != val) {
|
||
this.last.liveText = val
|
||
this.search(this.last.field, val)
|
||
}
|
||
if (event.keyCode == 40) { // arrow down
|
||
this.searchSuggest(true)
|
||
}
|
||
}, 250)
|
||
input
|
||
.on('change', event => {
|
||
if (!this.liveSearch) {
|
||
this.search(this.last.field, event.target.value)
|
||
this.searchSuggest(true, true, this)
|
||
}
|
||
})
|
||
.on('blur', () => { this.last.liveText = '' })
|
||
.on('keyup', slowSearch)
|
||
}
|
||
})
|
||
}
|
||
if (Array.isArray(tb_items)) {
|
||
let ids = tb_items.map(item => item.id)
|
||
if (this.show.toolbarAdd && !ids.includes(this.buttons.add.id)) {
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.add))
|
||
}
|
||
if (this.show.toolbarEdit && !ids.includes(this.buttons.edit.id)) {
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.edit))
|
||
}
|
||
if (this.show.toolbarDelete && !ids.includes(this.buttons.delete.id)) {
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.delete))
|
||
}
|
||
if (this.show.toolbarSave && !ids.includes(this.buttons.save.id)) {
|
||
if (this.show.toolbarAdd || this.show.toolbarDelete || this.show.toolbarEdit) {
|
||
this.toolbar.items.push({ type: 'break', id: 'w2ui-break2' })
|
||
}
|
||
this.toolbar.items.push(w2utils.extend({}, this.buttons.save))
|
||
}
|
||
// fill in overwritten items with default buttons
|
||
// ids are w2ui-* but in this.buttons the map is just [add, edit, delete]
|
||
// must specify at least {id, name} in this.toolbar.items if you want to keep order
|
||
tb_items = tb_items.map(item => this.buttons[item.name]
|
||
? w2utils.extend({}, this.buttons[item.name], item) : item)
|
||
}
|
||
// add original buttons
|
||
this.toolbar.items.push(...tb_items)
|
||
// =============================================
|
||
// ------ Toolbar onClick processing
|
||
this.toolbar.on('click', (event) => {
|
||
let edata = this.trigger('toolbar', { target: event.target, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
let edata2
|
||
switch (event.detail.item.id) {
|
||
case 'w2ui-reload':
|
||
edata2 = this.trigger('reload', { target: this.name })
|
||
if (edata2.isCancelled === true) return false
|
||
this.reload()
|
||
edata2.finish()
|
||
break
|
||
case 'w2ui-column-on-off':
|
||
// TODO: tap on columns will hide menu before opening, only in grid not in toolbar
|
||
if (event.detail.subItem) {
|
||
let id = event.detail.subItem.id
|
||
if (['w2ui-stateSave', 'w2ui-stateReset'].includes(id)) {
|
||
this[id.substring(5)]()
|
||
} else if (id == 'w2ui-skip') {
|
||
// empty
|
||
} else {
|
||
this.columnOnOff(event, event.detail.subItem.id)
|
||
}
|
||
} else {
|
||
this.initColumnOnOff()
|
||
// init input control with records to skip
|
||
setTimeout(() => {
|
||
query(`#w2overlay-${this.name}_toolbar-drop .w2ui-grid-skip`)
|
||
.off('.w2ui-grid')
|
||
.on('click.w2ui-grid', evt => {
|
||
evt.stopPropagation()
|
||
})
|
||
.on('keypress', evt => {
|
||
if (evt.keyCode == 13) {
|
||
this.skip(evt.target.value)
|
||
this.toolbar.click('w2ui-column-on-off') // close menu
|
||
}
|
||
})
|
||
}, 100)
|
||
}
|
||
break
|
||
case 'w2ui-add':
|
||
// events
|
||
edata2 = this.trigger('add', { target: this.name, recid: null })
|
||
if (edata2.isCancelled === true) return false
|
||
edata2.finish()
|
||
break
|
||
case 'w2ui-edit': {
|
||
let sel = this.getSelection()
|
||
let recid = null
|
||
if (sel.length == 1) recid = sel[0]
|
||
// events
|
||
edata2 = this.trigger('edit', { target: this.name, recid: recid })
|
||
if (edata2.isCancelled === true) return false
|
||
edata2.finish()
|
||
break
|
||
}
|
||
case 'w2ui-delete':
|
||
this.delete()
|
||
break
|
||
case 'w2ui-save':
|
||
this.save()
|
||
break
|
||
}
|
||
// no default action
|
||
edata.finish()
|
||
})
|
||
this.toolbar.on('refresh', (event) => {
|
||
if (event.target == 'w2ui-search') {
|
||
let sd = this.searchData
|
||
setTimeout(() => {
|
||
this.searchInitInput(this.last.field, (sd.length == 1 ? sd[0].value : null))
|
||
}, 1)
|
||
}
|
||
})
|
||
}
|
||
initResize() {
|
||
let obj = this
|
||
query(this.box).find('.w2ui-resizer')
|
||
.off('.grid-col-resize')
|
||
.on('click.grid-col-resize', function(event) {
|
||
if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true
|
||
if (event.preventDefault) event.preventDefault()
|
||
})
|
||
.on('mousedown.grid-col-resize', function(event) {
|
||
if (!event) event = window.event
|
||
obj.last.colResizing = true
|
||
obj.last.tmp = {
|
||
x : event.screenX,
|
||
y : event.screenY,
|
||
gx : event.screenX,
|
||
gy : event.screenY,
|
||
col : parseInt(query(this).attr('name'))
|
||
}
|
||
// find tds that will be resized
|
||
obj.last.tmp.tds = query(obj.box).find('#grid_'+ obj.name +'_body table tr:first-child td[col="'+ obj.last.tmp.col +'"]')
|
||
if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true
|
||
if (event.preventDefault) event.preventDefault()
|
||
// fix sizes
|
||
for (let c = 0; c < obj.columns.length; c++) {
|
||
if (obj.columns[c].hidden) continue
|
||
if (obj.columns[c].sizeOriginal == null) obj.columns[c].sizeOriginal = obj.columns[c].size
|
||
obj.columns[c].size = obj.columns[c].sizeCalculated
|
||
}
|
||
let edata = { phase: 'before', type: 'columnResize', target: obj.name, column: obj.last.tmp.col, field: obj.columns[obj.last.tmp.col].field }
|
||
edata = obj.trigger(w2utils.extend(edata, { resizeBy: 0, originalEvent: event }))
|
||
// set move event
|
||
let timer
|
||
let mouseMove = function(event) {
|
||
if (obj.last.colResizing != true) return
|
||
if (!event) event = window.event
|
||
// event before
|
||
edata = obj.trigger(w2utils.extend(edata, { resizeBy: (event.screenX - obj.last.tmp.gx), originalEvent: event }))
|
||
if (edata.isCancelled === true) { edata.isCancelled = false; return }
|
||
// default action
|
||
obj.last.tmp.x = (event.screenX - obj.last.tmp.x)
|
||
obj.last.tmp.y = (event.screenY - obj.last.tmp.y)
|
||
let newWidth = (parseInt(obj.columns[obj.last.tmp.col].size) + obj.last.tmp.x) + 'px'
|
||
obj.columns[obj.last.tmp.col].size = newWidth
|
||
if (timer) clearTimeout(timer)
|
||
timer = setTimeout(() => {
|
||
obj.resizeRecords()
|
||
obj.scroll()
|
||
}, 100)
|
||
// quick resize
|
||
obj.last.tmp.tds.css({ width: newWidth })
|
||
// reset
|
||
obj.last.tmp.x = event.screenX
|
||
obj.last.tmp.y = event.screenY
|
||
}
|
||
let mouseUp = function(event) {
|
||
query(document).off('.grid-col-resize')
|
||
obj.resizeRecords()
|
||
obj.scroll()
|
||
// event after
|
||
edata.finish({ originalEvent: event })
|
||
// need timeout to finish processing events
|
||
setTimeout(() => { obj.last.colResizing = false }, 1)
|
||
}
|
||
query(document)
|
||
.off('.grid-col-resize')
|
||
.on('mousemove.grid-col-resize', mouseMove)
|
||
.on('mouseup.grid-col-resize', mouseUp)
|
||
})
|
||
.on('dblclick.grid-col-resize', function(event) {
|
||
let colId = parseInt(query(this).attr('name')),
|
||
col = obj.columns[colId],
|
||
maxDiff = 0
|
||
if (col.autoResize === false) {
|
||
return true
|
||
}
|
||
if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true
|
||
if (event.preventDefault) event.preventDefault()
|
||
query(obj.box).find('.w2ui-grid-records td[col="' + colId + '"] > div', obj.box).each(() => {
|
||
let thisDiff = this.offsetWidth - this.scrollWidth
|
||
if (thisDiff < maxDiff) {
|
||
maxDiff = thisDiff - 3 // 3px buffer needed for Firefox
|
||
}
|
||
})
|
||
// event before
|
||
let edata = { phase: 'before', type: 'columnAutoResize', target: obj.name, column: col, field: col.field }
|
||
edata = obj.trigger(w2utils.extend(edata, { resizeBy: Math.abs(maxDiff), originalEvent: event }))
|
||
if (edata.isCancelled === true) { edata.isCancelled = false; return }
|
||
if (maxDiff < 0) {
|
||
col.size = Math.min(parseInt(col.size) + Math.abs(maxDiff), col.max || Infinity) + 'px'
|
||
obj.resizeRecords()
|
||
obj.resizeRecords() // Why do we have to call it twice in order to show the scrollbar?
|
||
obj.scroll()
|
||
}
|
||
// event after
|
||
edata.finish({ originalEvent: event })
|
||
})
|
||
.each(el => {
|
||
let td = query(el).get(0).parentNode
|
||
query(el).css({
|
||
'height' : td.clientHeight + 'px',
|
||
'margin-left' : (td.clientWidth - 3) + 'px'
|
||
})
|
||
})
|
||
}
|
||
resizeBoxes() {
|
||
// elements
|
||
let header = query(this.box).find(`#grid_${this.name}_header`)
|
||
let toolbar = query(this.box).find(`#grid_${this.name}_toolbar`)
|
||
let fsummary = query(this.box).find(`#grid_${this.name}_fsummary`)
|
||
let summary = query(this.box).find(`#grid_${this.name}_summary`)
|
||
let footer = query(this.box).find(`#grid_${this.name}_footer`)
|
||
let body = query(this.box).find(`#grid_${this.name}_body`)
|
||
if (this.show.header) {
|
||
header.css({
|
||
top: '0px',
|
||
left: '0px',
|
||
right: '0px'
|
||
})
|
||
}
|
||
if (this.show.toolbar) {
|
||
toolbar.css({
|
||
top: (0 + (this.show.header ? w2utils.getSize(header, 'height') : 0)) + 'px',
|
||
left: '0px',
|
||
right: '0px'
|
||
})
|
||
}
|
||
if (this.summary.length > 0) {
|
||
fsummary.css({
|
||
bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0)) + 'px'
|
||
})
|
||
summary.css({
|
||
bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0)) + 'px',
|
||
right: '0px'
|
||
})
|
||
}
|
||
if (this.show.footer) {
|
||
footer.css({
|
||
bottom: '0px',
|
||
left: '0px',
|
||
right: '0px'
|
||
})
|
||
}
|
||
body.css({
|
||
top: (0 + (this.show.header ? w2utils.getSize(header, 'height') : 0) + (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)) + 'px',
|
||
bottom: (0 + (this.show.footer ? w2utils.getSize(footer, 'height') : 0) + (this.summary.length > 0 ? w2utils.getSize(summary, 'height') : 0)) + 'px',
|
||
left: '0px',
|
||
right: '0px'
|
||
})
|
||
}
|
||
resizeRecords() {
|
||
let obj = this
|
||
// remove empty records
|
||
query(this.box).find('.w2ui-empty-record').remove()
|
||
// -- Calculate Column size in PX
|
||
let box = query(this.box)
|
||
let grid = query(this.box).find(':scope > div.w2ui-grid-box')
|
||
let header = query(this.box).find(`#grid_${this.name}_header`)
|
||
let toolbar = query(this.box).find(`#grid_${this.name}_toolbar`)
|
||
let summary = query(this.box).find(`#grid_${this.name}_summary`)
|
||
let fsummary = query(this.box).find(`#grid_${this.name}_fsummary`)
|
||
let footer = query(this.box).find(`#grid_${this.name}_footer`)
|
||
let body = query(this.box).find(`#grid_${this.name}_body`)
|
||
let columns = query(this.box).find(`#grid_${this.name}_columns`)
|
||
let fcolumns = query(this.box).find(`#grid_${this.name}_fcolumns`)
|
||
let records = query(this.box).find(`#grid_${this.name}_records`)
|
||
let frecords = query(this.box).find(`#grid_${this.name}_frecords`)
|
||
let scroll1 = query(this.box).find(`#grid_${this.name}_scroll1`)
|
||
let lineNumberWidth = String(this.total).length * 8 + 10
|
||
if (lineNumberWidth < 34) lineNumberWidth = 34 // 3 digit width
|
||
if (this.lineNumberWidth != null) lineNumberWidth = this.lineNumberWidth
|
||
let bodyOverflowX = false
|
||
let bodyOverflowY = false
|
||
let sWidth = 0
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
if (this.columns[i].frozen || this.columns[i].hidden) continue
|
||
let cSize = parseInt(this.columns[i].sizeCalculated ? this.columns[i].sizeCalculated : this.columns[i].size)
|
||
sWidth += cSize
|
||
}
|
||
if (records[0]?.clientWidth < sWidth) bodyOverflowX = true
|
||
if (body[0]?.clientHeight - (columns[0]?.clientHeight ?? 0)
|
||
< (query(records).find(':scope > table')[0]?.clientHeight ?? 0) + (bodyOverflowX ? w2utils.scrollBarSize() : 0)) {
|
||
bodyOverflowY = true
|
||
}
|
||
// body might be expanded by data
|
||
if (!this.fixedBody) {
|
||
// allow it to render records, then resize
|
||
let bodyHeight = w2utils.getSize(columns, 'height')
|
||
+ w2utils.getSize(query(this.box).find('#grid_'+ this.name +'_records table'), 'height')
|
||
+ (bodyOverflowX ? w2utils.scrollBarSize() : 0)
|
||
let calculatedHeight = bodyHeight
|
||
+ (this.show.header ? w2utils.getSize(header, 'height') : 0)
|
||
+ (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)
|
||
+ (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0)
|
||
+ (this.show.footer ? w2utils.getSize(footer, 'height') : 0)
|
||
grid.css('height', calculatedHeight + 'px')
|
||
body.css('height', bodyHeight + 'px')
|
||
box.css('height', w2utils.getSize(grid, 'height') + 'px')
|
||
} else {
|
||
// fixed body height
|
||
let calculatedHeight = grid[0]?.clientHeight
|
||
- (this.show.header ? w2utils.getSize(header, 'height') : 0)
|
||
- (this.show.toolbar ? w2utils.getSize(toolbar, 'height') : 0)
|
||
- (summary.css('display') != 'none' ? w2utils.getSize(summary, 'height') : 0)
|
||
- (this.show.footer ? w2utils.getSize(footer, 'height') : 0)
|
||
body.css('height', calculatedHeight + 'px')
|
||
}
|
||
let buffered = this.records.length
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length
|
||
// apply overflow
|
||
if (!this.fixedBody) { bodyOverflowY = false }
|
||
if (bodyOverflowX || bodyOverflowY) {
|
||
columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last')
|
||
.css('width', w2utils.scrollBarSize() + 'px')
|
||
.show()
|
||
records.css({
|
||
top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) +'px',
|
||
'-webkit-overflow-scrolling': 'touch',
|
||
'overflow-x': (bodyOverflowX ? 'auto' : 'hidden'),
|
||
'overflow-y': (bodyOverflowY ? 'auto' : 'hidden')
|
||
})
|
||
} else {
|
||
columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last').hide()
|
||
records.css({
|
||
top: ((this.columnGroups.length > 0 && this.show.columns ? 1 : 0) + w2utils.getSize(columns, 'height')) +'px',
|
||
overflow: 'hidden'
|
||
})
|
||
if (records.length > 0) { this.last.scrollTop = 0; this.last.scrollLeft = 0 } // if no scrollbars, always show top
|
||
}
|
||
if (bodyOverflowX) {
|
||
frecords.css('margin-bottom', w2utils.scrollBarSize() + 'px')
|
||
scroll1.show()
|
||
} else {
|
||
frecords.css('margin-bottom', 0)
|
||
scroll1.hide()
|
||
}
|
||
frecords.css({ overflow: 'hidden', top: records.css('top') })
|
||
if (this.show.emptyRecords && !bodyOverflowY) {
|
||
let max = Math.floor((records[0]?.clientHeight ?? 0) / this.recordHeight) - 1
|
||
let leftover = 0
|
||
if (records[0]) leftover = records[0].scrollHeight - max * this.recordHeight
|
||
if (leftover >= this.recordHeight) {
|
||
leftover -= this.recordHeight
|
||
max++
|
||
}
|
||
if (this.fixedBody) {
|
||
for (let di = buffered; di < max; di++) {
|
||
addEmptyRow(di, this.recordHeight, this)
|
||
}
|
||
addEmptyRow(max, leftover, this)
|
||
}
|
||
}
|
||
function addEmptyRow(row, height, grid) {
|
||
let html1 = ''
|
||
let html2 = ''
|
||
let htmlp = ''
|
||
html1 += '<tr class="'+ (row % 2 ? 'w2ui-even' : 'w2ui-odd') + ' w2ui-empty-record" recid="-none-" style="height: '+ height +'px">'
|
||
html2 += '<tr class="'+ (row % 2 ? 'w2ui-even' : 'w2ui-odd') + ' w2ui-empty-record" recid="-none-" style="height: '+ height +'px">'
|
||
if (grid.show.lineNumbers) html1 += '<td class="w2ui-col-number"></td>'
|
||
if (grid.show.selectColumn) html1 += '<td class="w2ui-grid-data w2ui-col-select"></td>'
|
||
if (grid.show.expandColumn) html1 += '<td class="w2ui-grid-data w2ui-col-expand"></td>'
|
||
html2 += '<td class="w2ui-grid-data-spacer" col="start" style="border-right: 0"></td>'
|
||
if (grid.reorderRows) html2 += '<td class="w2ui-grid-data w2ui-col-order" col="order"></td>'
|
||
for (let j = 0; j < grid.columns.length; j++) {
|
||
let col = grid.columns[j]
|
||
if ((col.hidden || j < grid.last.colStart || j > grid.last.colEnd) && !col.frozen) continue
|
||
htmlp = '<td class="w2ui-grid-data" '+ (col.attr != null ? col.attr : '') +' col="'+ j +'"></td>'
|
||
if (col.frozen) html1 += htmlp; else html2 += htmlp
|
||
}
|
||
html1 += '<td class="w2ui-grid-data-last"></td> </tr>'
|
||
html2 += '<td class="w2ui-grid-data-last" col="end"></td> </tr>'
|
||
query(grid.box).find('#grid_'+ grid.name +'_frecords > table').append(html1)
|
||
query(grid.box).find('#grid_'+ grid.name +'_records > table').append(html2)
|
||
}
|
||
let width_box, percent
|
||
if (body.length > 0) {
|
||
let width_max = parseInt(body[0].clientWidth)
|
||
- (bodyOverflowY ? w2utils.scrollBarSize() : 0)
|
||
- (this.show.lineNumbers ? lineNumberWidth : 0)
|
||
- (this.reorderRows ? 26 : 0)
|
||
- (this.show.selectColumn ? 26 : 0)
|
||
- (this.show.expandColumn ? 26 : 0)
|
||
- 1 // left is 1px due to border width
|
||
width_box = width_max
|
||
percent = 0
|
||
// gridMinWidth processing
|
||
let restart = false
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.gridMinWidth > 0) {
|
||
if (col.gridMinWidth > width_box && col.hidden !== true) {
|
||
col.hidden = true
|
||
restart = true
|
||
}
|
||
if (col.gridMinWidth < width_box && col.hidden === true) {
|
||
col.hidden = false
|
||
restart = true
|
||
}
|
||
}
|
||
}
|
||
if (restart === true) {
|
||
this.refresh()
|
||
return
|
||
}
|
||
// assign PX column s
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.hidden) continue
|
||
if (String(col.size).substr(String(col.size).length-2).toLowerCase() == 'px') {
|
||
width_max -= parseFloat(col.size)
|
||
this.columns[i].sizeCalculated = col.size
|
||
this.columns[i].sizeType = 'px'
|
||
} else {
|
||
percent += parseFloat(col.size)
|
||
this.columns[i].sizeType = '%'
|
||
delete col.sizeCorrected
|
||
}
|
||
}
|
||
// if sum != 100% -- reassign proportionally
|
||
if (percent != 100 && percent > 0) {
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.hidden) continue
|
||
if (col.sizeType == '%') {
|
||
col.sizeCorrected = Math.round(parseFloat(col.size) * 100 * 100 / percent) / 100 + '%'
|
||
}
|
||
}
|
||
}
|
||
// calculate % columns
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.hidden) continue
|
||
if (col.sizeType == '%') {
|
||
if (this.columns[i].sizeCorrected != null) {
|
||
// make it 1px smaller, so margin of error can be calculated correctly
|
||
this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.sizeCorrected) / 100) - 1 + 'px'
|
||
} else {
|
||
// make it 1px smaller, so margin of error can be calculated correctly
|
||
this.columns[i].sizeCalculated = Math.floor(width_max * parseFloat(col.size) / 100) - 1 + 'px'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// fix margin of error that is due percentage calculations
|
||
let width_cols = 0
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
if (col.hidden) continue
|
||
if (col.min == null) col.min = 20
|
||
if (parseInt(col.sizeCalculated) < parseInt(col.min)) col.sizeCalculated = col.min + 'px'
|
||
if (parseInt(col.sizeCalculated) > parseInt(col.max)) col.sizeCalculated = col.max + 'px'
|
||
width_cols += parseInt(col.sizeCalculated)
|
||
}
|
||
let width_diff = parseInt(width_box) - parseInt(width_cols)
|
||
if (width_diff > 0 && percent > 0) {
|
||
let i = 0
|
||
while (true) {
|
||
let col = this.columns[i]
|
||
if (col == null) { i = 0; continue }
|
||
if (col.hidden || col.sizeType == 'px') { i++; continue }
|
||
col.sizeCalculated = (parseInt(col.sizeCalculated) + 1) + 'px'
|
||
width_diff--
|
||
if (width_diff === 0) break
|
||
i++
|
||
}
|
||
} else if (width_diff > 0) {
|
||
columns.find(':scope > table > tbody > tr:nth-child(1) td.w2ui-head-last')
|
||
.css('width', w2utils.scrollBarSize() + 'px')
|
||
.show()
|
||
}
|
||
// find width of frozen columns
|
||
let fwidth = 1
|
||
if (this.show.lineNumbers) fwidth += lineNumberWidth
|
||
if (this.show.selectColumn) fwidth += 26
|
||
// if (this.reorderRows) fwidth += 26;
|
||
if (this.show.expandColumn) fwidth += 26
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
if (this.columns[i].hidden) continue
|
||
if (this.columns[i].frozen) fwidth += parseInt(this.columns[i].sizeCalculated)
|
||
}
|
||
fcolumns.css('width', fwidth + 'px')
|
||
frecords.css('width', fwidth + 'px')
|
||
fsummary.css('width', fwidth + 'px')
|
||
scroll1.css('width', fwidth + 'px')
|
||
columns.css('left', fwidth + 'px')
|
||
records.css('left', fwidth + 'px')
|
||
summary.css('left', fwidth + 'px')
|
||
// resize columns
|
||
columns.find(':scope > table > tbody > tr:nth-child(1) td')
|
||
.add(fcolumns.find(':scope > table > tbody > tr:nth-child(1) td'))
|
||
.each(el => {
|
||
// line numbers
|
||
if (query(el).hasClass('w2ui-col-number')) {
|
||
query(el).css('width', lineNumberWidth + 'px')
|
||
}
|
||
// records
|
||
let ind = query(el).attr('col')
|
||
if (ind != null) {
|
||
if (ind == 'start') {
|
||
let width = 0
|
||
for (let i = 0; i < obj.last.colStart; i++) {
|
||
if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue
|
||
width += parseInt(obj.columns[i].sizeCalculated)
|
||
}
|
||
query(el).css('width', width + 'px')
|
||
}
|
||
if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated) // already has px
|
||
}
|
||
// last column
|
||
if (query(el).hasClass('w2ui-head-last')) {
|
||
if (obj.last.colEnd + 1 < obj.columns.length) {
|
||
let width = 0
|
||
for (let i = obj.last.colEnd + 1; i < obj.columns.length; i++) {
|
||
if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue
|
||
width += parseInt(obj.columns[i].sizeCalculated)
|
||
}
|
||
query(el).css('width', width + 'px')
|
||
} else {
|
||
query(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px')
|
||
}
|
||
}
|
||
})
|
||
// if there are column groups - hide first row (needed for sizing)
|
||
if (columns.find(':scope > table > tbody > tr').length == 3) {
|
||
columns.find(':scope > table > tbody > tr:nth-child(1) td')
|
||
.add(fcolumns.find(':scope > table > tbody > tr:nth-child(1) td'))
|
||
.html('').css({
|
||
'height' : '0',
|
||
'border' : '0',
|
||
'padding': '0',
|
||
'margin' : '0'
|
||
})
|
||
}
|
||
// resize records
|
||
records.find(':scope > table > tbody > tr:nth-child(1) td')
|
||
.add(frecords.find(':scope > table > tbody > tr:nth-child(1) td'))
|
||
.each(el => {
|
||
// line numbers
|
||
if (query(el).hasClass('w2ui-col-number')) {
|
||
query(el).css('width', lineNumberWidth + 'px')
|
||
}
|
||
// records
|
||
let ind = query(el).attr('col')
|
||
if (ind != null) {
|
||
if (ind == 'start') {
|
||
let width = 0
|
||
for (let i = 0; i < obj.last.colStart; i++) {
|
||
if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue
|
||
width += parseInt(obj.columns[i].sizeCalculated)
|
||
}
|
||
query(el).css('width', width + 'px')
|
||
}
|
||
if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated)
|
||
}
|
||
// last column
|
||
if (query(el).hasClass('w2ui-grid-data-last') && query(el).parents('.w2ui-grid-frecords').length === 0) { // not in frecords
|
||
if (obj.last.colEnd + 1 < obj.columns.length) {
|
||
let width = 0
|
||
for (let i = obj.last.colEnd + 1; i < obj.columns.length; i++) {
|
||
if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue
|
||
width += parseInt(obj.columns[i].sizeCalculated)
|
||
}
|
||
query(el).css('width', width + 'px')
|
||
} else {
|
||
query(el).css('width', (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px')
|
||
}
|
||
}
|
||
})
|
||
// resize summary
|
||
summary.find(':scope > table > tbody > tr:nth-child(1) td')
|
||
.add(fsummary.find(':scope > table > tbody > tr:nth-child(1) td'))
|
||
.each(el => {
|
||
// line numbers
|
||
if (query(el).hasClass('w2ui-col-number')) {
|
||
query(el).css('width', lineNumberWidth + 'px')
|
||
}
|
||
// records
|
||
let ind = query(el).attr('col')
|
||
if (ind != null) {
|
||
if (ind == 'start') {
|
||
let width = 0
|
||
for (let i = 0; i < obj.last.colStart; i++) {
|
||
if (!obj.columns[i] || obj.columns[i].frozen || obj.columns[i].hidden) continue
|
||
width += parseInt(obj.columns[i].sizeCalculated)
|
||
}
|
||
query(el).css('width', width + 'px')
|
||
}
|
||
if (obj.columns[ind]) query(el).css('width', obj.columns[ind].sizeCalculated)
|
||
}
|
||
// last column
|
||
if (query(el).hasClass('w2ui-grid-data-last') && query(el).parents('.w2ui-grid-frecords').length === 0) { // not in frecords
|
||
query(el).css('width', w2utils.scrollBarSize() + (width_diff > 0 && percent === 0 ? width_diff : 0) + 'px')
|
||
}
|
||
})
|
||
this.initResize()
|
||
this.refreshRanges()
|
||
// apply last scroll if any
|
||
if ((this.last.scrollTop || this.last.scrollLeft) && records.length > 0) {
|
||
columns.prop('scrollLeft', this.last.scrollLeft)
|
||
records.prop('scrollTop', this.last.scrollTop)
|
||
records.prop('scrollLeft', this.last.scrollLeft)
|
||
}
|
||
// Improved performance when scrolling through tables
|
||
columns.css('will-change', 'scroll-position')
|
||
}
|
||
getSearchesHTML() {
|
||
let html = `
|
||
<div class="search-title">
|
||
${w2utils.lang('Advanced Search')}
|
||
<span class="search-logic" style="${this.show.searchLogic ? '' : 'display: none'}">
|
||
<select id="grid_${this.name}_logic" class="w2ui-input">
|
||
<option value="AND" ${this.last.logic == 'AND' ? 'selected' : ''}>${w2utils.lang('All')}</option>
|
||
<option value="OR" ${this.last.logic == 'OR' ? 'selected' : ''}>${w2utils.lang('Any')}</option>
|
||
</select>
|
||
</span>
|
||
</div>
|
||
<table cellspacing="0"><tbody>
|
||
`
|
||
for (let i = 0; i < this.searches.length; i++) {
|
||
let s = this.searches[i]
|
||
s.type = String(s.type).toLowerCase()
|
||
if (s.hidden) continue
|
||
if (s.attr == null) s.attr = ''
|
||
if (s.text == null) s.text = ''
|
||
if (s.style == null) s.style = ''
|
||
if (s.type == null) s.type = 'text'
|
||
if (s.label == null && s.caption != null) {
|
||
console.log('NOTICE: grid search.caption property is deprecated, please use search.label. Search ->', s)
|
||
s.label = s.caption
|
||
}
|
||
let operator =`<select id="grid_${this.name}_operator_${i}" class="w2ui-input" data-change="initOperator|${i}">
|
||
${this.getOperators(s.type, s.operators)}
|
||
</select>`
|
||
html += `<tr>
|
||
<td class="caption">${(w2utils.lang(s.label) || '')}</td>
|
||
<td class="operator">${operator}</td>
|
||
<td class="value">`
|
||
let tmpStyle
|
||
switch (s.type) {
|
||
case 'text':
|
||
case 'alphanumeric':
|
||
case 'hex':
|
||
case 'color':
|
||
case 'list':
|
||
case 'combo':
|
||
case 'enum':
|
||
tmpStyle = 'width: 250px;'
|
||
if (['hex', 'color'].indexOf(s.type) != -1) tmpStyle = 'width: 90px;'
|
||
html += `<input rel="search" type="text" id="grid_${this.name}_field_${i}" name="${s.field}"
|
||
class="w2ui-input" style="${tmpStyle + s.style}" ${s.attr}>`
|
||
break
|
||
case 'int':
|
||
case 'float':
|
||
case 'money':
|
||
case 'currency':
|
||
case 'percent':
|
||
case 'date':
|
||
case 'time':
|
||
case 'datetime':
|
||
tmpStyle = 'width: 90px;'
|
||
if (s.type == 'datetime') tmpStyle = 'width: 140px;'
|
||
html += `<input id="grid_${this.name}_field_${i}" name="${s.field}" ${s.attr} rel="search" type="text"
|
||
class="w2ui-input" style="${tmpStyle + s.style}">
|
||
<span id="grid_${this.name}_range_${i}" style="display: none"> -  
|
||
<input rel="search" type="text" class="w2ui-input" style="${tmpStyle + s.style}" id="grid_${this.name}_field2_${i}" name="${s.field}" ${s.attr}>
|
||
</span>`
|
||
break
|
||
case 'select':
|
||
html += `<select rel="search" class="w2ui-input" style="${s.style}" id="grid_${this.name}_field_${i}"
|
||
name="${s.field}" ${s.attr}></select>`
|
||
break
|
||
}
|
||
html += s.text +
|
||
' </td>' +
|
||
'</tr>'
|
||
}
|
||
html += `<tr>
|
||
<td colspan="2" class="actions">
|
||
<button type="button" class="w2ui-btn close-btn" data-click="searchClose">${w2utils.lang('Close')}</button>
|
||
</td>
|
||
<td class="actions">
|
||
<button type="button" class="w2ui-btn" data-click="searchReset">${w2utils.lang('Reset')}</button>
|
||
<button type="button" class="w2ui-btn w2ui-btn-blue" data-click="search">${w2utils.lang('Search')}</button>
|
||
</td>
|
||
</tr></tbody></table>`
|
||
return html
|
||
}
|
||
getOperators(type, opers) {
|
||
let operators = this.operators[this.operatorsMap[type]] || []
|
||
if (opers != null && Array.isArray(opers)) {
|
||
operators = opers
|
||
}
|
||
let html = ''
|
||
operators.forEach(oper => {
|
||
let displayText = oper
|
||
let operValue = oper
|
||
if (Array.isArray(oper)) {
|
||
displayText = oper[1]
|
||
operValue = oper[0]
|
||
} else if (w2utils.isPlainObject(oper)) {
|
||
displayText = oper.text
|
||
operValue = oper.oper
|
||
}
|
||
if (displayText == null) displayText = oper
|
||
html += `<option name="11" value="${operValue}">${w2utils.lang(displayText)}</option>\n`
|
||
})
|
||
return html
|
||
}
|
||
initOperator(ind) {
|
||
let options
|
||
let search = this.searches[ind]
|
||
let sdata = this.getSearchData(search.field)
|
||
let overlay = query(`#w2overlay-${this.name}-search-overlay`)
|
||
let $rng = overlay.find(`#grid_${this.name}_range_${ind}`)
|
||
let $fld1 = overlay.find(`#grid_${this.name}_field_${ind}`)
|
||
let $fld2 = overlay.find(`#grid_${this.name}_field2_${ind}`)
|
||
let $oper = overlay.find(`#grid_${this.name}_operator_${ind}`)
|
||
let oper = $oper.val()
|
||
$fld1.show()
|
||
$rng.hide()
|
||
// init based on operator value
|
||
switch (oper) {
|
||
case 'between':
|
||
$rng.show()
|
||
break
|
||
case 'null':
|
||
case 'not null':
|
||
$fld1.hide()
|
||
$fld1.val(oper) // need to insert something for search to activate
|
||
$fld1.trigger('change')
|
||
break
|
||
}
|
||
// init based on search type
|
||
switch (search.type) {
|
||
case 'text':
|
||
case 'alphanumeric':
|
||
let fld = $fld1[0]._w2field
|
||
if (fld) { fld.reset() }
|
||
break
|
||
case 'int':
|
||
case 'float':
|
||
case 'hex':
|
||
case 'color':
|
||
case 'money':
|
||
case 'currency':
|
||
case 'percent':
|
||
case 'date':
|
||
case 'time':
|
||
case 'datetime':
|
||
if (!$fld1[0]._w2field) {
|
||
// init fields
|
||
new w2field(search.type, { el: $fld1[0], ...search.options })
|
||
new w2field(search.type, { el: $fld2[0], ...search.options })
|
||
setTimeout(() => { // convert to date if it is number
|
||
$fld1.trigger('keydown')
|
||
$fld2.trigger('keydown')
|
||
}, 1)
|
||
}
|
||
break
|
||
case 'list':
|
||
case 'combo':
|
||
case 'enum':
|
||
options = search.options
|
||
if (search.type == 'list') options.selected = {}
|
||
if (search.type == 'enum') options.selected = []
|
||
if (sdata) options.selected = sdata.value
|
||
if (!$fld1[0]._w2field) {
|
||
let fld = new w2field(search.type, { el: $fld1[0], ...options })
|
||
if (sdata && sdata.text != null) {
|
||
fld.set({ id: sdata.value, text: sdata.text })
|
||
}
|
||
}
|
||
break
|
||
case 'select':
|
||
// build options
|
||
options = '<option value="">--</option>'
|
||
for (let i = 0; i < search.options.items.length; i++) {
|
||
let si = search.options.items[i]
|
||
if (w2utils.isPlainObject(search.options.items[i])) {
|
||
let val = si.id
|
||
let txt = si.text
|
||
if (val == null && si.value != null) val = si.value
|
||
if (txt == null && si.text != null) txt = si.text
|
||
if (val == null) val = ''
|
||
options += '<option value="'+ val +'">'+ txt +'</option>'
|
||
} else {
|
||
options += '<option value="'+ si +'">'+ si +'</option>'
|
||
}
|
||
}
|
||
$fld1.html(options)
|
||
break
|
||
}
|
||
}
|
||
initSearches() {
|
||
let overlay = query(`#w2overlay-${this.name}-search-overlay`)
|
||
// init searches
|
||
for (let ind = 0; ind < this.searches.length; ind++) {
|
||
let search = this.searches[ind]
|
||
let sdata = this.getSearchData(search.field)
|
||
search.type = String(search.type).toLowerCase()
|
||
if (typeof search.options != 'object') search.options = {}
|
||
// operators
|
||
let operator = search.operator
|
||
let operators = [...this.operators[this.operatorsMap[search.type]]] || [] // need a copy
|
||
if (search.operators) operators = search.operators
|
||
// normalize
|
||
if (w2utils.isPlainObject(operator)) operator = operator.oper
|
||
operators.forEach((oper, ind) => {
|
||
if (w2utils.isPlainObject(oper)) operators[ind] = oper.oper
|
||
})
|
||
if (sdata && sdata.operator) {
|
||
operator = sdata.operator
|
||
}
|
||
// default operator
|
||
let def = this.defaultOperator[this.operatorsMap[search.type]]
|
||
if (operators.indexOf(operator) == -1) {
|
||
operator = def
|
||
}
|
||
overlay.find(`#grid_${this.name}_operator_${ind}`).val(operator)
|
||
this.initOperator(ind)
|
||
// populate field value
|
||
let $fld1 = overlay.find(`#grid_${this.name}_field_${ind}`)
|
||
let $fld2 = overlay.find(`#grid_${this.name}_field2_${ind}`)
|
||
if (sdata != null) {
|
||
if (!Array.isArray(sdata.value)) {
|
||
if (sdata.value != null) $fld1.val(sdata.value).trigger('change')
|
||
} else {
|
||
if (['in', 'not in'].includes(sdata.operator)) {
|
||
$fld1[0]._w2field.set(sdata.value)
|
||
} else {
|
||
$fld1.val(sdata.value[0]).trigger('change')
|
||
$fld2.val(sdata.value[1]).trigger('change')
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// add on change event
|
||
overlay.find('.w2ui-grid-search-advanced *[rel=search]')
|
||
.on('keypress', evnt => {
|
||
if (evnt.keyCode == 13) {
|
||
this.search()
|
||
w2tooltip.hide(this.name + '-search-overlay')
|
||
}
|
||
})
|
||
}
|
||
getColumnsHTML() {
|
||
let self = this
|
||
let html1 = ''
|
||
let html2 = ''
|
||
if (this.show.columnHeaders) {
|
||
if (this.columnGroups.length > 0) {
|
||
let tmp1 = getColumns(true)
|
||
let tmp2 = getGroups()
|
||
let tmp3 = getColumns(false)
|
||
html1 = tmp1[0] + tmp2[0] + tmp3[0]
|
||
html2 = tmp1[1] + tmp2[1] + tmp3[1]
|
||
} else {
|
||
let tmp = getColumns(true)
|
||
html1 = tmp[0]
|
||
html2 = tmp[1]
|
||
}
|
||
}
|
||
return [html1, html2]
|
||
function getGroups() {
|
||
let html1 = '<tr>'
|
||
let html2 = '<tr>'
|
||
let tmpf = ''
|
||
// add empty group at the end
|
||
let tmp = self.columnGroups.length - 1
|
||
if (self.columnGroups[tmp].text == null && self.columnGroups[tmp].caption != null) {
|
||
console.log('NOTICE: grid columnGroup.caption property is deprecated, please use columnGroup.text. Group -> ', self.columnGroups[tmp])
|
||
self.columnGroups[tmp].text = self.columnGroups[tmp].caption
|
||
}
|
||
if (self.columnGroups[self.columnGroups.length-1].text != '') self.columnGroups.push({ text: '' })
|
||
if (self.show.lineNumbers) {
|
||
html1 += '<td class="w2ui-head w2ui-col-number" col="line-number">' +
|
||
' <div> </div>' +
|
||
'</td>'
|
||
}
|
||
if (self.show.selectColumn) {
|
||
html1 += '<td class="w2ui-head w2ui-col-select" col="select">' +
|
||
' <div style="height: 25px"> </div>' +
|
||
'</td>'
|
||
}
|
||
if (self.show.expandColumn) {
|
||
html1 += '<td class="w2ui-head w2ui-col-expand" col="expand">' +
|
||
' <div style="height: 25px"> </div>' +
|
||
'</td>'
|
||
}
|
||
let ii = 0
|
||
html2 += `<td id="grid_${self.name}_column_start" class="w2ui-head" col="start" style="border-right: 0"></td>`
|
||
if (self.reorderRows) {
|
||
html2 += '<td class="w2ui-head w2ui-col-order" col="order">' +
|
||
' <div style="height: 25px"> </div>' +
|
||
'</td>'
|
||
}
|
||
for (let i = 0; i < self.columnGroups.length; i++) {
|
||
let colg = self.columnGroups[i]
|
||
let col = self.columns[ii] || {}
|
||
if (colg.colspan != null) colg.span = colg.colspan
|
||
if (colg.span == null || colg.span != parseInt(colg.span)) colg.span = 1
|
||
if (col.text == null && col.caption != null) {
|
||
console.log('NOTICE: grid column.caption property is deprecated, please use column.text. Column ->', col)
|
||
col.text = col.caption
|
||
}
|
||
let colspan = 0
|
||
for (let jj = ii; jj < ii + colg.span; jj++) {
|
||
if (self.columns[jj] && !self.columns[jj].hidden) {
|
||
colspan++
|
||
}
|
||
}
|
||
if (i == self.columnGroups.length-1) {
|
||
colspan = 100 // last column
|
||
}
|
||
if (colspan <= 0) {
|
||
// do nothing here, all columns in the group are hidden.
|
||
} else if (colg.main === true) {
|
||
let sortStyle = ''
|
||
for (let si = 0; si < self.sortData.length; si++) {
|
||
if (self.sortData[si].field == col.field) {
|
||
if ((self.sortData[si].direction || '').toLowerCase() === 'asc') sortStyle = 'w2ui-sort-up'
|
||
if ((self.sortData[si].direction || '').toLowerCase() === 'desc') sortStyle = 'w2ui-sort-down'
|
||
}
|
||
}
|
||
let resizer = ''
|
||
if (col.resizable !== false) {
|
||
resizer = `<div class="w2ui-resizer" name="${ii}"></div>`
|
||
}
|
||
let text = w2utils.lang(typeof col.text == 'function' ? col.text(col) : col.text)
|
||
tmpf = `<td id="grid_${self.name}_column_${ii}" class="w2ui-head ${sortStyle}" col="${ii}" `+
|
||
` rowspan="2" colspan="${colspan}">`+ resizer +
|
||
` <div class="w2ui-col-group w2ui-col-header ${sortStyle ? 'w2ui-col-sorted' : ''}">` +
|
||
` <div class="${sortStyle}"></div>` + (!text ? ' ' : text) +
|
||
' </div>'+
|
||
'</td>'
|
||
if (col && col.frozen) html1 += tmpf; else html2 += tmpf
|
||
} else {
|
||
let gText = w2utils.lang(typeof colg.text == 'function' ? colg.text(colg) : colg.text)
|
||
tmpf = `<td id="grid_${self.name}_column_${ii}" class="w2ui-head" col="${ii}" colspan="${colspan}">` +
|
||
` <div class="w2ui-col-group" style="${colg.style ?? ''}">${!gText ? ' ' : gText}</div>` +
|
||
'</td>'
|
||
if (col && col.frozen) html1 += tmpf; else html2 += tmpf
|
||
}
|
||
ii += colg.span
|
||
}
|
||
html1 += '<td></td></tr>' // need empty column for border-right
|
||
html2 += `<td id="grid_${self.name}_column_end" class="w2ui-head" col="end"></td></tr>`
|
||
return [html1, html2]
|
||
}
|
||
function getColumns(main) {
|
||
let html1 = '<tr>'
|
||
let html2 = '<tr>'
|
||
if (self.show.lineNumbers) {
|
||
html1 += '<td class="w2ui-head w2ui-col-number" col="line-number">' +
|
||
' <div>#</div>' +
|
||
'</td>'
|
||
}
|
||
if (self.show.selectColumn) {
|
||
html1 += '<td class="w2ui-head w2ui-col-select" col="select">' +
|
||
' <div>' +
|
||
` <input type="checkbox" id="grid_${self.name}_check_all" class="w2ui-select-all" tabindex="-1"` +
|
||
` style="${self.multiSelect == false ? 'display: none;' : ''}"` +
|
||
' >' +
|
||
' </div>' +
|
||
'</td>'
|
||
}
|
||
if (self.show.expandColumn) {
|
||
html1 += '<td class="w2ui-head w2ui-col-expand" col="expand">' +
|
||
' <div> </div>' +
|
||
'</td>'
|
||
}
|
||
let ii = 0
|
||
let id = 0
|
||
let colg
|
||
html2 += `<td id="grid_${self.name}_column_start" class="w2ui-head" col="start" style="border-right: 0"></td>`
|
||
if (self.reorderRows) {
|
||
html2 += '<td class="w2ui-head w2ui-col-order" col="order">'+
|
||
' <div> </div>'+
|
||
'</td>'
|
||
}
|
||
for (let i = 0; i < self.columns.length; i++) {
|
||
let col = self.columns[i]
|
||
if (col.text == null && col.caption != null) {
|
||
console.log('NOTICE: grid column.caption property is deprecated, please use column.text. Column -> ', col)
|
||
col.text = col.caption
|
||
}
|
||
if (col.size == null) col.size = '100%'
|
||
if (i == id) { // always true on first iteration
|
||
colg = self.columnGroups[ii++] || {}
|
||
id = id + colg.span
|
||
}
|
||
if ((i < self.last.colStart || i > self.last.colEnd) && !col.frozen)
|
||
continue
|
||
if (col.hidden)
|
||
continue
|
||
if (colg.main !== true || main) { // grouping of columns
|
||
let colCellHTML = self.getColumnCellHTML(i)
|
||
if (col && col.frozen) html1 += colCellHTML; else html2 += colCellHTML
|
||
}
|
||
}
|
||
html1 += '<td class="w2ui-head w2ui-head-last"><div> </div></td>'
|
||
html2 += '<td class="w2ui-head w2ui-head-last" col="end"><div> </div></td>'
|
||
html1 += '</tr>'
|
||
html2 += '</tr>'
|
||
return [html1, html2]
|
||
}
|
||
}
|
||
getColumnCellHTML(i) {
|
||
let col = this.columns[i]
|
||
if (col == null) return ''
|
||
// reorder style
|
||
let reorderCols = (this.reorderColumns && (!this.columnGroups || !this.columnGroups.length)) ? ' w2ui-col-reorderable ' : ''
|
||
// sort style
|
||
let sortStyle = ''
|
||
for (let si = 0; si < this.sortData.length; si++) {
|
||
if (this.sortData[si].field == col.field) {
|
||
if ((this.sortData[si].direction || '').toLowerCase() === 'asc') sortStyle = 'w2ui-sort-up'
|
||
if ((this.sortData[si].direction || '').toLowerCase() === 'desc') sortStyle = 'w2ui-sort-down'
|
||
}
|
||
}
|
||
// col selected
|
||
let tmp = this.last.selection.columns
|
||
let selected = false
|
||
for (let t in tmp) {
|
||
for (let si = 0; si < tmp[t].length; si++) {
|
||
if (tmp[t][si] == i) selected = true
|
||
}
|
||
}
|
||
let text = w2utils.lang(typeof col.text == 'function' ? col.text(col) : col.text)
|
||
let html = '<td id="grid_'+ this.name + '_column_' + i +'" col="'+ i +'" class="w2ui-head '+ sortStyle + reorderCols + '">' +
|
||
(col.resizable !== false ? '<div class="w2ui-resizer" name="'+ i +'"></div>' : '') +
|
||
' <div class="w2ui-col-header '+ (sortStyle ? 'w2ui-col-sorted' : '') +' '+ (selected ? 'w2ui-col-selected' : '') +'">'+
|
||
' <div class="'+ sortStyle +'"></div>'+
|
||
(!text ? ' ' : text) +
|
||
' </div>'+
|
||
'</td>'
|
||
return html
|
||
}
|
||
columnTooltipShow(ind, event) {
|
||
let $el = query(this.box).find('#grid_'+ this.name + '_column_'+ ind)
|
||
let item = this.columns[ind]
|
||
let pos = this.columnTooltip
|
||
w2tooltip.show({
|
||
name: this.name + '-column-tooltip',
|
||
anchor: $el.get(0),
|
||
html: item.tooltip,
|
||
position: pos,
|
||
})
|
||
}
|
||
columnTooltipHide(ind, event) {
|
||
w2tooltip.hide(this.name + '-column-tooltip')
|
||
}
|
||
getRecordsHTML() {
|
||
let buffered = this.records.length
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length
|
||
// larger number works better with chrome, smaller with FF.
|
||
if (buffered > this.vs_start) this.last.show_extra = this.vs_extra; else this.last.show_extra = this.vs_start
|
||
let records = query(this.box).find(`#grid_${this.name}_records`)
|
||
let limit = Math.floor((records.get(0)?.clientHeight || 0) / this.recordHeight) + this.last.show_extra + 1
|
||
if (!this.fixedBody || limit > buffered) limit = buffered
|
||
// always need first record for resizing purposes
|
||
let rec_html = this.getRecordHTML(-1, 0)
|
||
let html1 = '<table><tbody>' + rec_html[0]
|
||
let html2 = '<table><tbody>' + rec_html[1]
|
||
// first empty row with height
|
||
html1 += '<tr id="grid_'+ this.name + '_frec_top" line="top" style="height: '+ 0 +'px">'+
|
||
' <td colspan="2000"></td>'+
|
||
'</tr>'
|
||
html2 += '<tr id="grid_'+ this.name + '_rec_top" line="top" style="height: '+ 0 +'px">'+
|
||
' <td colspan="2000"></td>'+
|
||
'</tr>'
|
||
for (let i = 0; i < limit; i++) {
|
||
rec_html = this.getRecordHTML(i, i+1)
|
||
html1 += rec_html[0]
|
||
html2 += rec_html[1]
|
||
}
|
||
let h2 = (buffered - limit) * this.recordHeight
|
||
html1 += '<tr id="grid_' + this.name + '_frec_bottom" rec="bottom" line="bottom" style="height: ' + h2 + 'px; vertical-align: top">' +
|
||
' <td colspan="2000" style="border-right: 1px solid #D6D5D7;"></td>'+
|
||
'</tr>'+
|
||
'<tr id="grid_'+ this.name +'_frec_more" style="display: none; ">'+
|
||
' <td colspan="2000" class="w2ui-load-more"></td>'+
|
||
'</tr>'+
|
||
'</tbody></table>'
|
||
html2 += '<tr id="grid_' + this.name + '_rec_bottom" rec="bottom" line="bottom" style="height: ' + h2 + 'px; vertical-align: top">' +
|
||
' <td colspan="2000" style="border: 0"></td>'+
|
||
'</tr>'+
|
||
'<tr id="grid_'+ this.name +'_rec_more" style="display: none">'+
|
||
' <td colspan="2000" class="w2ui-load-more"></td>'+
|
||
'</tr>'+
|
||
'</tbody></table>'
|
||
this.last.range_start = 0
|
||
this.last.range_end = limit
|
||
return [html1, html2]
|
||
}
|
||
getSummaryHTML() {
|
||
if (this.summary.length === 0) return
|
||
let rec_html = this.getRecordHTML(-1, 0) // need this in summary too for colspan to work properly
|
||
let html1 = '<table><tbody>' + rec_html[0]
|
||
let html2 = '<table><tbody>' + rec_html[1]
|
||
for (let i = 0; i < this.summary.length; i++) {
|
||
rec_html = this.getRecordHTML(i, i+1, true)
|
||
html1 += rec_html[0]
|
||
html2 += rec_html[1]
|
||
}
|
||
html1 += '</tbody></table>'
|
||
html2 += '</tbody></table>'
|
||
return [html1, html2]
|
||
}
|
||
scroll(event) {
|
||
let obj = this
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
let records = query(this.box).find(`#grid_${this.name}_records`)
|
||
let frecords = query(this.box).find(`#grid_${this.name}_frecords`)
|
||
// sync scroll positions
|
||
if (event) {
|
||
let sTop = event.target.scrollTop
|
||
let sLeft = event.target.scrollLeft
|
||
this.last.scrollTop = sTop
|
||
this.last.scrollLeft = sLeft
|
||
let cols = query(this.box).find(`#grid_${this.name}_columns`)[0]
|
||
let summary = query(this.box).find(`#grid_${this.name}_summary`)[0]
|
||
if (cols) cols.scrollLeft = sLeft
|
||
if (summary) summary.scrollLeft = sLeft
|
||
if (frecords[0]) frecords[0].scrollTop = sTop
|
||
}
|
||
// hide bubble
|
||
if (this.last.bubbleEl) {
|
||
w2tooltip.hide(this.name + '-bubble')
|
||
this.last.bubbleEl = null
|
||
}
|
||
// column virtual scroll
|
||
let colStart = null
|
||
let colEnd = null
|
||
if (this.disableCVS || this.columnGroups.length > 0) {
|
||
// disable virtual scroll
|
||
colStart = 0
|
||
colEnd = this.columns.length - 1
|
||
} else {
|
||
let sWidth = records.prop('clientWidth')
|
||
let cLeft = 0
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
if (this.columns[i].frozen || this.columns[i].hidden) continue
|
||
let cSize = parseInt(this.columns[i].sizeCalculated ? this.columns[i].sizeCalculated : this.columns[i].size)
|
||
if (cLeft + cSize + 30 > this.last.scrollLeft && colStart == null) colStart = i
|
||
if (cLeft + cSize - 30 > this.last.scrollLeft + sWidth && colEnd == null) colEnd = i
|
||
cLeft += cSize
|
||
}
|
||
if (colEnd == null) colEnd = this.columns.length - 1
|
||
}
|
||
if (colStart != null) {
|
||
if (colStart < 0) colStart = 0
|
||
if (colEnd < 0) colEnd = 0
|
||
if (colStart == colEnd) {
|
||
if (colStart > 0) colStart--; else colEnd++ // show at least one column
|
||
}
|
||
// ---------
|
||
if (colStart != this.last.colStart || colEnd != this.last.colEnd) {
|
||
let $box = query(this.box)
|
||
let deltaStart = Math.abs(colStart - this.last.colStart)
|
||
let deltaEnd = Math.abs(colEnd - this.last.colEnd)
|
||
// add/remove columns for small jumps
|
||
if (deltaStart < 5 && deltaEnd < 5) {
|
||
let $cfirst = $box.find(`.w2ui-grid-columns #grid_${this.name}_column_start`)
|
||
let $clast = $box.find('.w2ui-grid-columns .w2ui-head-last')
|
||
let $rfirst = $box.find(`#grid_${this.name}_records .w2ui-grid-data-spacer`)
|
||
let $rlast = $box.find(`#grid_${this.name}_records .w2ui-grid-data-last`)
|
||
let $sfirst = $box.find(`#grid_${this.name}_summary .w2ui-grid-data-spacer`)
|
||
let $slast = $box.find(`#grid_${this.name}_summary .w2ui-grid-data-last`)
|
||
// remove on left
|
||
if (colStart > this.last.colStart) {
|
||
for (let i = this.last.colStart; i < colStart; i++) {
|
||
$box.find('#grid_'+ this.name +'_columns #grid_'+ this.name +'_column_'+ i).remove() // column
|
||
$box.find('#grid_'+ this.name +'_records td[col="'+ i +'"]').remove() // record
|
||
$box.find('#grid_'+ this.name +'_summary td[col="'+ i +'"]').remove() // summary
|
||
}
|
||
}
|
||
// remove on right
|
||
if (colEnd < this.last.colEnd) {
|
||
for (let i = this.last.colEnd; i > colEnd; i--) {
|
||
$box.find('#grid_'+ this.name +'_columns #grid_'+ this.name +'_column_'+ i).remove() // column
|
||
$box.find('#grid_'+ this.name +'_records td[col="'+ i +'"]').remove() // record
|
||
$box.find('#grid_'+ this.name +'_summary td[col="'+ i +'"]').remove() // summary
|
||
}
|
||
}
|
||
// add on left
|
||
if (colStart < this.last.colStart) {
|
||
for (let i = this.last.colStart - 1; i >= colStart; i--) {
|
||
if (this.columns[i] && (this.columns[i].frozen || this.columns[i].hidden)) continue
|
||
$cfirst.after(this.getColumnCellHTML(i)) // column
|
||
// record
|
||
$rfirst.each(el => {
|
||
let index = query(el).parent().attr('index')
|
||
let td = '<td class="w2ui-grid-data" col="'+ i +'" style="height: 0px"></td>' // width column
|
||
if (index != null) td = this.getCellHTML(parseInt(index), i, false)
|
||
query(el).after(td)
|
||
})
|
||
// summary
|
||
$sfirst.each(el => {
|
||
let index = query(el).parent().attr('index')
|
||
let td = '<td class="w2ui-grid-data" col="'+ i +'" style="height: 0px"></td>' // width column
|
||
if (index != null) td = this.getCellHTML(parseInt(index), i, true)
|
||
query(el).after(td)
|
||
})
|
||
}
|
||
}
|
||
// add on right
|
||
if (colEnd > this.last.colEnd) {
|
||
for (let i = this.last.colEnd + 1; i <= colEnd; i++) {
|
||
if (this.columns[i] && (this.columns[i].frozen || this.columns[i].hidden)) continue
|
||
$clast.before(this.getColumnCellHTML(i)) // column
|
||
// record
|
||
$rlast.each(el => {
|
||
let index = query(el).parent().attr('index')
|
||
let td = '<td class="w2ui-grid-data" col="'+ i +'" style="height: 0px"></td>' // width column
|
||
if (index != null) td = this.getCellHTML(parseInt(index), i, false)
|
||
query(el).before(td)
|
||
})
|
||
// summary
|
||
$slast.each(el => {
|
||
let index = query(el).parent().attr('index') || -1
|
||
let td = this.getCellHTML(parseInt(index), i, true)
|
||
query(el).before(td)
|
||
})
|
||
}
|
||
}
|
||
this.last.colStart = colStart
|
||
this.last.colEnd = colEnd
|
||
this.resizeRecords()
|
||
} else {
|
||
this.last.colStart = colStart
|
||
this.last.colEnd = colEnd
|
||
// dot not just call this.refresh();
|
||
let colHTML = this.getColumnsHTML()
|
||
let recHTML = this.getRecordsHTML()
|
||
let sumHTML = this.getSummaryHTML()
|
||
let $columns = $box.find(`#grid_${this.name}_columns`)
|
||
let $records = $box.find(`#grid_${this.name}_records`)
|
||
let $frecords = $box.find(`#grid_${this.name}_frecords`)
|
||
let $summary = $box.find(`#grid_${this.name}_summary`)
|
||
$columns.find('tbody').html(colHTML[1])
|
||
$frecords.html(recHTML[0])
|
||
$records.prepend(recHTML[1])
|
||
if (sumHTML != null) $summary.html(sumHTML[1])
|
||
// need timeout to clean up (otherwise scroll problem)
|
||
setTimeout(() => {
|
||
$records.find(':scope > table').filter(':not(table:first-child)').remove()
|
||
if ($summary[0]) $summary[0].scrollLeft = this.last.scrollLeft
|
||
}, 1)
|
||
this.resizeRecords()
|
||
}
|
||
}
|
||
}
|
||
// perform virtual scroll
|
||
let buffered = this.records.length
|
||
if (buffered > this.total && this.total !== -1) buffered = this.total
|
||
if (this.searchData.length != 0 && !url) buffered = this.last.searchIds.length
|
||
if (buffered === 0 || records.length === 0 || records.prop('clientHeight') === 0) return
|
||
if (buffered > this.vs_start) this.last.show_extra = this.vs_extra; else this.last.show_extra = this.vs_start
|
||
// update footer
|
||
let t1 = Math.round(records.prop('scrollTop') / this.recordHeight + 1)
|
||
let t2 = t1 + (Math.round(records.prop('clientHeight') / this.recordHeight) - 1)
|
||
if (t1 > buffered) t1 = buffered
|
||
if (t2 >= buffered - 1) t2 = buffered
|
||
query(this.box).find('#grid_'+ this.name + '_footer .w2ui-footer-right').html(
|
||
(this.show.statusRange
|
||
? w2utils.formatNumber(this.offset + t1) + '-' + w2utils.formatNumber(this.offset + t2) +
|
||
(this.total != -1 ? ' ' + w2utils.lang('of') + ' ' + w2utils.formatNumber(this.total) : '')
|
||
: '') +
|
||
(url && this.show.statusBuffered ? ' ('+ w2utils.lang('buffered') + ' '+ w2utils.formatNumber(buffered) +
|
||
(this.offset > 0 ? ', skip ' + w2utils.formatNumber(this.offset) : '') + ')' : '')
|
||
)
|
||
// only for local data source, else no extra records loaded
|
||
if (!url && (!this.fixedBody || (this.total != -1 && this.total <= this.vs_start))) return
|
||
// regular processing
|
||
let start = Math.floor(records.prop('scrollTop') / this.recordHeight) - this.last.show_extra
|
||
let end = start + Math.floor(records.prop('clientHeight') / this.recordHeight) + this.last.show_extra * 2 + 1
|
||
// let div = start - this.last.range_start;
|
||
if (start < 1) start = 1
|
||
if (end > this.total && this.total != -1) end = this.total
|
||
let tr1 = records.find('#grid_'+ this.name +'_rec_top')
|
||
let tr2 = records.find('#grid_'+ this.name +'_rec_bottom')
|
||
let tr1f = frecords.find('#grid_'+ this.name +'_frec_top')
|
||
let tr2f = frecords.find('#grid_'+ this.name +'_frec_bottom')
|
||
// if row is expanded
|
||
if (String(tr1.next().prop('id')).indexOf('_expanded_row') != -1) {
|
||
tr1.next().remove()
|
||
tr1f.next().remove()
|
||
}
|
||
if (this.total > end && String(tr2.prev().prop('id')).indexOf('_expanded_row') != -1) {
|
||
tr2.prev().remove()
|
||
tr2f.prev().remove()
|
||
}
|
||
let first = parseInt(tr1.next().attr('line'))
|
||
let last = parseInt(tr2.prev().attr('line'))
|
||
let tmp, tmp1, tmp2, rec_start, rec_html
|
||
if (first < start || first == 1 || this.last.pull_refresh) { // scroll down
|
||
if (end <= last + this.last.show_extra - 2 && end != this.total) return
|
||
this.last.pull_refresh = false
|
||
// remove from top
|
||
while (true) {
|
||
tmp1 = frecords.find('#grid_'+ this.name +'_frec_top').next()
|
||
tmp2 = records.find('#grid_'+ this.name +'_rec_top').next()
|
||
if (tmp2.attr('line') == 'bottom') break
|
||
if (parseInt(tmp2.attr('line')) < start) {
|
||
tmp1.remove()
|
||
tmp2.remove()
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
// add at bottom
|
||
tmp = records.find('#grid_'+ this.name +'_rec_bottom').prev()
|
||
rec_start = tmp.attr('line')
|
||
if (rec_start == 'top') rec_start = start
|
||
for (let i = parseInt(rec_start) + 1; i <= end; i++) {
|
||
if (!this.records[i-1]) continue
|
||
tmp2 = this.records[i-1].w2ui
|
||
if (tmp2 && !Array.isArray(tmp2.children)) {
|
||
tmp2.expanded = false
|
||
}
|
||
rec_html = this.getRecordHTML(i-1, i)
|
||
tr2.before(rec_html[1])
|
||
tr2f.before(rec_html[0])
|
||
}
|
||
markSearch()
|
||
setTimeout(() => { this.refreshRanges() }, 0)
|
||
} else { // scroll up
|
||
if (start >= first - this.last.show_extra + 2 && start > 1) return
|
||
// remove from bottom
|
||
while (true) {
|
||
tmp1 = frecords.find('#grid_'+ this.name +'_frec_bottom').prev()
|
||
tmp2 = records.find('#grid_'+ this.name +'_rec_bottom').prev()
|
||
if (tmp2.attr('line') == 'top') break
|
||
if (parseInt(tmp2.attr('line')) > end) {
|
||
tmp1.remove()
|
||
tmp2.remove()
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
// add at top
|
||
tmp = records.find('#grid_'+ this.name +'_rec_top').next()
|
||
rec_start = tmp.attr('line')
|
||
if (rec_start == 'bottom') rec_start = end
|
||
for (let i = parseInt(rec_start) - 1; i >= start; i--) {
|
||
if (!this.records[i-1]) continue
|
||
tmp2 = this.records[i-1].w2ui
|
||
if (tmp2 && !Array.isArray(tmp2.children)) {
|
||
tmp2.expanded = false
|
||
}
|
||
rec_html = this.getRecordHTML(i-1, i)
|
||
tr1.after(rec_html[1])
|
||
tr1f.after(rec_html[0])
|
||
}
|
||
markSearch()
|
||
setTimeout(() => { this.refreshRanges() }, 0)
|
||
}
|
||
// first/last row size
|
||
let h1 = (start - 1) * this.recordHeight
|
||
let h2 = (buffered - end) * this.recordHeight
|
||
if (h2 < 0) h2 = 0
|
||
tr1.css('height', h1 + 'px')
|
||
tr1f.css('height', h1 + 'px')
|
||
tr2.css('height', h2 + 'px')
|
||
tr2f.css('height', h2 + 'px')
|
||
this.last.range_start = start
|
||
this.last.range_end = end
|
||
// load more if needed
|
||
let s = Math.floor(records.prop('scrollTop') / this.recordHeight)
|
||
let e = s + Math.floor(records.prop('clientHeight') / this.recordHeight)
|
||
if (e + 10 > buffered && this.last.pull_more !== true && (buffered < this.total - this.offset || (this.total == -1 && this.last.fetch.hasMore))) {
|
||
if (this.autoLoad === true) {
|
||
this.last.pull_more = true
|
||
this.last.fetch.offset += this.limit
|
||
this.request('load')
|
||
}
|
||
// scroll function
|
||
let more = query(this.box).find('#grid_'+ this.name +'_rec_more, #grid_'+ this.name +'_frec_more')
|
||
more.show()
|
||
.eq(1) // only main table
|
||
.off('.load-more')
|
||
.on('click.load-more', function() {
|
||
// show spinner
|
||
query(this).find('td').html('<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>')
|
||
// load more
|
||
obj.last.pull_more = true
|
||
obj.last.fetch.offset += obj.limit
|
||
obj.request('load')
|
||
})
|
||
.find('td')
|
||
.html(obj.autoLoad
|
||
? '<div><div style="width: 20px; height: 20px;" class="w2ui-spinner"></div></div>'
|
||
: '<div style="padding-top: 15px">'+ w2utils.lang('Load ${count} more...', { count: obj.limit }) + '</div>'
|
||
)
|
||
}
|
||
function markSearch() {
|
||
// mark search
|
||
if (!obj.markSearch) return
|
||
clearTimeout(obj.last.marker_timer)
|
||
obj.last.marker_timer = setTimeout(() => {
|
||
// mark all search strings
|
||
let search = []
|
||
for (let s = 0; s < obj.searchData.length; s++) {
|
||
let sdata = obj.searchData[s]
|
||
let fld = obj.getSearch(sdata.field)
|
||
if (!fld || fld.hidden) continue
|
||
let ind = obj.getColumn(sdata.field, true)
|
||
search.push({ field: sdata.field, search: sdata.value, col: ind })
|
||
}
|
||
if (search.length > 0) {
|
||
search.forEach((item) => {
|
||
let el = query(obj.box).find('td[col="'+ item.col +'"]:not(.w2ui-head)')
|
||
w2utils.marker(el, item.search)
|
||
})
|
||
}
|
||
}, 50)
|
||
}
|
||
}
|
||
getRecordHTML(ind, lineNum, summary) {
|
||
let tmph = ''
|
||
let rec_html1 = ''
|
||
let rec_html2 = ''
|
||
let sel = this.last.selection
|
||
let record
|
||
// first record needs for resize purposes
|
||
if (ind == -1) {
|
||
rec_html1 += '<tr line="0">'
|
||
rec_html2 += '<tr line="0">'
|
||
if (this.show.lineNumbers) rec_html1 += '<td class="w2ui-col-number" style="height: 0px"></td>'
|
||
if (this.show.selectColumn) rec_html1 += '<td class="w2ui-col-select" style="height: 0px"></td>'
|
||
if (this.show.expandColumn) rec_html1 += '<td class="w2ui-col-expand" style="height: 0px"></td>'
|
||
rec_html2 += '<td class="w2ui-grid-data w2ui-grid-data-spacer" col="start" style="height: 0px; width: 0px"></td>'
|
||
if (this.reorderRows) rec_html2 += '<td class="w2ui-col-order" style="height: 0px"></td>'
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
tmph = '<td class="w2ui-grid-data" col="'+ i +'" style="height: 0px;"></td>'
|
||
if (col.frozen && !col.hidden) {
|
||
rec_html1 += tmph
|
||
} else {
|
||
if (col.hidden || i < this.last.colStart || i > this.last.colEnd) continue
|
||
rec_html2 += tmph
|
||
}
|
||
}
|
||
rec_html1 += '<td class="w2ui-grid-data-last" style="height: 0px"></td>'
|
||
rec_html2 += '<td class="w2ui-grid-data-last" col="end" style="height: 0px"></td>'
|
||
rec_html1 += '</tr>'
|
||
rec_html2 += '</tr>'
|
||
return [rec_html1, rec_html2]
|
||
}
|
||
// regular record
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (summary !== true) {
|
||
if (this.searchData.length > 0 && !url) {
|
||
if (ind >= this.last.searchIds.length) return ''
|
||
ind = this.last.searchIds[ind]
|
||
record = this.records[ind]
|
||
} else {
|
||
if (ind >= this.records.length) return ''
|
||
record = this.records[ind]
|
||
}
|
||
} else {
|
||
if (ind >= this.summary.length) return ''
|
||
record = this.summary[ind]
|
||
}
|
||
if (!record) return ''
|
||
if (record.recid == null && this.recid != null) {
|
||
let rid = this.parseField(record, this.recid)
|
||
if (rid != null) record.recid = rid
|
||
}
|
||
let isRowSelected = false
|
||
if (sel.indexes.indexOf(ind) != -1) isRowSelected = true
|
||
let rec_style = (record.w2ui ? record.w2ui.style : '')
|
||
if (rec_style == null || typeof rec_style != 'string') rec_style = ''
|
||
let rec_class = (record.w2ui ? record.w2ui.class : '')
|
||
if (rec_class == null || typeof rec_class != 'string') rec_class = ''
|
||
// render TR
|
||
rec_html1 += '<tr id="grid_'+ this.name +'_frec_'+ record.recid +'" recid="'+ record.recid +'" line="'+ lineNum +'" index="'+ ind +'" '+
|
||
' class="'+ (lineNum % 2 === 0 ? 'w2ui-even' : 'w2ui-odd') + ' w2ui-record ' + rec_class +
|
||
(isRowSelected && this.selectType == 'row' ? ' w2ui-selected' : '') +
|
||
(record.w2ui && record.w2ui.editable === false ? ' w2ui-no-edit' : '') +
|
||
(record.w2ui && record.w2ui.expanded === true ? ' w2ui-expanded' : '') + '" ' +
|
||
' style="height: '+ this.recordHeight +'px; '+ (!isRowSelected && rec_style != '' ? rec_style : rec_style.replace('background-color', 'none')) +'" '+
|
||
(rec_style != '' ? 'custom_style="'+ rec_style +'"' : '') +
|
||
'>'
|
||
rec_html2 += '<tr id="grid_'+ this.name +'_rec_'+ record.recid +'" recid="'+ record.recid +'" line="'+ lineNum +'" index="'+ ind +'" '+
|
||
' class="'+ (lineNum % 2 === 0 ? 'w2ui-even' : 'w2ui-odd') + ' w2ui-record ' + rec_class +
|
||
(isRowSelected && this.selectType == 'row' ? ' w2ui-selected' : '') +
|
||
(record.w2ui && record.w2ui.editable === false ? ' w2ui-no-edit' : '') +
|
||
(record.w2ui && record.w2ui.expanded === true ? ' w2ui-expanded' : '') + '" ' +
|
||
' style="height: '+ this.recordHeight +'px; '+ (!isRowSelected && rec_style != '' ? rec_style : rec_style.replace('background-color', 'none')) +'" '+
|
||
(rec_style != '' ? 'custom_style="'+ rec_style +'"' : '') +
|
||
'>'
|
||
if (this.show.lineNumbers) {
|
||
rec_html1 += '<td id="grid_'+ this.name +'_cell_'+ ind +'_number' + (summary ? '_s' : '') + '" '+
|
||
' class="w2ui-col-number '+ (isRowSelected ? ' w2ui-row-selected' : '') +'"'+
|
||
(this.reorderRows ? ' style="cursor: move"' : '') + '>'+
|
||
(summary !== true ? this.getLineHTML(lineNum, record) : '') +
|
||
'</td>'
|
||
}
|
||
if (this.show.selectColumn) {
|
||
rec_html1 +=
|
||
'<td id="grid_'+ this.name +'_cell_'+ ind +'_select' + (summary ? '_s' : '') + '" class="w2ui-grid-data w2ui-col-select">'+
|
||
(summary !== true && !(record.w2ui && record.w2ui.hideCheckBox === true) ?
|
||
' <div>'+
|
||
' <input class="w2ui-grid-select-check" type="checkbox" tabindex="-1" '+
|
||
(isRowSelected ? 'checked="checked"' : '') + ' style="pointer-events: none"/>'+
|
||
' </div>'
|
||
:
|
||
'' ) +
|
||
'</td>'
|
||
}
|
||
if (this.show.expandColumn) {
|
||
let tmp_img = ''
|
||
if (record.w2ui?.expanded === true) tmp_img = '-'; else tmp_img = '+'
|
||
if ((record.w2ui?.expanded == 'none' || !Array.isArray(record.w2ui.children) || !record.w2ui.children.length)) tmp_img = '+'
|
||
if (record.w2ui?.expanded == 'spinner') tmp_img = '<div class="w2ui-spinner" style="width: 16px; margin: -2px 2px;"></div>'
|
||
rec_html1 +=
|
||
'<td id="grid_'+ this.name +'_cell_'+ ind +'_expand' + (summary ? '_s' : '') + '" class="w2ui-grid-data w2ui-col-expand">'+
|
||
(summary !== true ? `<div>${tmp_img}</div>` : '' ) +
|
||
'</td>'
|
||
}
|
||
// insert empty first column
|
||
rec_html2 += '<td class="w2ui-grid-data-spacer" col="start" style="border-right: 0"></td>'
|
||
if (this.reorderRows) {
|
||
rec_html2 +=
|
||
'<td id="grid_'+ this.name +'_cell_'+ ind +'_order' + (summary ? '_s' : '') + '" class="w2ui-grid-data w2ui-col-order" col="order">'+
|
||
(summary !== true ? '<div title="Drag to reorder"> </div>' : '' ) +
|
||
'</td>'
|
||
}
|
||
let col_ind = 0
|
||
let col_skip = 0
|
||
while (true) {
|
||
let col_span = 1
|
||
let col = this.columns[col_ind]
|
||
if (col == null) break
|
||
if (col.hidden) {
|
||
col_ind++
|
||
if (col_skip > 0) col_skip--
|
||
continue
|
||
}
|
||
if (col_skip > 0) {
|
||
col_ind++
|
||
if (this.columns[col_ind] == null) break
|
||
record.w2ui.colspan[this.columns[col_ind-1].field] = 0 // need it for other methods
|
||
col_skip--
|
||
continue
|
||
} else if (record.w2ui) {
|
||
let tmp1 = record.w2ui.colspan
|
||
let tmp2 = this.columns[col_ind].field
|
||
if (tmp1 && tmp1[tmp2] === 0) {
|
||
delete tmp1[tmp2] // if no longer colspan then remove 0
|
||
}
|
||
}
|
||
// column virtual scroll
|
||
if ((col_ind < this.last.colStart || col_ind > this.last.colEnd) && !col.frozen) {
|
||
col_ind++
|
||
continue
|
||
}
|
||
if (record.w2ui) {
|
||
if (typeof record.w2ui.colspan == 'object') {
|
||
let span = parseInt(record.w2ui.colspan[col.field]) || null
|
||
if (span > 1) {
|
||
// if there are hidden columns, then no colspan on them
|
||
let hcnt = 0
|
||
for (let i = col_ind; i < col_ind + span; i++) {
|
||
if (i >= this.columns.length) break
|
||
if (this.columns[i].hidden) hcnt++
|
||
}
|
||
col_span = span - hcnt
|
||
col_skip = span - 1
|
||
}
|
||
}
|
||
}
|
||
let rec_cell = this.getCellHTML(ind, col_ind, summary, col_span)
|
||
if (col.frozen) rec_html1 += rec_cell; else rec_html2 += rec_cell
|
||
col_ind++
|
||
}
|
||
rec_html1 += '<td class="w2ui-grid-data-last"></td>'
|
||
rec_html2 += '<td class="w2ui-grid-data-last" col="end"></td>'
|
||
rec_html1 += '</tr>'
|
||
rec_html2 += '</tr>'
|
||
return [rec_html1, rec_html2]
|
||
}
|
||
getLineHTML(lineNum) {
|
||
return '<div>' + lineNum + '</div>'
|
||
}
|
||
getCellHTML(ind, col_ind, summary, col_span) {
|
||
let obj = this
|
||
let col = this.columns[col_ind]
|
||
if (col == null) return ''
|
||
let record = (summary !== true ? this.records[ind] : this.summary[ind])
|
||
// value, attr, style, className, divAttr
|
||
let { value, style, className, attr, divAttr } = this.getCellValue(ind, col_ind, summary, true)
|
||
let edit = (ind !== -1 ? this.getCellEditable(ind, col_ind) : '')
|
||
let divStyle = 'max-height: '+ parseInt(this.recordHeight) +'px;' + (col.clipboardCopy ? 'margin-right: 20px' : '')
|
||
let isChanged = !summary && record?.w2ui?.changes && record.w2ui.changes[col.field] != null
|
||
let sel = this.last.selection
|
||
let isRowSelected = false
|
||
let infoBubble = ''
|
||
if (sel.indexes.indexOf(ind) != -1) isRowSelected = true
|
||
if (col_span == null) {
|
||
if (record?.w2ui?.colspan && record.w2ui.colspan[col.field]) {
|
||
col_span = record.w2ui.colspan[col.field]
|
||
} else {
|
||
col_span = 1
|
||
}
|
||
}
|
||
// expand icon
|
||
if (col_ind === 0 && Array.isArray(record?.w2ui?.children)) {
|
||
let level = 0
|
||
let subrec = this.get(record.w2ui.parent_recid, true)
|
||
while (true) {
|
||
if (subrec != null) {
|
||
level++
|
||
let tmp = this.records[subrec].w2ui
|
||
if (tmp != null && tmp.parent_recid != null) {
|
||
subrec = this.get(tmp.parent_recid, true)
|
||
} else {
|
||
break
|
||
}
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
if (record.w2ui.parent_recid) {
|
||
for (let i = 0; i < level; i++) {
|
||
infoBubble += '<span class="w2ui-show-children w2ui-icon-empty"></span>'
|
||
}
|
||
}
|
||
let className = record.w2ui.children.length > 0
|
||
? (record.w2ui.expanded ? 'w2ui-icon-collapse' : 'w2ui-icon-expand')
|
||
: 'w2ui-icon-empty'
|
||
infoBubble += `<span class="w2ui-show-children ${className}"></span>`
|
||
}
|
||
// info bubble
|
||
if (col.info === true) col.info = {}
|
||
if (col.info != null) {
|
||
let infoIcon = 'w2ui-icon-info'
|
||
if (typeof col.info.icon == 'function') {
|
||
infoIcon = col.info.icon(record, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
} else if (typeof col.info.icon == 'object') {
|
||
infoIcon = col.info.icon[this.parseField(record, col.field)] || ''
|
||
} else if (typeof col.info.icon == 'string') {
|
||
infoIcon = col.info.icon
|
||
}
|
||
let infoStyle = col.info.style || ''
|
||
if (typeof col.info.style == 'function') {
|
||
infoStyle = col.info.style(record, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
} else if (typeof col.info.style == 'object') {
|
||
infoStyle = col.info.style[this.parseField(record, col.field)] || ''
|
||
} else if (typeof col.info.style == 'string') {
|
||
infoStyle = col.info.style
|
||
}
|
||
infoBubble += `<span class="w2ui-info ${infoIcon}" style="${infoStyle}"></span>`
|
||
}
|
||
let data = value
|
||
// if editable checkbox
|
||
if (edit && ['checkbox', 'check'].indexOf(edit.type) != -1) {
|
||
let changeInd = summary ? -(ind + 1) : ind
|
||
divStyle += 'text-align: center;'
|
||
data = `<input tabindex="-1" type="checkbox" class="w2ui-editable-checkbox"
|
||
data-changeInd="${changeInd}" data-colInd="${col_ind}" ${data ? 'checked="checked"' : ''}>`
|
||
infoBubble = ''
|
||
}
|
||
data = `<div style="${divStyle}" ${getTitle(data)} ${divAttr}>${infoBubble}${String(data)}</div>`
|
||
if (data == null) data = ''
|
||
// --> cell TD
|
||
if (typeof col.render == 'string') {
|
||
let tmp = col.render.toLowerCase().split(':')
|
||
if (['number', 'int', 'float', 'money', 'currency', 'percent', 'size'].indexOf(tmp[0]) != -1) {
|
||
style += 'text-align: right;'
|
||
}
|
||
}
|
||
if (record?.w2ui) {
|
||
if (typeof record.w2ui.style == 'object') {
|
||
if (typeof record.w2ui.style[col_ind] == 'string') style += record.w2ui.style[col_ind] + ';'
|
||
if (typeof record.w2ui.style[col.field] == 'string') style += record.w2ui.style[col.field] + ';'
|
||
}
|
||
if (typeof record.w2ui.class == 'object') {
|
||
if (typeof record.w2ui.class[col_ind] == 'string') className += record.w2ui.class[col_ind] + ' '
|
||
if (typeof record.w2ui.class[col.field] == 'string') className += record.w2ui.class[col.field] + ' '
|
||
}
|
||
}
|
||
let isCellSelected = false
|
||
if (isRowSelected && sel.columns[ind]?.includes(col_ind)) isCellSelected = true
|
||
// clipboardCopy
|
||
let clipboardIcon
|
||
if (col.clipboardCopy){
|
||
clipboardIcon = '<span class="w2ui-clipboard-copy w2ui-icon-paste"></span>'
|
||
}
|
||
// data
|
||
data = '<td class="w2ui-grid-data'+ (isCellSelected ? ' w2ui-selected' : '') + ' ' + className +
|
||
(isChanged ? ' w2ui-changed' : '') + '" '+
|
||
' id="grid_'+ this.name +'_data_'+ ind +'_'+ col_ind +'" col="'+ col_ind +'" '+
|
||
' style="'+ style + (col.style != null ? col.style : '') +'" '+
|
||
(col.attr != null ? col.attr : '') + attr +
|
||
(col_span > 1 ? 'colspan="'+ col_span + '"' : '') +
|
||
'>' + data + (clipboardIcon && w2utils.stripTags(data) ? clipboardIcon : '') +'</td>'
|
||
// summary top row
|
||
if (ind === -1 && summary === true) {
|
||
data = '<td class="w2ui-grid-data" col="'+ col_ind +'" style="height: 0px; '+ style + '" '+
|
||
(col_span > 1 ? 'colspan="'+ col_span + '"' : '') +
|
||
'></td>'
|
||
}
|
||
return data
|
||
function getTitle(cellData){
|
||
let title
|
||
if (obj.show.recordTitles) {
|
||
if (col.title != null) {
|
||
if (typeof col.title == 'function') {
|
||
title = col.title.call(obj, record, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
}
|
||
if (typeof col.title == 'string') title = col.title
|
||
} else {
|
||
title = w2utils.stripTags(String(cellData).replace(/"/g, '\'\''))
|
||
}
|
||
}
|
||
return (title != null) ? 'title="' + String(title) + '"' : ''
|
||
}
|
||
}
|
||
clipboardCopy(ind, col_ind, summary) {
|
||
let rec = summary ? this.summary[ind] : this.records[ind]
|
||
let col = this.columns[col_ind]
|
||
let txt = (col ? this.parseField(rec, col.field) : '')
|
||
if (typeof col.clipboardCopy == 'function') {
|
||
txt = col.clipboardCopy(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
}
|
||
query(this.box).find('#grid_' + this.name + '_focus').text(txt).get(0).select()
|
||
document.execCommand('copy')
|
||
}
|
||
showBubble(ind, col_ind, summary) {
|
||
let info = this.columns[col_ind].info
|
||
if (!info) return
|
||
let html = ''
|
||
let rec = this.records[ind]
|
||
let el = query(this.box).find(`${summary ? '.w2ui-grid-summary' : ''} #grid_${this.name}_data_${ind}_${col_ind} .w2ui-info`)
|
||
if (this.last.bubbleEl) {
|
||
w2tooltip.hide(this.name + '-bubble')
|
||
}
|
||
this.last.bubbleEl = el
|
||
// if no fields defined - show all
|
||
if (info.fields == null) {
|
||
info.fields = []
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
info.fields.push(col.field + (typeof col.render == 'string' ? ':' + col.render : ''))
|
||
}
|
||
}
|
||
let fields = info.fields
|
||
if (typeof fields == 'function') {
|
||
fields = fields(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary }) // custom renderer
|
||
}
|
||
// generate html
|
||
if (typeof info.render == 'function') {
|
||
html = info.render(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
} else if (Array.isArray(fields)) {
|
||
// display mentioned fields
|
||
html = '<table cellpadding="0" cellspacing="0">'
|
||
for (let i = 0; i < fields.length; i++) {
|
||
let tmp = String(fields[i]).split(':')
|
||
if (tmp[0] == '' || tmp[0] == '-' || tmp[0] == '--' || tmp[0] == '---') {
|
||
html += '<tr><td colspan=2><div style="border-top: '+ (tmp[0] == '' ? '0' : '1') +'px solid #C1BEBE; margin: 6px 0px;"></div></td></tr>'
|
||
continue
|
||
}
|
||
let col = this.getColumn(tmp[0])
|
||
if (col == null) col = { field: tmp[0], caption: tmp[0] } // if not found in columns
|
||
let val = (col ? this.parseField(rec, col.field) : '')
|
||
if (tmp.length > 1) {
|
||
if (w2utils.formatters[tmp[1]]) {
|
||
val = w2utils.formatters[tmp[1]](val, tmp[2] || null, rec)
|
||
} else {
|
||
console.log('ERROR: w2utils.formatters["'+ tmp[1] + '"] does not exists.')
|
||
}
|
||
}
|
||
if (info.showEmpty !== true && (val == null || val == '')) continue
|
||
if (info.maxLength != null && typeof val == 'string' && val.length > info.maxLength) val = val.substr(0, info.maxLength) + '...'
|
||
html += '<tr><td>' + col.text + '</td><td>' + ((val === 0 ? '0' : val) || '') + '</td></tr>'
|
||
}
|
||
html += '</table>'
|
||
} else if (w2utils.isPlainObject(fields)) {
|
||
// display some fields
|
||
html = '<table cellpadding="0" cellspacing="0">'
|
||
for (let caption in fields) {
|
||
let fld = fields[caption]
|
||
if (fld == '' || fld == '-' || fld == '--' || fld == '---') {
|
||
html += '<tr><td colspan=2><div style="border-top: '+ (fld == '' ? '0' : '1') +'px solid #C1BEBE; margin: 6px 0px;"></div></td></tr>'
|
||
continue
|
||
}
|
||
let tmp = String(fld).split(':')
|
||
let col = this.getColumn(tmp[0])
|
||
if (col == null) col = { field: tmp[0], caption: tmp[0] } // if not found in columns
|
||
let val = (col ? this.parseField(rec, col.field) : '')
|
||
if (tmp.length > 1) {
|
||
if (w2utils.formatters[tmp[1]]) {
|
||
val = w2utils.formatters[tmp[1]](val, tmp[2] || null, rec)
|
||
} else {
|
||
console.log('ERROR: w2utils.formatters["'+ tmp[1] + '"] does not exists.')
|
||
}
|
||
}
|
||
if (typeof fld == 'function') {
|
||
val = fld(rec, { self: this, index: ind, colIndex: col_ind, summary: !!summary })
|
||
}
|
||
if (info.showEmpty !== true && (val == null || val == '')) continue
|
||
if (info.maxLength != null && typeof val == 'string' && val.length > info.maxLength) val = val.substr(0, info.maxLength) + '...'
|
||
html += '<tr><td>' + caption + '</td><td>' + ((val === 0 ? '0' : val) || '') + '</td></tr>'
|
||
}
|
||
html += '</table>'
|
||
}
|
||
return w2tooltip.show(w2utils.extend({
|
||
name: this.name + '-bubble',
|
||
html,
|
||
anchor: el.get(0),
|
||
position: 'top|bottom',
|
||
class: 'w2ui-info-bubble',
|
||
style: '',
|
||
hideOn: ['doc-click']
|
||
}, info.options ?? {}))
|
||
.hide(() => [
|
||
this.last.bubbleEl = null
|
||
])
|
||
}
|
||
// return null or the editable object if the given cell is editable
|
||
getCellEditable(ind, col_ind) {
|
||
let col = this.columns[col_ind]
|
||
let rec = this.records[ind]
|
||
if (!rec || !col) return null
|
||
let edit = (rec.w2ui ? rec.w2ui.editable : null)
|
||
if (edit === false) return null
|
||
if (edit == null || edit === true) {
|
||
edit = (Object.keys(col.editable ?? {}).length > 0 ? col.editable : null)
|
||
if (typeof edit === 'function') {
|
||
let value = this.getCellValue(ind, col_ind, false)
|
||
// same arguments as col.render()
|
||
edit = edit.call(this, rec, { self: this, value, index: ind, colIndex: col_ind })
|
||
}
|
||
}
|
||
return edit
|
||
}
|
||
getCellValue(ind, col_ind, summary, extra) {
|
||
let col = this.columns[col_ind]
|
||
let record = (summary !== true ? this.records[ind] : this.summary[ind])
|
||
let value = this.parseField(record, col.field)
|
||
let className = '', style = '', attr = '', divAttr = ''
|
||
// if change by inline editing
|
||
if (record?.w2ui?.changes?.[col.field] != null) {
|
||
value = record.w2ui.changes[col.field]
|
||
}
|
||
// if there is a cell renderer
|
||
if (col.render != null && ind !== -1) {
|
||
if (typeof col.render == 'function' && record != null) {
|
||
let html
|
||
try {
|
||
html = col.render(record, { self: this, value, index: ind, colIndex: col_ind, summary: !!summary })
|
||
} catch (e) {
|
||
throw new Error(`Render function for column "${col.field}" in grid "${this.name}": -- ` + e.message)
|
||
}
|
||
if (html != null && typeof html == 'object' && typeof html != 'function') {
|
||
if (html.id != null && html.text != null) {
|
||
// normalized menu kind of return
|
||
value = html.text
|
||
} else if (typeof html.html == 'string') {
|
||
value = (html.html || '').trim()
|
||
} else {
|
||
value = ''
|
||
console.log('ERROR: render function should return a primitive or an object of the following structure.',
|
||
{ html: '', attr: '', style: '', class: '', divAttr: '' })
|
||
}
|
||
attr = html.attr ?? ''
|
||
style = html.style ?? ''
|
||
className = html.class ?? ''
|
||
divAttr = html.divAttr ?? ''
|
||
} else {
|
||
value = String(html || '').trim()
|
||
}
|
||
}
|
||
// if it is an object
|
||
if (typeof col.render == 'object') {
|
||
let tmp = col.render[value]
|
||
if (tmp != null && tmp !== '') {
|
||
value = tmp
|
||
}
|
||
}
|
||
// formatters
|
||
if (typeof col.render == 'string') {
|
||
let strInd = col.render.toLowerCase().indexOf(':')
|
||
let tmp = []
|
||
if (strInd == -1) {
|
||
tmp[0] = col.render.toLowerCase()
|
||
tmp[1] = ''
|
||
} else {
|
||
tmp[0] = col.render.toLowerCase().substr(0, strInd)
|
||
tmp[1] = col.render.toLowerCase().substr(strInd + 1)
|
||
}
|
||
// formatters
|
||
let func = w2utils.formatters[tmp[0]]
|
||
if (col.options && col.options.autoFormat === false) {
|
||
func = null
|
||
}
|
||
value = (typeof func == 'function' ? func(value, tmp[1], record) : '')
|
||
}
|
||
}
|
||
if (value == null) value = ''
|
||
return !extra ? value : { value, attr, style, className, divAttr }
|
||
}
|
||
getFooterHTML() {
|
||
return '<div>'+
|
||
' <div class="w2ui-footer-left"></div>'+
|
||
' <div class="w2ui-footer-right"></div>'+
|
||
' <div class="w2ui-footer-center"></div>'+
|
||
'</div>'
|
||
}
|
||
status(msg) {
|
||
if (msg != null) {
|
||
query(this.box).find(`#grid_${this.name}_footer`).find('.w2ui-footer-left').html(msg)
|
||
} else {
|
||
// show number of selected
|
||
let msgLeft = ''
|
||
let sel = this.getSelection()
|
||
if (sel.length > 0) {
|
||
if (this.show.statusSelection && sel.length > 1) {
|
||
msgLeft = String(sel.length).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + w2utils.settings.groupSymbol) + ' ' + w2utils.lang('selected')
|
||
}
|
||
if (this.show.statusRecordID && sel.length == 1) {
|
||
let tmp = sel[0]
|
||
if (typeof tmp == 'object') tmp = tmp.recid + ', '+ w2utils.lang('Column') +': '+ tmp.column
|
||
msgLeft = w2utils.lang('Record ID') + ': '+ tmp + ' '
|
||
}
|
||
}
|
||
query(this.box).find('#grid_'+ this.name +'_footer .w2ui-footer-left').html(msgLeft)
|
||
}
|
||
}
|
||
lock(msg, showSpinner) {
|
||
let args = Array.from(arguments)
|
||
args.unshift(this.box)
|
||
setTimeout(() => {
|
||
// hide empty msg if any
|
||
query(this.box).find('#grid_'+ this.name +'_empty_msg').remove()
|
||
w2utils.lock(...args)
|
||
}, 10)
|
||
}
|
||
unlock(speed) {
|
||
setTimeout(() => {
|
||
// do not unlock if there is a message
|
||
if (query(this.box).find('.w2ui-message').hasClass('w2ui-closing')) return
|
||
w2utils.unlock(this.box, speed)
|
||
}, 25) // needed timer so if server fast, it will not flash
|
||
}
|
||
stateSave(returnOnly) {
|
||
let state = {
|
||
columns: [],
|
||
show: w2utils.clone(this.show),
|
||
last: {
|
||
search: this.last.search,
|
||
multi : this.last.multi,
|
||
logic : this.last.logic,
|
||
label : this.last.label,
|
||
field : this.last.field,
|
||
scrollTop : this.last.scrollTop,
|
||
scrollLeft: this.last.scrollLeft
|
||
},
|
||
sortData : [],
|
||
searchData: []
|
||
}
|
||
let prop_val
|
||
for (let i = 0; i < this.columns.length; i++) {
|
||
let col = this.columns[i]
|
||
let col_save_obj = {}
|
||
// iterate properties to save
|
||
Object.keys(this.stateColProps).forEach((prop, idx) => {
|
||
if (this.stateColProps[prop]){
|
||
// check if the property is defined on the column
|
||
if (col[prop] !== undefined){
|
||
prop_val = col[prop]
|
||
} else {
|
||
// use fallback or null
|
||
prop_val = this.colTemplate[prop] || null
|
||
}
|
||
col_save_obj[prop] = prop_val
|
||
}
|
||
})
|
||
state.columns.push(col_save_obj)
|
||
}
|
||
for (let i = 0; i < this.sortData.length; i++) state.sortData.push(w2utils.clone(this.sortData[i]))
|
||
for (let i = 0; i < this.searchData.length; i++) state.searchData.push(w2utils.clone(this.searchData[i]))
|
||
// event before
|
||
let edata = this.trigger('stateSave', { target: this.name, state: state })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
// save into local storage
|
||
if (returnOnly !== true) {
|
||
this.cacheSave('state', state)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
return state
|
||
}
|
||
stateRestore(newState) {
|
||
let url = (typeof this.url != 'object' ? this.url : this.url.get)
|
||
if (!newState) {
|
||
newState = this.cache('state')
|
||
}
|
||
// event before
|
||
let edata = this.trigger('stateRestore', { target: this.name, state: newState })
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
// default behavior
|
||
if (w2utils.isPlainObject(newState)) {
|
||
w2utils.extend(this.show, newState.show ?? {})
|
||
w2utils.extend(this.last, newState.last ?? {})
|
||
let sTop = this.last.scrollTop
|
||
let sLeft = this.last.scrollLeft
|
||
for (let c = 0; c < newState.columns?.length; c++) {
|
||
let tmp = newState.columns[c]
|
||
let col_index = this.getColumn(tmp.field, true)
|
||
if (col_index !== null) {
|
||
w2utils.extend(this.columns[col_index], tmp)
|
||
// restore column order from saved state
|
||
if (c !== col_index) this.columns.splice(c, 0, this.columns.splice(col_index, 1)[0])
|
||
}
|
||
}
|
||
this.sortData.splice(0, this.sortData.length)
|
||
for (let c = 0; c < newState.sortData?.length; c++) {
|
||
this.sortData.push(newState.sortData[c])
|
||
}
|
||
this.searchData.splice(0, this.searchData.length)
|
||
for (let c = 0; c < newState.searchData?.length; c++) {
|
||
this.searchData.push(newState.searchData[c])
|
||
}
|
||
// apply sort and search
|
||
setTimeout(() => {
|
||
// needs timeout as records need to be populated
|
||
// ez 10.09.2014 this -->
|
||
if (!url) {
|
||
if (this.sortData.length > 0) this.localSort()
|
||
if (this.searchData.length > 0) this.localSearch()
|
||
}
|
||
this.last.scrollTop = sTop
|
||
this.last.scrollLeft = sLeft
|
||
this.refresh()
|
||
}, 1)
|
||
console.log(`INFO (w2ui): state restored for "${this.name}"`)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
return true
|
||
}
|
||
stateReset() {
|
||
this.stateRestore(this.last.state)
|
||
this.cacheSave('state', null)
|
||
}
|
||
parseField(obj, field) {
|
||
if (this.nestedFields) {
|
||
let val = ''
|
||
try { // need this to make sure no error in fields
|
||
val = obj
|
||
let tmp = String(field).split('.')
|
||
for (let i = 0; i < tmp.length; i++) {
|
||
val = val[tmp[i]]
|
||
}
|
||
} catch (event) {
|
||
val = ''
|
||
}
|
||
return val
|
||
} else {
|
||
return obj ? obj[field] : ''
|
||
}
|
||
}
|
||
prepareData() {
|
||
let obj = this
|
||
// loops thru records and prepares date and time objects
|
||
for (let r = 0; r < this.records.length; r++) {
|
||
let rec = this.records[r]
|
||
prepareRecord(rec)
|
||
}
|
||
// prepare date and time objects for the 'rec' record and its closed children
|
||
function prepareRecord(rec) {
|
||
for (let c = 0; c < obj.columns.length; c++) {
|
||
let column = obj.columns[c]
|
||
if (rec[column.field] == null || typeof column.render != 'string') continue
|
||
// number
|
||
if (['number', 'int', 'float', 'money', 'currency', 'percent'].indexOf(column.render.split(':')[0]) != -1) {
|
||
if (typeof rec[column.field] != 'number') rec[column.field] = parseFloat(rec[column.field])
|
||
}
|
||
// date
|
||
if (['date', 'age'].indexOf(column.render.split(':')[0]) != -1) {
|
||
if (!rec[column.field + '_']) {
|
||
let dt = rec[column.field]
|
||
if (w2utils.isInt(dt)) dt = parseInt(dt)
|
||
rec[column.field + '_'] = new Date(dt)
|
||
}
|
||
}
|
||
// time
|
||
if (['time'].indexOf(column.render) != -1) {
|
||
if (w2utils.isTime(rec[column.field])) { // if string
|
||
let tmp = w2utils.isTime(rec[column.field], true)
|
||
let dt = new Date()
|
||
dt.setHours(tmp.hours, tmp.minutes, (tmp.seconds ? tmp.seconds : 0), 0) // sets hours, min, sec, mills
|
||
if (!rec[column.field + '_']) rec[column.field + '_'] = dt
|
||
} else { // if date object
|
||
let tmp = rec[column.field]
|
||
if (w2utils.isInt(tmp)) tmp = parseInt(tmp)
|
||
tmp = (tmp != null ? new Date(tmp) : new Date())
|
||
let dt = new Date()
|
||
dt.setHours(tmp.getHours(), tmp.getMinutes(), tmp.getSeconds(), 0) // sets hours, min, sec, mills
|
||
if (!rec[column.field + '_']) rec[column.field + '_'] = dt
|
||
}
|
||
}
|
||
}
|
||
if (rec.w2ui?.children && rec.w2ui?.expanded !== true) {
|
||
// there are closed children, prepare them too.
|
||
for (let r = 0; r < rec.w2ui.children.length; r++) {
|
||
let subRec = rec.w2ui.children[r]
|
||
prepareRecord(subRec)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
nextCell(index, col_ind, editable) {
|
||
let check = col_ind + 1
|
||
if (check >= this.columns.length) {
|
||
index = this.nextRow(index)
|
||
return index == null ? index : this.nextCell(index, -1, editable)
|
||
}
|
||
let tmp = this.records[index].w2ui
|
||
let col = this.columns[check]
|
||
let span = (tmp && tmp.colspan && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1)
|
||
if (col == null) return null
|
||
if (col && col.hidden || span === 0) return this.nextCell(index, check, editable)
|
||
if (editable) {
|
||
let edit = this.getCellEditable(index, check)
|
||
if (edit == null || ['checkbox', 'check'].indexOf(edit.type) != -1) {
|
||
return this.nextCell(index, check, editable)
|
||
}
|
||
}
|
||
return { index, colIndex: check }
|
||
}
|
||
prevCell(index, col_ind, editable) {
|
||
let check = col_ind - 1
|
||
if (check < 0) {
|
||
index = this.prevRow(index)
|
||
return index == null ? index : this.prevCell(index, this.columns.length, editable)
|
||
}
|
||
if (check < 0) return null
|
||
let tmp = this.records[index].w2ui
|
||
let col = this.columns[check]
|
||
let span = (tmp && tmp.colspan && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1)
|
||
if (col == null) return null
|
||
if (col && col.hidden || span === 0) return this.prevCell(index, check, editable)
|
||
if (editable) {
|
||
let edit = this.getCellEditable(index, check)
|
||
if (edit == null || ['checkbox', 'check'].indexOf(edit.type) != -1) {
|
||
return this.prevCell(index, check, editable)
|
||
}
|
||
}
|
||
return { index, colIndex: check }
|
||
}
|
||
nextRow(ind, col_ind, numRows) {
|
||
let sids = this.last.searchIds
|
||
let ret = null
|
||
if (numRows == null) numRows = 1
|
||
if (numRows == -1) {
|
||
return this.records.length-1
|
||
}
|
||
if ((ind + numRows < this.records.length && sids.length === 0) // if there are more records
|
||
|| (sids.length > 0 && ind < sids[sids.length-numRows])) {
|
||
ind += numRows
|
||
if (sids.length > 0) while (true) {
|
||
if (sids.includes(ind) || ind > this.records.length) break
|
||
ind += numRows
|
||
}
|
||
// colspan
|
||
let tmp = this.records[ind].w2ui
|
||
let col = this.columns[col_ind]
|
||
let span = (tmp && tmp.colspan && col != null && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1)
|
||
if (span === 0) {
|
||
ret = this.nextRow(ind, col_ind, numRows)
|
||
} else {
|
||
ret = ind
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
prevRow(ind, col_ind, numRows) {
|
||
let sids = this.last.searchIds
|
||
let ret = null
|
||
if (numRows == null) numRows = 1
|
||
if (numRows == -1) {
|
||
return 0
|
||
}
|
||
if ((ind - numRows >= 0 && sids.length === 0) // if there are more records
|
||
|| (sids.length > 0 && ind > sids[0])) {
|
||
ind -= numRows
|
||
if (sids.length > 0) while (true) {
|
||
if (sids.includes(ind) || ind < 0) break
|
||
ind -= numRows
|
||
}
|
||
// colspan
|
||
let tmp = this.records[ind].w2ui
|
||
let col = this.columns[col_ind]
|
||
let span = (tmp && tmp.colspan && col != null && !isNaN(tmp.colspan[col.field]) ? parseInt(tmp.colspan[col.field]) : 1)
|
||
if (span === 0) {
|
||
ret = this.prevRow(ind, col_ind, numRows)
|
||
} else {
|
||
ret = ind
|
||
}
|
||
}
|
||
return ret
|
||
}
|
||
selectionSave() {
|
||
this.last.saved_sel = this.getSelection()
|
||
return this.last.saved_sel
|
||
}
|
||
selectionRestore(noRefresh) {
|
||
let time = Date.now()
|
||
this.last.selection = { indexes: [], columns: {} }
|
||
let sel = this.last.selection
|
||
let lst = this.last.saved_sel
|
||
if (lst) for (let i = 0; i < lst.length; i++) {
|
||
if (w2utils.isPlainObject(lst[i])) {
|
||
// selectType: cell
|
||
let tmp = this.get(lst[i].recid, true)
|
||
if (tmp != null) {
|
||
if (sel.indexes.indexOf(tmp) == -1) sel.indexes.push(tmp)
|
||
if (!sel.columns[tmp]) sel.columns[tmp] = []
|
||
sel.columns[tmp].push(lst[i].column)
|
||
}
|
||
} else {
|
||
// selectType: row
|
||
let tmp = this.get(lst[i], true)
|
||
if (tmp != null) sel.indexes.push(tmp)
|
||
}
|
||
}
|
||
delete this.last.saved_sel
|
||
if (noRefresh !== true) this.refresh()
|
||
return Date.now() - time
|
||
}
|
||
message(options) {
|
||
return w2utils.message({
|
||
owner: this,
|
||
box : this.box,
|
||
after: '.w2ui-grid-header'
|
||
}, options)
|
||
}
|
||
confirm(options) {
|
||
return w2utils.confirm({
|
||
owner: this,
|
||
box : this.box,
|
||
after: '.w2ui-grid-header'
|
||
}, options)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tabs, w2toolbar, w2tooltip, w2field
|
||
*
|
||
* == TODO ==
|
||
* - include delta on save
|
||
* - tabs below some fields (could already be implemented)
|
||
* - form with toolbar & tabs
|
||
* - promise for load, save, etc.
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
* - removed jQuery dependency
|
||
* - better groups support tabs now
|
||
* - form.confirm - refactored
|
||
* - form.message - refactored
|
||
* - observeResize for the box
|
||
* - removed msgNotJSON, msgAJAXerror
|
||
* - applyFocus -> setFocus
|
||
* - getFieldValue(fieldName) = returns { curent, previous, original }
|
||
* - setFieldVallue(fieldName, value)
|
||
* - getValue(..., original) -- return original if any
|
||
* - added .hideErrors()
|
||
* - reuqest, save, submit - return promises
|
||
* - added prepareParams
|
||
* - this.recid = null if no record needs to be pulled
|
||
* - remove form.multiplart
|
||
* - this.method - for saving only
|
||
*/
|
||
|
||
class w2form extends w2base {
|
||
constructor(options) {
|
||
super(options.name)
|
||
this.name = null
|
||
this.header = ''
|
||
this.box = null // HTML element that hold this element
|
||
this.url = ''
|
||
this.method = null // if defined, it will be http method when saving
|
||
this.routeData = {} // data for dynamic routes
|
||
this.formURL = '' // url where to get form HTML
|
||
this.formHTML = '' // form HTML (might be loaded from the url)
|
||
this.page = 0 // current page
|
||
this.pageStyle = ''
|
||
this.recid = null // if not null, then load record
|
||
this.fields = []
|
||
this.actions = {}
|
||
this.record = {}
|
||
this.original = null
|
||
this.dataType = null // only used when not null, otherwise from w2utils.settings.dataType
|
||
this.postData = {}
|
||
this.httpHeaders = {}
|
||
this.toolbar = {} // if not empty, then it is toolbar
|
||
this.tabs = {} // if not empty, then it is tabs object
|
||
this.style = ''
|
||
this.focus = 0 // focus first or other element
|
||
this.autosize = true // autosize, if false the container must have a height set
|
||
this.nestedFields = true // use field name containing dots as separator to look into object
|
||
this.tabindexBase = 0 // this will be added to the auto numbering
|
||
this.isGenerated = false
|
||
this.last = {
|
||
fetchCtrl: null, // last fetch AbortController
|
||
fetchOptions: null, // last fetch options
|
||
errors: []
|
||
}
|
||
this.onRequest = null
|
||
this.onLoad = null
|
||
this.onValidate = null
|
||
this.onSubmit = null
|
||
this.onProgress = null
|
||
this.onSave = null
|
||
this.onChange = null
|
||
this.onInput = null
|
||
this.onRender = null
|
||
this.onRefresh = null
|
||
this.onResize = null
|
||
this.onDestroy = null
|
||
this.onAction = null
|
||
this.onToolbar = null
|
||
this.onError = null
|
||
this.msgRefresh = 'Loading...'
|
||
this.msgSaving = 'Saving...'
|
||
this.msgServerError = 'Server error'
|
||
this.ALL_TYPES = [ 'text', 'textarea', 'email', 'pass', 'password', 'int', 'float', 'money', 'currency',
|
||
'percent', 'hex', 'alphanumeric', 'color', 'date', 'time', 'datetime', 'toggle', 'checkbox', 'radio',
|
||
'check', 'checks', 'list', 'combo', 'enum', 'file', 'select', 'map', 'array', 'div', 'custom', 'html',
|
||
'empty']
|
||
this.LIST_TYPES = ['select', 'radio', 'check', 'checks', 'list', 'combo', 'enum']
|
||
this.W2FIELD_TYPES = ['int', 'float', 'money', 'currency', 'percent', 'hex', 'alphanumeric', 'color',
|
||
'date', 'time', 'datetime', 'list', 'combo', 'enum', 'file']
|
||
// mix in options
|
||
w2utils.extend(this, options)
|
||
// remember items
|
||
let record = options.record
|
||
let original = options.original
|
||
let fields = options.fields
|
||
let toolbar = options.toolbar
|
||
let tabs = options.tabs
|
||
// extend items
|
||
Object.assign(this, { record: {}, original: null, fields: [], tabs: {}, toolbar: {}, handlers: [] })
|
||
// preprocess fields
|
||
if (fields) {
|
||
let sub =_processFields(fields)
|
||
this.fields = sub.fields
|
||
if (!tabs && sub.tabs.length > 0) {
|
||
tabs = sub.tabs
|
||
}
|
||
}
|
||
// prepare tabs
|
||
if (Array.isArray(tabs)) {
|
||
w2utils.extend(this.tabs, { tabs: [] })
|
||
for (let t = 0; t < tabs.length; t++) {
|
||
let tmp = tabs[t]
|
||
if (typeof tmp === 'object') {
|
||
this.tabs.tabs.push(tmp)
|
||
if (tmp.active === true) {
|
||
this.tabs.active = tmp.id
|
||
}
|
||
} else {
|
||
this.tabs.tabs.push({ id: tmp, text: tmp })
|
||
}
|
||
}
|
||
} else {
|
||
w2utils.extend(this.tabs, tabs)
|
||
}
|
||
w2utils.extend(this.toolbar, toolbar)
|
||
for (let p in record) { // it is an object
|
||
if (w2utils.isPlainObject(record[p])) {
|
||
this.record[p] = w2utils.clone(record[p])
|
||
} else {
|
||
this.record[p] = record[p]
|
||
}
|
||
}
|
||
for (let p in original) { // it is an object
|
||
if (w2utils.isPlainObject(original[p])) {
|
||
this.original[p] = w2utils.clone(original[p])
|
||
} else {
|
||
this.original[p] = original[p]
|
||
}
|
||
}
|
||
// generate html if necessary
|
||
if (this.formURL !== '') {
|
||
fetch(this.formURL)
|
||
.then(resp => resp.text())
|
||
.then(text => {
|
||
this.formHTML = text
|
||
this.isGenerated = true
|
||
if (this.box) this.render(this.box)
|
||
})
|
||
} else if (!this.formURL && !this.formHTML) {
|
||
this.formHTML = this.generateHTML()
|
||
this.isGenerated = true
|
||
} else if (this.formHTML) {
|
||
this.isGenerated = true
|
||
}
|
||
// render if box specified
|
||
if (typeof this.box == 'string') this.box = query(this.box).get(0)
|
||
if (this.box) this.render(this.box)
|
||
function _processFields(fields) {
|
||
let newFields = []
|
||
let tabs = []
|
||
// if it is an object
|
||
if (w2utils.isPlainObject(fields)) {
|
||
let tmp = fields
|
||
fields = []
|
||
Object.keys(tmp).forEach((key) => {
|
||
let fld = tmp[key]
|
||
if (fld.type == 'group') {
|
||
fld.text = key
|
||
if (w2utils.isPlainObject(fld.fields)) {
|
||
let tmp2 = fld.fields
|
||
fld.fields = []
|
||
Object.keys(tmp2).forEach((key2) => {
|
||
let fld2 = tmp2[key2]
|
||
fld2.field = key2
|
||
fld.fields.push(_process(fld2))
|
||
})
|
||
}
|
||
fields.push(fld)
|
||
} else if (fld.type == 'tab') {
|
||
// add tab
|
||
let tab = { id: key, text: key }
|
||
if (fld.style) {
|
||
tab.style = fld.style
|
||
}
|
||
tabs.push(tab)
|
||
// add page to fields
|
||
let sub = _processFields(fld.fields).fields
|
||
sub.forEach(fld2 => {
|
||
fld2.html = fld2.html || {}
|
||
fld2.html.page = tabs.length -1
|
||
_process2(fld, fld2)
|
||
})
|
||
fields.push(...sub)
|
||
} else {
|
||
fld.field = key
|
||
fields.push(_process(fld))
|
||
}
|
||
})
|
||
function _process(fld) {
|
||
let ignore = ['html']
|
||
if (fld.html == null) fld.html = {}
|
||
Object.keys(fld).forEach((key => {
|
||
if (ignore.indexOf(key) != -1) return
|
||
if (['label', 'attr', 'style', 'text', 'span', 'page', 'column', 'anchor',
|
||
'group', 'groupStyle', 'groupTitleStyle', 'groupCollapsible'].indexOf(key) != -1) {
|
||
fld.html[key] = fld[key]
|
||
delete fld[key]
|
||
}
|
||
}))
|
||
return fld
|
||
}
|
||
function _process2(fld, fld2) {
|
||
let ignore = ['style', 'html']
|
||
Object.keys(fld).forEach((key => {
|
||
if (ignore.indexOf(key) != -1) return
|
||
if (['span', 'column', 'attr', 'text', 'label'].indexOf(key) != -1) {
|
||
if (fld[key] && !fld2.html[key]) {
|
||
fld2.html[key] = fld[key]
|
||
}
|
||
}
|
||
}))
|
||
}
|
||
}
|
||
// process groups
|
||
fields.forEach(field => {
|
||
if (field.type == 'group') {
|
||
// group properties
|
||
let group = {
|
||
group: field.text || '',
|
||
groupStyle: field.style || '',
|
||
groupTitleStyle: field.titleStyle || '',
|
||
groupCollapsible: field.collapsible === true ? true : false,
|
||
}
|
||
// loop through fields
|
||
if (Array.isArray(field.fields)) {
|
||
field.fields.forEach(gfield => {
|
||
let fld = w2utils.clone(gfield)
|
||
if (fld.html == null) fld.html = {}
|
||
w2utils.extend(fld.html, group)
|
||
Array('span', 'column', 'attr', 'label', 'page').forEach(key => {
|
||
if (fld.html[key] == null && field[key] != null) {
|
||
fld.html[key] = field[key]
|
||
}
|
||
})
|
||
if (fld.field == null && fld.name != null) {
|
||
console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field)
|
||
fld.field = fld.name
|
||
}
|
||
newFields.push(fld)
|
||
})
|
||
}
|
||
} else {
|
||
let fld = w2utils.clone(field)
|
||
if (fld.field == null && fld.name != null) {
|
||
console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field)
|
||
fld.field = fld.name
|
||
}
|
||
newFields.push(fld)
|
||
}
|
||
})
|
||
return { fields: newFields, tabs }
|
||
}
|
||
}
|
||
get(field, returnIndex) {
|
||
if (arguments.length === 0) {
|
||
let all = []
|
||
for (let f1 = 0; f1 < this.fields.length; f1++) {
|
||
if (this.fields[f1].field != null) all.push(this.fields[f1].field)
|
||
}
|
||
return all
|
||
} else {
|
||
for (let f2 = 0; f2 < this.fields.length; f2++) {
|
||
if (this.fields[f2].field == field) {
|
||
if (returnIndex === true) return f2; else return this.fields[f2]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
}
|
||
set(field, obj) {
|
||
for (let f = 0; f < this.fields.length; f++) {
|
||
if (this.fields[f].field == field) {
|
||
w2utils.extend(this.fields[f] , obj)
|
||
this.refresh(field)
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
getValue(field, original) {
|
||
if (this.nestedFields) {
|
||
let val = undefined
|
||
try { // need this to make sure no error in fields
|
||
let rec = original === true ? this.original : this.record
|
||
val = String(field).split('.').reduce((rec, i) => { return rec[i] }, rec)
|
||
} catch (event) {
|
||
}
|
||
return val
|
||
} else {
|
||
return this.record[field]
|
||
}
|
||
}
|
||
setValue(field, value) {
|
||
// will not refresh the form!
|
||
if (value === '' || value == null
|
||
|| (Array.isArray(value) && value.length === 0)
|
||
|| (w2utils.isPlainObject(value) && Object.keys(value).length == 0)) {
|
||
value = null
|
||
}
|
||
if (this.nestedFields) {
|
||
try { // need this to make sure no error in fields
|
||
let rec = this.record
|
||
String(field).split('.').map((fld, i, arr) => {
|
||
if (arr.length - 1 !== i) {
|
||
if (rec[fld]) rec = rec[fld]; else { rec[fld] = {}; rec = rec[fld] }
|
||
} else {
|
||
rec[fld] = value
|
||
}
|
||
})
|
||
return true
|
||
} catch (event) {
|
||
return false
|
||
}
|
||
} else {
|
||
this.record[field] = value
|
||
return true
|
||
}
|
||
}
|
||
getFieldValue(name) {
|
||
let field = this.get(name)
|
||
if (field == null) return
|
||
let el = field.el
|
||
let previous = this.getValue(name)
|
||
let original = this.getValue(name, true)
|
||
// orginary input control
|
||
let current = el.value
|
||
// should not be set to '', incosistent logic
|
||
// if (previous == null) previous = ''
|
||
// clean extra chars
|
||
if (['int', 'float', 'percent', 'money', 'currency'].includes(field.type)) {
|
||
current = field.w2field.clean(current)
|
||
}
|
||
// radio list
|
||
if (['radio'].includes(field.type)) {
|
||
let selected = query(el).closest('div').find('input:checked').get(0)
|
||
if (selected) {
|
||
let item = field.options.items[query(selected).data('index')]
|
||
current = item.id
|
||
} else {
|
||
current = null
|
||
}
|
||
}
|
||
// single checkbox
|
||
if (['toggle', 'checkbox'].includes(field.type)) {
|
||
current = el.checked
|
||
}
|
||
// check list
|
||
if (['check', 'checks'].indexOf(field.type) !== -1) {
|
||
current = []
|
||
let selected = query(el).closest('div').find('input:checked')
|
||
if (selected.length > 0) {
|
||
selected.each(el => {
|
||
let item = field.options.items[query(el).data('index')]
|
||
current.push(item.id)
|
||
})
|
||
}
|
||
if (!Array.isArray(previous)) previous = []
|
||
}
|
||
// lists
|
||
let selected = el._w2field?.selected // drop downs and other w2field objects
|
||
if (['list', 'enum', 'file'].includes(field.type) && selected) {
|
||
// TODO: check when w2field is refactored
|
||
let nv = selected
|
||
let cv = previous
|
||
if (Array.isArray(nv)) {
|
||
current = []
|
||
for (let i = 0; i < nv.length; i++) current[i] = w2utils.clone(nv[i]) // clone array
|
||
}
|
||
if (Array.isArray(cv)) {
|
||
previous = []
|
||
for (let i = 0; i < cv.length; i++) previous[i] = w2utils.clone(cv[i]) // clone array
|
||
}
|
||
if (w2utils.isPlainObject(nv)) {
|
||
current = w2utils.clone(nv) // clone object
|
||
}
|
||
if (w2utils.isPlainObject(cv)) {
|
||
previous = w2utils.clone(cv) // clone object
|
||
}
|
||
}
|
||
// map, array
|
||
if (['map', 'array'].includes(field.type)) {
|
||
current = (field.type == 'map' ? {} : [])
|
||
field.$el.parent().find('.w2ui-map-field').each(div => {
|
||
let key = query(div).find('.w2ui-map.key').val()
|
||
let value = query(div).find('.w2ui-map.value').val()
|
||
if (field.type == 'map') {
|
||
current[key] = value
|
||
} else {
|
||
current.push(value)
|
||
}
|
||
})
|
||
}
|
||
return { current, previous, original } // current - in input, previous - in form.record, original - before form change
|
||
}
|
||
setFieldValue(name, value) {
|
||
let field = this.get(name)
|
||
if (field == null) return
|
||
let el = field.el
|
||
switch (field.type) {
|
||
case 'toggle':
|
||
case 'checkbox':
|
||
el.checked = value ? true : false
|
||
break
|
||
case 'radio': {
|
||
value = value?.id ?? value
|
||
let inputs = query(el).closest('div').find('input')
|
||
let items = field.options.items
|
||
items.forEach((it, ind) => {
|
||
if (it.id === value) { // need exact match so to match empty string and 0
|
||
inputs.filter(`[data-index="${ind}"]`).prop('checked', true)
|
||
}
|
||
})
|
||
break
|
||
}
|
||
case 'check':
|
||
case 'checks': {
|
||
if (!Array.isArray(value)) {
|
||
if (value != null) {
|
||
value = [value]
|
||
} else {
|
||
value = []
|
||
}
|
||
}
|
||
value = value.map(val => val?.id ?? val) // convert if array of objects
|
||
let inputs = query(el).closest('div').find('input')
|
||
let items = field.options.items
|
||
items.forEach((it, ind) => {
|
||
inputs.filter(`[data-index="${ind}"]`).prop('checked', value.includes(it.id) ? true : false)
|
||
})
|
||
break
|
||
}
|
||
case 'list':
|
||
case 'combo':
|
||
let item = value
|
||
// find item in options.items, if any
|
||
if (item?.id == null && Array.isArray(field.options?.items)) {
|
||
field.options.items.forEach(it => {
|
||
if (it.id === value) item = it
|
||
})
|
||
}
|
||
// if item is found in field.options, update it in the this.records
|
||
if (item != value) {
|
||
this.setValue(field.name, item)
|
||
}
|
||
if (field.type == 'list') {
|
||
field.w2field.selected = item
|
||
field.w2field.refresh()
|
||
} else {
|
||
field.el.value = item?.text ?? value
|
||
}
|
||
break
|
||
case 'enum':
|
||
case 'file': {
|
||
if (!Array.isArray(value)) {
|
||
value = value != null ? [value] : []
|
||
}
|
||
let items = [...value]
|
||
// find item in options.items, if any
|
||
let updated = false
|
||
items.forEach((item, ind) => {
|
||
if (item?.id == null && Array.isArray(field.options.items)) {
|
||
field.options.items.forEach(it => {
|
||
if (it.id == item) {
|
||
items[ind] = it
|
||
updated = true
|
||
}
|
||
})
|
||
}
|
||
})
|
||
if (updated) {
|
||
this.setValue(field.name, items)
|
||
}
|
||
field.w2field.selected = items
|
||
field.w2field.refresh()
|
||
break
|
||
}
|
||
case 'map':
|
||
case 'array': {
|
||
// init map
|
||
if (field.type == 'map' && (value == null || !w2utils.isPlainObject(value))) {
|
||
this.setValue(field.field, {})
|
||
value = this.getValue(field.field)
|
||
}
|
||
if (field.type == 'array' && (value == null || !Array.isArray(value))) {
|
||
this.setValue(field.field, [])
|
||
value = this.getValue(field.field)
|
||
}
|
||
let container = query(field.el).parent().find('.w2ui-map-container')
|
||
field.el.mapRefresh(value, container)
|
||
break
|
||
}
|
||
case 'div':
|
||
case 'custom':
|
||
query(el).html(value)
|
||
break
|
||
case 'html':
|
||
case 'empty':
|
||
break
|
||
default:
|
||
// regular text fields
|
||
el.value = value ?? ''
|
||
break
|
||
}
|
||
}
|
||
show() {
|
||
let effected = []
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
let fld = this.get(arguments[a])
|
||
if (fld && fld.hidden) {
|
||
fld.hidden = false
|
||
effected.push(fld.field)
|
||
}
|
||
}
|
||
if (effected.length > 0) this.refresh.apply(this, effected)
|
||
this.updateEmptyGroups()
|
||
return effected
|
||
}
|
||
hide() {
|
||
let effected = []
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
let fld = this.get(arguments[a])
|
||
if (fld && !fld.hidden) {
|
||
fld.hidden = true
|
||
effected.push(fld.field)
|
||
}
|
||
}
|
||
if (effected.length > 0) this.refresh.apply(this, effected)
|
||
this.updateEmptyGroups()
|
||
return effected
|
||
}
|
||
enable() {
|
||
let effected = []
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
let fld = this.get(arguments[a])
|
||
if (fld && fld.disabled) {
|
||
fld.disabled = false
|
||
effected.push(fld.field)
|
||
}
|
||
}
|
||
if (effected.length > 0) this.refresh.apply(this, effected)
|
||
return effected
|
||
}
|
||
disable() {
|
||
let effected = []
|
||
for (let a = 0; a < arguments.length; a++) {
|
||
let fld = this.get(arguments[a])
|
||
if (fld && !fld.disabled) {
|
||
fld.disabled = true
|
||
effected.push(fld.field)
|
||
}
|
||
}
|
||
if (effected.length > 0) this.refresh.apply(this, effected)
|
||
return effected
|
||
}
|
||
updateEmptyGroups() {
|
||
// hide empty groups
|
||
query(this.box).find('.w2ui-group').each((group) =>{
|
||
if (isHidden(query(group).find('.w2ui-field'))) {
|
||
query(group).hide()
|
||
} else {
|
||
query(group).show()
|
||
}
|
||
})
|
||
function isHidden($els) {
|
||
let flag = true
|
||
$els.each((el) => {
|
||
if (el.style.display != 'none') flag = false
|
||
})
|
||
return flag
|
||
}
|
||
}
|
||
change() {
|
||
Array.from(arguments).forEach((field) => {
|
||
let tmp = this.get(field)
|
||
if (tmp.$el) tmp.$el.change()
|
||
})
|
||
}
|
||
reload(callBack) {
|
||
let url = (typeof this.url !== 'object' ? this.url : this.url.get)
|
||
if (url && this.recid != null) {
|
||
// this.clear();
|
||
return this.request(callBack) // returns promise
|
||
} else {
|
||
// this.refresh(); // no need to refresh
|
||
if (typeof callBack === 'function') callBack()
|
||
return new Promise(resolve => { resolve() }) // resolved promise
|
||
}
|
||
}
|
||
clear() {
|
||
if (arguments.length != 0) {
|
||
Array.from(arguments).forEach((field) => {
|
||
let rec = this.record
|
||
String(field).split('.').map((fld, i, arr) => {
|
||
if (arr.length - 1 !== i) rec = rec[fld]; else delete rec[fld]
|
||
})
|
||
this.refresh(field)
|
||
})
|
||
} else {
|
||
this.recid = null
|
||
this.record = {}
|
||
this.original = null
|
||
this.refresh()
|
||
this.hideErrors()
|
||
}
|
||
}
|
||
error(msg) {
|
||
// let the management of the error outside of the form
|
||
let edata = this.trigger('error', {
|
||
target: this.name,
|
||
message: msg,
|
||
fetchCtrl: this.last.fetchCtrl,
|
||
fetchOptions: this.last.fetchOptions
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// need a time out because message might be already up)
|
||
setTimeout(() => { this.message(msg) }, 1)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
message(options) {
|
||
return w2utils.message({
|
||
owner: this,
|
||
box : this.box,
|
||
after: '.w2ui-form-header'
|
||
}, options)
|
||
}
|
||
confirm(options) {
|
||
return w2utils.confirm({
|
||
owner: this,
|
||
box : this.box,
|
||
after: '.w2ui-form-header'
|
||
}, options)
|
||
}
|
||
validate(showErrors) {
|
||
if (showErrors == null) showErrors = true
|
||
// validate before saving
|
||
let errors = []
|
||
for (let f = 0; f < this.fields.length; f++) {
|
||
let field = this.fields[f]
|
||
if (this.getValue(field.field) == null) this.setValue(field.field, '')
|
||
if (['int', 'float', 'currency', 'money'].indexOf(field.type) != -1) {
|
||
let val = this.getValue(field.field)
|
||
let min = field.options.min
|
||
let max = field.options.max
|
||
if (min != null && val < min) {
|
||
errors.push({ field: field, error: w2utils.lang('Should be more than ${min}', { min }) })
|
||
}
|
||
if (max != null && val > max) {
|
||
errors.push({ field: field, error: w2utils.lang('Should be less than ${max}', { max }) })
|
||
}
|
||
}
|
||
switch (field.type) {
|
||
case 'alphanumeric':
|
||
if (this.getValue(field.field) && !w2utils.isAlphaNumeric(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not alpha-numeric') })
|
||
}
|
||
break
|
||
case 'int':
|
||
if (this.getValue(field.field) && !w2utils.isInt(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not an integer') })
|
||
}
|
||
break
|
||
case 'percent':
|
||
case 'float':
|
||
if (this.getValue(field.field) && !w2utils.isFloat(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not a float') })
|
||
}
|
||
break
|
||
case 'currency':
|
||
case 'money':
|
||
if (this.getValue(field.field) && !w2utils.isMoney(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not in money format') })
|
||
}
|
||
break
|
||
case 'color':
|
||
case 'hex':
|
||
if (this.getValue(field.field) && !w2utils.isHex(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not a hex number') })
|
||
}
|
||
break
|
||
case 'email':
|
||
if (this.getValue(field.field) && !w2utils.isEmail(this.getValue(field.field))) {
|
||
errors.push({ field: field, error: w2utils.lang('Not a valid email') })
|
||
}
|
||
break
|
||
case 'checkbox':
|
||
// convert true/false
|
||
if (this.getValue(field.field) == true) {
|
||
this.setValue(field.field, true)
|
||
} else {
|
||
this.setValue(field.field, false)
|
||
}
|
||
break
|
||
case 'date':
|
||
// format date before submit
|
||
if (!field.options.format) field.options.format = w2utils.settings.dateFormat
|
||
if (this.getValue(field.field) && !w2utils.isDate(this.getValue(field.field), field.options.format)) {
|
||
errors.push({ field: field, error: w2utils.lang('Not a valid date') + ': ' + field.options.format })
|
||
}
|
||
break
|
||
case 'list':
|
||
case 'combo':
|
||
break
|
||
case 'enum':
|
||
break
|
||
}
|
||
// === check required - if field is '0' it should be considered not empty
|
||
let val = this.getValue(field.field)
|
||
if (field.hidden !== true && field.required
|
||
&& !['div', 'custom', 'html', 'empty'].includes(field.type)
|
||
&& (val == null || val === '' || (Array.isArray(val) && val.length === 0)
|
||
|| (w2utils.isPlainObject(val) && Object.keys(val).length == 0))) {
|
||
errors.push({ field: field, error: w2utils.lang('Required field') })
|
||
}
|
||
if (field.hidden !== true && field.options?.minLength > 0
|
||
&& !['enum', 'list', 'combo'].includes(field.type) // since minLength is used there for other purpose
|
||
&& (val == null || val.length < field.options.minLength)) {
|
||
errors.push({ field: field, error: w2utils.lang('Field should be at least ${count} characters.',
|
||
{ count: field.options.minLength })})
|
||
}
|
||
}
|
||
// event before
|
||
let edata = this.trigger('validate', { target: this.name, errors: errors })
|
||
if (edata.isCancelled === true) return
|
||
// show error
|
||
this.last.errors = errors
|
||
if (showErrors) this.showErrors()
|
||
// event after
|
||
edata.finish()
|
||
return errors
|
||
}
|
||
showErrors() {
|
||
// TODO: check edge cases
|
||
// -- scroll
|
||
// -- invisible pages
|
||
// -- form refresh
|
||
let errors = this.last.errors
|
||
if (errors.length <= 0) return
|
||
// show errors
|
||
this.goto(errors[0].field.page)
|
||
query(errors[0].field.$el).parents('.w2ui-field')[0].scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
||
// show errors
|
||
// show only for visible controls
|
||
errors.forEach(error => {
|
||
let opt = w2utils.extend({
|
||
anchorClass: 'w2ui-error',
|
||
class: 'w2ui-light',
|
||
position: 'right|left',
|
||
hideOn: ['input']
|
||
}, error.options)
|
||
if (error.field == null) return
|
||
let anchor = error.field.el
|
||
if (error.field.type === 'radio') { // for radio and checkboxes
|
||
anchor = query(error.field.el).closest('div').get(0)
|
||
} else if (['enum', 'file'].includes(error.field.type)) {
|
||
// TODO: check
|
||
// anchor = (error.field.el).data('w2field').helpers.multi
|
||
// $(fld).addClass('w2ui-error')
|
||
}
|
||
w2tooltip.show(w2utils.extend({
|
||
anchor,
|
||
name: `${this.name}-${error.field.field}-error`,
|
||
html: error.error
|
||
}, opt))
|
||
})
|
||
// hide errors on scroll
|
||
query(errors[0].field.$el).parents('.w2ui-page')
|
||
.off('.hideErrors')
|
||
.on('scroll.hideErrors', (evt) => { this.hideErrors() })
|
||
}
|
||
hideErrors() {
|
||
this.fields.forEach(field => {
|
||
w2tooltip.hide(`${this.name}-${field.field}-error`)
|
||
})
|
||
}
|
||
getChanges() {
|
||
// TODO: not working on nested structures
|
||
let diff = {}
|
||
if (this.original != null && typeof this.original == 'object' && Object.keys(this.record).length !== 0) {
|
||
diff = doDiff(this.record, this.original, {})
|
||
}
|
||
return diff
|
||
function doDiff(record, original, result) {
|
||
if (Array.isArray(record) && Array.isArray(original)) {
|
||
while (record.length < original.length) {
|
||
record.push(null)
|
||
}
|
||
}
|
||
for (let i in record) {
|
||
if (record[i] != null && typeof record[i] === 'object') {
|
||
result[i] = doDiff(record[i], original[i] || {}, {})
|
||
if (!result[i] || (Object.keys(result[i]).length == 0 && Object.keys(original[i].length == 0))) delete result[i]
|
||
} else if (record[i] != original[i] || (record[i] == null && original[i] != null)) { // also catch field clear
|
||
result[i] = record[i]
|
||
}
|
||
}
|
||
return Object.keys(result).length != 0 ? result : null
|
||
}
|
||
}
|
||
getCleanRecord(strict) {
|
||
let data = w2utils.clone(this.record)
|
||
this.fields.forEach((fld) => {
|
||
if (['list', 'combo', 'enum'].indexOf(fld.type) != -1) {
|
||
let tmp = { nestedFields: true, record: data }
|
||
let val = this.getValue.call(tmp, fld.field)
|
||
if (w2utils.isPlainObject(val) && val.id != null) { // should be true if val.id === ''
|
||
this.setValue.call(tmp, fld.field, val.id)
|
||
}
|
||
if (Array.isArray(val)) {
|
||
val.forEach((item, ind) => {
|
||
if (w2utils.isPlainObject(item) && item.id) {
|
||
val[ind] = item.id
|
||
}
|
||
})
|
||
}
|
||
}
|
||
if (fld.type == 'map') {
|
||
let tmp = { nestedFields: true, record: data }
|
||
let val = this.getValue.call(tmp, fld.field)
|
||
if (val._order) delete val._order
|
||
}
|
||
if (fld.type == 'file') {
|
||
let tmp = { nestedFields: true, record: data }
|
||
let val = this.getValue.call(tmp, fld.field) ?? []
|
||
val.forEach(v => {
|
||
delete v.file
|
||
delete v.modified
|
||
})
|
||
this.setValue.call(tmp, fld.field, val)
|
||
}
|
||
})
|
||
// return only records present in description
|
||
if (strict === true) {
|
||
Object.keys(data).forEach((key) => {
|
||
if (!this.get(key)) delete data[key]
|
||
})
|
||
}
|
||
return data
|
||
}
|
||
prepareParams(url, fetchOptions) {
|
||
let dataType = this.dataType ?? w2utils.settings.dataType
|
||
let postParams = fetchOptions.body
|
||
switch (dataType) {
|
||
case 'HTTPJSON':
|
||
postParams = { request: postParams }
|
||
body2params()
|
||
break
|
||
case 'HTTP':
|
||
body2params()
|
||
break
|
||
case 'RESTFULL':
|
||
if (fetchOptions.method == 'POST') {
|
||
fetchOptions.headers['Content-Type'] = 'application/json'
|
||
} else {
|
||
body2params()
|
||
}
|
||
break
|
||
case 'JSON':
|
||
if (fetchOptions.method == 'GET') {
|
||
postParams = { request: postParams }
|
||
body2params()
|
||
} else {
|
||
fetchOptions.headers['Content-Type'] = 'application/json'
|
||
fetchOptions.method = 'POST'
|
||
}
|
||
break
|
||
}
|
||
fetchOptions.body = typeof fetchOptions.body == 'string' ? fetchOptions.body : JSON.stringify(fetchOptions.body)
|
||
return fetchOptions
|
||
function body2params() {
|
||
Object.keys(postParams).forEach(key => {
|
||
let param = postParams[key]
|
||
if (typeof param == 'object') param = JSON.stringify(param)
|
||
url.searchParams.append(key, param)
|
||
})
|
||
delete fetchOptions.body
|
||
}
|
||
}
|
||
request(postData, callBack) { // if (1) param then it is call back if (2) then postData and callBack
|
||
let self = this
|
||
let resolve, reject
|
||
let responseProm = new Promise((res, rej) => { resolve = res; reject = rej })
|
||
// check for multiple params
|
||
if (typeof postData === 'function') {
|
||
callBack = postData
|
||
postData = null
|
||
}
|
||
if (postData == null) postData = {}
|
||
if (!this.url || (typeof this.url === 'object' && !this.url.get)) return
|
||
// build parameters list
|
||
let params = {}
|
||
// add list params
|
||
params.action = 'get'
|
||
params.recid = this.recid
|
||
params.name = this.name
|
||
// append other params
|
||
w2utils.extend(params, this.postData)
|
||
w2utils.extend(params, postData)
|
||
// event before
|
||
let edata = this.trigger('request', { target: this.name, url: this.url, httpMethod: 'GET',
|
||
postData: params, httpHeaders: this.httpHeaders })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
this.record = {}
|
||
this.original = null
|
||
// call server to get data
|
||
this.lock(w2utils.lang(this.msgRefresh))
|
||
let url = edata.detail.url
|
||
if (typeof url === 'object' && url.get) url = url.get
|
||
if (this.last.fetchCtrl) try { this.last.fetchCtrl.abort() } catch (e) {}
|
||
// process url with routeData
|
||
if (Object.keys(this.routeData).length != 0) {
|
||
let info = w2utils.parseRoute(url)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (this.routeData[info.keys[k].name] == null) continue
|
||
url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
}
|
||
url = new URL(url, location)
|
||
let fetchOptions = this.prepareParams(url, {
|
||
method: edata.detail.httpMethod,
|
||
headers: edata.detail.httpHeaders,
|
||
body: edata.detail.postData
|
||
})
|
||
this.last.fetchCtrl = new AbortController()
|
||
fetchOptions.signal = this.last.fetchCtrl.signal
|
||
this.last.fetchOptions = fetchOptions
|
||
fetch(url, fetchOptions)
|
||
.catch(processError)
|
||
.then((resp) => {
|
||
if (resp?.status != 200) {
|
||
// if resp is undefined, it means request was aborted
|
||
if (resp) processError(resp)
|
||
return
|
||
}
|
||
resp.json()
|
||
.catch(processError)
|
||
.then(data => {
|
||
// event before
|
||
let edata = self.trigger('load', {
|
||
target: self.name,
|
||
fetchCtrl: this.last.fetchCtrl,
|
||
fetchOptions: this.last.fetchOptions,
|
||
data
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// for backward compatibility
|
||
if (data.error == null && data.status === 'error') {
|
||
data.error = true
|
||
}
|
||
// if data.record is not present, then assume that entire response is the record
|
||
if (!data.record) {
|
||
Object.assign(data, { record: w2utils.clone(data) })
|
||
}
|
||
// server response error, not due to network issues
|
||
if (data.error === true) {
|
||
self.error(w2utils.lang(data.message ?? this.msgServerError))
|
||
} else {
|
||
self.record = w2utils.clone(data.record)
|
||
}
|
||
// event after
|
||
self.unlock()
|
||
edata.finish()
|
||
self.refresh()
|
||
self.setFocus()
|
||
// call back
|
||
if (typeof callBack === 'function') callBack(data)
|
||
resolve(data)
|
||
})
|
||
})
|
||
// event after
|
||
edata.finish()
|
||
return responseProm
|
||
function processError(response) {
|
||
if (response.name === 'AbortError') {
|
||
// request was aborted by the form
|
||
return
|
||
}
|
||
self.unlock()
|
||
// trigger event
|
||
let edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
if (response.status && response.status != 200) {
|
||
self.error(response.status + ': ' + response.statusText)
|
||
} else {
|
||
console.log('ERROR: Server request failed.', response, '. ',
|
||
'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }},
|
||
'OR:', { error: true, message: 'Error description' })
|
||
self.error(String(response))
|
||
}
|
||
// event after
|
||
edata2.finish()
|
||
reject(response)
|
||
}
|
||
}
|
||
submit(postData, callBack) {
|
||
return this.save(postData, callBack)
|
||
}
|
||
save(postData, callBack) {
|
||
let self = this
|
||
let resolve, reject
|
||
let saveProm = new Promise((res, rej) => { resolve = res; reject = rej })
|
||
// check for multiple params
|
||
if (typeof postData === 'function') {
|
||
callBack = postData
|
||
postData = null
|
||
}
|
||
// validation
|
||
let errors = self.validate(true)
|
||
if (errors.length !== 0) return
|
||
// submit save
|
||
if (postData == null) postData = {}
|
||
if (!self.url || (typeof self.url === 'object' && !self.url.save)) {
|
||
console.log('ERROR: Form cannot be saved because no url is defined.')
|
||
return
|
||
}
|
||
self.lock(w2utils.lang(self.msgSaving) + ' <span id="'+ self.name +'_progress"></span>')
|
||
// build parameters list
|
||
let params = {}
|
||
// add list params
|
||
params.action = 'save'
|
||
params.recid = self.recid
|
||
params.name = self.name
|
||
// append other params
|
||
w2utils.extend(params, self.postData)
|
||
w2utils.extend(params, postData)
|
||
params.record = w2utils.clone(self.record)
|
||
// event before
|
||
let edata = self.trigger('submit', { target: self.name, url: self.url, httpMethod: this.method ?? 'POST',
|
||
postData: params, httpHeaders: self.httpHeaders })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
let url = edata.detail.url
|
||
if (typeof url === 'object' && url.save) url = url.save
|
||
if (self.last.fetchCtrl) self.last.fetchCtrl.abort()
|
||
// process url with routeData
|
||
if (Object.keys(self.routeData).length > 0) {
|
||
let info = w2utils.parseRoute(url)
|
||
if (info.keys.length > 0) {
|
||
for (let k = 0; k < info.keys.length; k++) {
|
||
if (self.routeData[info.keys[k].name] == null) continue
|
||
url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), self.routeData[info.keys[k].name])
|
||
}
|
||
}
|
||
}
|
||
url = new URL(url, location)
|
||
let fetchOptions = this.prepareParams(url, {
|
||
method: edata.detail.httpMethod,
|
||
headers: edata.detail.httpHeaders,
|
||
body: edata.detail.postData
|
||
})
|
||
this.last.fetchCtrl = new AbortController()
|
||
fetchOptions.signal = this.last.fetchCtrl.signal
|
||
this.last.fetchOptions = fetchOptions
|
||
fetch(url, fetchOptions)
|
||
.catch(processError)
|
||
.then(resp => {
|
||
self.unlock()
|
||
if (resp?.status != 200) {
|
||
processError(resp ?? {})
|
||
return
|
||
}
|
||
// parse server response
|
||
resp.json()
|
||
.catch(processError)
|
||
.then(data => {
|
||
// event before
|
||
let edata = self.trigger('save', {
|
||
target: self.name,
|
||
fetchCtrl: this.last.fetchCtrl,
|
||
fetchOptions: this.last.fetchOptions,
|
||
data
|
||
})
|
||
if (edata.isCancelled === true) return
|
||
// server error, not due to network issues
|
||
if (data.error === true) {
|
||
self.error(w2utils.lang(data.message ?? this.msgServerError))
|
||
} else {
|
||
self.original = null
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
self.refresh()
|
||
// call back
|
||
if (typeof callBack === 'function') callBack(data)
|
||
resolve(data)
|
||
})
|
||
})
|
||
// event after
|
||
edata.finish()
|
||
return saveProm
|
||
function processError(response) {
|
||
if (response?.name === 'AbortError') {
|
||
// request was aborted by the form
|
||
return
|
||
}
|
||
self.unlock()
|
||
// trigger event
|
||
let edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
if (response.status && response.status != 200) {
|
||
self.error(response.status + ': ' + response.statusText)
|
||
} else {
|
||
console.log('ERROR: Server request failed.', response, '. ',
|
||
'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }},
|
||
'OR:', { error: true, message: 'Error description' })
|
||
self.error(String(response))
|
||
}
|
||
// event after
|
||
edata2.finish()
|
||
reject()
|
||
}
|
||
}
|
||
lock(msg, showSpinner) {
|
||
let args = Array.from(arguments)
|
||
args.unshift(this.box)
|
||
w2utils.lock(...args)
|
||
}
|
||
unlock(speed) {
|
||
let box = this.box
|
||
w2utils.unlock(box, speed)
|
||
}
|
||
lockPage(page, msg, spinner) {
|
||
let $page = query(this.box).find('.page-' + page)
|
||
if ($page.length){
|
||
// page found
|
||
w2utils.lock($page, msg, spinner)
|
||
return true
|
||
}
|
||
// page with this id not found!
|
||
return false
|
||
}
|
||
unlockPage(page, speed) {
|
||
let $page = query(this.box).find('.page-' + page)
|
||
if ($page.length) {
|
||
// page found
|
||
w2utils.unlock($page, speed)
|
||
return true
|
||
}
|
||
// page with this id not found!
|
||
return false
|
||
}
|
||
goto(page) {
|
||
if (this.page === page) return // already on this page
|
||
if (page != null) this.page = page
|
||
// if it was auto size, resize it
|
||
if (query(this.box).data('autoSize') === true) {
|
||
query(this.box).get(0).clientHeight = 0
|
||
}
|
||
this.refresh()
|
||
}
|
||
generateHTML() {
|
||
let pages = [] // array for each page
|
||
let group = ''
|
||
let page
|
||
let column
|
||
let html
|
||
let tabindex
|
||
let tabindex_str
|
||
for (let f = 0; f < this.fields.length; f++) {
|
||
html = ''
|
||
tabindex = this.tabindexBase + f + 1
|
||
tabindex_str = ' tabindex="'+ tabindex +'"'
|
||
let field = this.fields[f]
|
||
if (field.html == null) field.html = {}
|
||
if (field.options == null) field.options = {}
|
||
if (field.html.caption != null && field.html.label == null) {
|
||
console.log('NOTICE: form field.html.caption property is deprecated, please use field.html.label. Field ->', field)
|
||
field.html.label = field.html.caption
|
||
}
|
||
if (field.html.label == null) field.html.label = field.field
|
||
field.html = w2utils.extend({ label: '', span: 6, attr: '', text: '', style: '', page: 0, column: 0 }, field.html)
|
||
if (page == null) page = field.html.page
|
||
if (column == null) column = field.html.column
|
||
// input control
|
||
let input = `<input id="${field.field}" name="${field.field}" class="w2ui-input" type="text" ${field.html.attr + tabindex_str}>`
|
||
switch (field.type) {
|
||
case 'pass':
|
||
case 'password':
|
||
input = input.replace('type="text"', 'type="password"')
|
||
break
|
||
case 'checkbox': {
|
||
input = `
|
||
<label class="w2ui-box-label">
|
||
<input id="${field.field}" name="${field.field}" class="w2ui-input" type="checkbox" ${field.html.attr + tabindex_str}>
|
||
<span>${field.html.label}</span>
|
||
</label>`
|
||
break
|
||
}
|
||
case 'check':
|
||
case 'checks': {
|
||
if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
|
||
let items = field.options.items
|
||
input = ''
|
||
// normalized options
|
||
if (!Array.isArray(items)) items = []
|
||
if (items.length > 0) {
|
||
items = w2utils.normMenu.call(this, items, field)
|
||
}
|
||
// generate
|
||
for (let i = 0; i < items.length; i++) {
|
||
input += `
|
||
<label class="w2ui-box-label">
|
||
<input id="${field.field + i}" name="${field.field}" class="w2ui-input" type="checkbox"
|
||
${field.html.attr + tabindex_str} data-value="${items[i].id}" data-index="${i}">
|
||
<span> ${items[i].text}</span>
|
||
</label>
|
||
<br>`
|
||
}
|
||
break
|
||
}
|
||
case 'radio': {
|
||
input = ''
|
||
// normalized options
|
||
if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
|
||
let items = field.options.items
|
||
if (!Array.isArray(items)) items = []
|
||
if (items.length > 0) {
|
||
items = w2utils.normMenu.call(this, items, field)
|
||
}
|
||
// generate
|
||
for (let i = 0; i < items.length; i++) {
|
||
input += `
|
||
<label class="w2ui-box-label">
|
||
<input id="${field.field + i}" name="${field.field}" class="w2ui-input" type="radio"
|
||
${field.html.attr + (i === 0 ? tabindex_str : '')}
|
||
data-value="${items[i].id}" data-index="${i}">
|
||
<span> ${items[i].text}</span>
|
||
</label>
|
||
<br>`
|
||
}
|
||
break
|
||
}
|
||
case 'select': {
|
||
input = `<select id="${field.field}" name="${field.field}" class="w2ui-input" ${field.html.attr + tabindex_str}>`
|
||
// normalized options
|
||
if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
|
||
let items = field.options.items
|
||
if (!Array.isArray(items)) items = []
|
||
if (items.length > 0) {
|
||
items = w2utils.normMenu.call(this, items, field)
|
||
}
|
||
// generate
|
||
for (let i = 0; i < items.length; i++) {
|
||
input += `<option value="${items[i].id}">${items[i].text}</option>`
|
||
}
|
||
input += '</select>'
|
||
break
|
||
}
|
||
case 'textarea':
|
||
input = `<textarea id="${field.field}" name="${field.field}" class="w2ui-input" ${field.html.attr + tabindex_str}></textarea>`
|
||
break
|
||
case 'toggle':
|
||
input = `<input id="${field.field}" name="${field.field}" class="w2ui-input w2ui-toggle" type="checkbox" ${field.html.attr + tabindex_str}>
|
||
<div><div></div></div>`
|
||
break
|
||
case 'map':
|
||
case 'array':
|
||
field.html.key = field.html.key || {}
|
||
field.html.value = field.html.value || {}
|
||
field.html.tabindex_str = tabindex_str
|
||
input = '<span style="float: right">' + (field.html.text || '') + '</span>' +
|
||
'<input id="'+ field.field +'" name="'+ field.field +'" type="hidden" '+ field.html.attr + tabindex_str + '>'+
|
||
'<div class="w2ui-map-container"></div>'
|
||
break
|
||
case 'div':
|
||
case 'custom':
|
||
input = '<div id="'+ field.field +'" name="'+ field.field +'" '+ field.html.attr + tabindex_str + ' class="w2ui-input">'+
|
||
(field && field.html && field.html.html ? field.html.html : '') +
|
||
'</div>'
|
||
break
|
||
case 'html':
|
||
case 'empty':
|
||
input = (field && field.html ? (field.html.html || '') + (field.html.text || '') : '')
|
||
break
|
||
}
|
||
if (group !== '') {
|
||
if (page != field.html.page || column != field.html.column || (field.html.group && (group != field.html.group))) {
|
||
pages[page][column] += '\n </div>\n </div>'
|
||
group = ''
|
||
}
|
||
}
|
||
if (field.html.group && (group != field.html.group)) {
|
||
let collapsible = ''
|
||
if (field.html.groupCollapsible) {
|
||
collapsible = '<span class="w2ui-icon-collapse" style="width: 15px; display: inline-block; position: relative; top: -2px;"></span>'
|
||
}
|
||
html += '\n <div class="w2ui-group">'
|
||
+ '\n <div class="w2ui-group-title w2ui-eaction" style="'+ (field.html.groupTitleStyle || '') + '; '
|
||
+ (collapsible != '' ? 'cursor: pointer; user-select: none' : '') + '"'
|
||
+ (collapsible != '' ? 'data-group="' + w2utils.base64encode(field.html.group) + '"' : '')
|
||
+ (collapsible != ''
|
||
? 'data-click="toggleGroup|' + field.html.group + '"'
|
||
: '')
|
||
+ '>'
|
||
+ collapsible + w2utils.lang(field.html.group) + '</div>\n'
|
||
+ ' <div class="w2ui-group-fields" style="'+ (field.html.groupStyle || '') +'">'
|
||
group = field.html.group
|
||
}
|
||
if (field.html.anchor == null) {
|
||
let span = (field.html.span != null ? 'w2ui-span'+ field.html.span : '')
|
||
if (field.html.span == -1) span = 'w2ui-span-none'
|
||
let label = '<label'+ (span == 'none' ? ' style="display: none"' : '') +'>' + w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text) +'</label>'
|
||
if (!field.html.label) label = ''
|
||
html += '\n <div class="w2ui-field '+ span +'" style="'+ (field.hidden ? 'display: none;' : '') + field.html.style +'">'+
|
||
'\n '+ label +
|
||
((field.type === 'empty') ? input : '\n <div>'+ input + (field.type != 'array' && field.type != 'map' ? w2utils.lang(field.type != 'checkbox' ? field.html.text : '') : '') + '</div>') +
|
||
'\n </div>'
|
||
} else {
|
||
pages[field.html.page].anchors = pages[field.html.page].anchors || {}
|
||
pages[field.html.page].anchors[field.html.anchor] = '<div class="w2ui-field w2ui-field-inline" style="'+ (field.hidden ? 'display: none;' : '') + field.html.style +'">'+
|
||
((field.type === 'empty') ? input : '<div>'+ w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text, true) + input + w2utils.lang(field.type != 'checkbox' ? field.html.text : '') + '</div>') +
|
||
'</div>'
|
||
}
|
||
if (pages[field.html.page] == null) pages[field.html.page] = {}
|
||
if (pages[field.html.page][field.html.column] == null) pages[field.html.page][field.html.column] = ''
|
||
pages[field.html.page][field.html.column] += html
|
||
page = field.html.page
|
||
column = field.html.column
|
||
}
|
||
if (group !== '') pages[page][column] += '\n </div>\n </div>'
|
||
if (this.tabs.tabs) {
|
||
for (let i = 0; i < this.tabs.tabs.length; i++) if (pages[i] == null) pages[i] = []
|
||
}
|
||
// buttons if any
|
||
let buttons = ''
|
||
if (Object.keys(this.actions).length > 0) {
|
||
buttons += '\n<div class="w2ui-buttons">'
|
||
tabindex = this.tabindexBase + this.fields.length + 1
|
||
for (let a in this.actions) { // it is an object
|
||
let act = this.actions[a]
|
||
let info = { text: '', style: '', 'class': '' }
|
||
if (w2utils.isPlainObject(act)) {
|
||
if (act.text == null && act.caption != null) {
|
||
console.log('NOTICE: form action.caption property is deprecated, please use action.text. Action ->', act)
|
||
act.text = act.caption
|
||
}
|
||
if (act.text) info.text = act.text
|
||
if (act.style) info.style = act.style
|
||
if (act.class) info.class = act.class
|
||
} else {
|
||
info.text = a
|
||
if (['save', 'update', 'create'].indexOf(a.toLowerCase()) !== -1) info.class = 'w2ui-btn-blue'; else info.class = ''
|
||
}
|
||
buttons += '\n <button name="'+ a +'" class="w2ui-btn '+ info.class +'" style="'+ info.style +'" tabindex="'+ tabindex +'">'+
|
||
w2utils.lang(info.text) +'</button>'
|
||
tabindex++
|
||
}
|
||
buttons += '\n</div>'
|
||
}
|
||
html = ''
|
||
for (let p = 0; p < pages.length; p++){
|
||
html += '<div class="w2ui-page page-'+ p +'" style="' + (p !== 0 ? 'display: none;' : '') + this.pageStyle + '">'
|
||
if (!pages[p]) {
|
||
console.log(`ERROR: Page ${p} does not exist`)
|
||
return false
|
||
}
|
||
if (pages[p].before) {
|
||
html += pages[p].before
|
||
}
|
||
html += '<div class="w2ui-column-container">'
|
||
Object.keys(pages[p]).sort().forEach((c, ind) => {
|
||
if (c == parseInt(c)) {
|
||
html += '<div class="w2ui-column col-'+ c +'">' + (pages[p][c] || '') + '\n</div>'
|
||
}
|
||
})
|
||
html += '\n</div>'
|
||
if (pages[p].after) {
|
||
html += pages[p].after
|
||
}
|
||
html += '\n</div>'
|
||
// process page anchors
|
||
if (pages[p].anchors) {
|
||
Object.keys(pages[p].anchors).forEach((key, ind) => {
|
||
html = html.replace(key, pages[p].anchors[key])
|
||
})
|
||
}
|
||
}
|
||
html += buttons
|
||
return html
|
||
}
|
||
toggleGroup(groupName, show) {
|
||
let el = query(this.box).find('.w2ui-group-title[data-group="' + w2utils.base64encode(groupName) + '"]')
|
||
if (el.length === 0) return
|
||
let el_next = query(el.prop('nextElementSibling'))
|
||
if (typeof show === 'undefined') {
|
||
show = (el_next.css('display') == 'none')
|
||
}
|
||
if (show) {
|
||
el_next.show()
|
||
el.find('span').addClass('w2ui-icon-collapse').removeClass('w2ui-icon-expand')
|
||
} else {
|
||
el_next.hide()
|
||
el.find('span').addClass('w2ui-icon-expand').removeClass('w2ui-icon-collapse')
|
||
}
|
||
}
|
||
action(action, event) {
|
||
let act = this.actions[action]
|
||
let click = act
|
||
if (w2utils.isPlainObject(act) && act.onClick) click = act.onClick
|
||
// event before
|
||
let edata = this.trigger('action', { target: action, action: act, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default actions
|
||
if (typeof click === 'function') click.call(this, event)
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
resize() {
|
||
let self = this
|
||
// event before
|
||
let edata = this.trigger('resize', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// default behaviour
|
||
let header = query(this.box).find(':scope > div .w2ui-form-header')
|
||
let toolbar = query(this.box).find(':scope > div .w2ui-form-toolbar')
|
||
let tabs = query(this.box).find(':scope > div .w2ui-form-tabs')
|
||
let page = query(this.box).find(':scope > div .w2ui-page')
|
||
let dpage = query(this.box).find(':scope > div .w2ui-page.page-'+ this.page + ' > div')
|
||
let buttons = query(this.box).find(':scope > div .w2ui-buttons')
|
||
// if no height, calculate it
|
||
let { headerHeight, tbHeight, tabsHeight } = resizeElements()
|
||
if (this.autosize) { // we don't need autosize every time
|
||
let cHeight = query(this.box).get(0).clientHeight
|
||
if (cHeight === 0 || query(this.box).data('autosize') == 'yes') {
|
||
query(this.box).css({
|
||
height: headerHeight + tbHeight + tabsHeight + 15 // 15 is extra height
|
||
+ (page.length > 0 ? w2utils.getSize(dpage, 'height') : 0)
|
||
+ (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0)
|
||
+ 'px'
|
||
})
|
||
query(this.box).data('autosize', 'yes')
|
||
}
|
||
resizeElements()
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
function resizeElements() {
|
||
let headerHeight = (self.header !== '' ? w2utils.getSize(header, 'height') : 0)
|
||
let tbHeight = (Array.isArray(self.toolbar?.items) && self.toolbar?.items?.length > 0)
|
||
? w2utils.getSize(toolbar, 'height')
|
||
: 0
|
||
let tabsHeight = (Array.isArray(self.tabs?.tabs) && self.tabs?.tabs?.length > 0)
|
||
? w2utils.getSize(tabs, 'height')
|
||
: 0
|
||
// resize elements
|
||
toolbar.css({ top: headerHeight + 'px' })
|
||
tabs.css({ top: headerHeight + tbHeight + 'px' })
|
||
page.css({
|
||
top: headerHeight + tbHeight + tabsHeight + 'px',
|
||
bottom: (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0) + 'px'
|
||
})
|
||
// return some params
|
||
return { headerHeight, tbHeight, tabsHeight }
|
||
}
|
||
}
|
||
refresh() {
|
||
let time = Date.now()
|
||
let self = this
|
||
if (!this.box) return
|
||
if (!this.isGenerated || !query(this.box).html()) return
|
||
// event before
|
||
let edata = this.trigger('refresh', { target: this.name, page: this.page, field: arguments[0], fields: arguments })
|
||
if (edata.isCancelled === true) return
|
||
let fields = Array.from(this.fields.keys())
|
||
if (arguments.length > 0) {
|
||
fields = Array.from(arguments)
|
||
.map((fld, ind) => {
|
||
if (typeof fld != 'string') console.log('ERROR: Arguments in refresh functions should be field names')
|
||
return this.get(fld, true) // get index of field
|
||
})
|
||
.filter((fld, ind) => {
|
||
if (fld != null) return true; else return false
|
||
})
|
||
} else {
|
||
// update field.page with page it belongs too
|
||
query(this.box).find('input, textarea, select').each(el => {
|
||
let name = (query(el).attr('name') != null ? query(el).attr('name') : query(el).attr('id'))
|
||
let field = this.get(name)
|
||
if (field) {
|
||
// find page
|
||
let div = query(el).closest('.w2ui-page')
|
||
if (div.length > 0) {
|
||
for (let i = 0; i < 100; i++) {
|
||
if (div.hasClass('page-'+i)) { field.page = i; break }
|
||
}
|
||
}
|
||
}
|
||
})
|
||
// default action
|
||
query(this.box).find('.w2ui-page').hide()
|
||
query(this.box).find('.w2ui-page.page-' + this.page).show()
|
||
query(this.box).find('.w2ui-form-header').html(w2utils.lang(this.header))
|
||
// refresh tabs if needed
|
||
if (typeof this.tabs === 'object' && Array.isArray(this.tabs.tabs) && this.tabs.tabs.length > 0) {
|
||
query(this.box).find('#form_'+ this.name +'_tabs').show()
|
||
this.tabs.active = this.tabs.tabs[this.page].id
|
||
this.tabs.refresh()
|
||
} else {
|
||
query(this.box).find('#form_'+ this.name +'_tabs').hide()
|
||
}
|
||
// refresh tabs if needed
|
||
if (typeof this.toolbar === 'object' && Array.isArray(this.toolbar.items) && this.toolbar.items.length > 0) {
|
||
query(this.box).find('#form_'+ this.name +'_toolbar').show()
|
||
this.toolbar.refresh()
|
||
} else {
|
||
query(this.box).find('#form_'+ this.name +'_toolbar').hide()
|
||
}
|
||
}
|
||
// refresh values of fields
|
||
for (let f = 0; f < fields.length; f++) {
|
||
let field = this.fields[fields[f]]
|
||
if (field.name == null && field.field != null) field.name = field.field
|
||
if (field.field == null && field.name != null) field.field = field.name
|
||
field.$el = query(this.box).find(`[name='${String(field.name).replace(/\\/g, '\\\\')}']`)
|
||
field.el = field.$el.get(0)
|
||
if (field.el) field.el.id = field.name
|
||
// TODO: check
|
||
if (field.w2field) {
|
||
field.w2field.reset()
|
||
}
|
||
field.$el
|
||
.off('.w2form')
|
||
.on('change.w2form', function(event) {
|
||
let value = self.getFieldValue(field.field)
|
||
// clear error class
|
||
if (['enum', 'file'].includes(field.type)) {
|
||
let helper = field.el._w2field?.helpers?.multi
|
||
query(helper).removeClass('w2ui-error')
|
||
}
|
||
if (this._previous != null) {
|
||
value.previous = this._previous
|
||
delete this._previous
|
||
}
|
||
// event before
|
||
let edata2 = self.trigger('change', { target: this.name, field: this.name, value, originalEvent: event })
|
||
if (edata2.isCancelled === true) return
|
||
// default behavior
|
||
self.setValue(this.name, value.current)
|
||
// event after
|
||
edata2.finish()
|
||
})
|
||
.on('input.w2form', function(event) {
|
||
// remember original
|
||
if (self.original == null) {
|
||
if (Object.keys(self.record).length > 0) {
|
||
self.original = w2utils.clone(self.record)
|
||
} else {
|
||
self.original = {}
|
||
}
|
||
}
|
||
let value = self.getFieldValue(field.field)
|
||
// save previous for change event
|
||
if (this._previous == null) {
|
||
this._previous = value.previous
|
||
}
|
||
// event before
|
||
let edata2 = self.trigger('input', { target: self.name, value, originalEvent: event })
|
||
if (edata2.isCancelled === true) return
|
||
// default action
|
||
self.setValue(this.name, value.current)
|
||
// event after
|
||
edata2.finish()
|
||
})
|
||
// required
|
||
if (field.required) {
|
||
field.$el.closest('.w2ui-field').addClass('w2ui-required')
|
||
} else {
|
||
field.$el.closest('.w2ui-field').removeClass('w2ui-required')
|
||
}
|
||
// disabled
|
||
if (field.disabled != null) {
|
||
if (field.disabled) {
|
||
if (field.$el.data('tabIndex') == null) {
|
||
field.$el.data('tabIndex', field.$el.prop('tabIndex'))
|
||
}
|
||
field.$el
|
||
.prop('readOnly', true)
|
||
.prop('disabled', true)
|
||
.prop('tabIndex', -1)
|
||
.closest('.w2ui-field')
|
||
.addClass('w2ui-disabled')
|
||
} else {
|
||
field.$el
|
||
.prop('readOnly', false)
|
||
.prop('disabled', false)
|
||
.prop('tabIndex', field.$el.data('tabIndex') ?? field.$el.prop('tabIndex') ?? 0)
|
||
.closest('.w2ui-field')
|
||
.removeClass('w2ui-disabled')
|
||
}
|
||
}
|
||
// hidden
|
||
let tmp = field.el
|
||
if (!tmp) tmp = query(this.box).find('#' + field.field)
|
||
if (field.hidden) {
|
||
query(tmp).closest('.w2ui-field').hide()
|
||
} else {
|
||
query(tmp).closest('.w2ui-field').show()
|
||
}
|
||
}
|
||
// attach actions on buttons
|
||
query(this.box).find('button, input[type=button]').each(el => {
|
||
query(el).off('click').on('click', function(event) {
|
||
let action = this.value
|
||
if (this.id) action = this.id
|
||
if (this.name) action = this.name
|
||
self.action(action, event)
|
||
})
|
||
})
|
||
// init controls with record
|
||
for (let f = 0; f < fields.length; f++) {
|
||
let field = this.fields[fields[f]]
|
||
if (!field.el) continue
|
||
if (!field.$el.hasClass('w2ui-input')) field.$el.addClass('w2ui-input')
|
||
field.type = String(field.type).toLowerCase()
|
||
if (!field.options) field.options = {}
|
||
// list type
|
||
if (this.LIST_TYPES.includes(field.type)) {
|
||
let items = field.options.items
|
||
if (items == null) field.options.items = []
|
||
field.options.items = w2utils.normMenu.call(this, items, field)
|
||
}
|
||
// HTML select
|
||
if (field.type == 'select') {
|
||
// generate options
|
||
let items = field.options.items
|
||
let options = ''
|
||
items.forEach(item => {
|
||
options += `<option value="${item.id}">${item.text}</option>`
|
||
})
|
||
field.$el.html(options)
|
||
}
|
||
// w2fields
|
||
if (this.W2FIELD_TYPES.includes(field.type)) {
|
||
field.w2field = field.w2field
|
||
?? new w2field(w2utils.extend({}, field.options, { type: field.type }))
|
||
field.w2field.render(field.el)
|
||
}
|
||
// map and arrays
|
||
if (['map', 'array'].includes(field.type)) {
|
||
// need closure
|
||
(function (obj, field) {
|
||
let keepFocus
|
||
field.el.mapAdd = function(field, div, cnt) {
|
||
let attr = (field.disabled ? ' readOnly ' : '') + (field.html.tabindex_str || '')
|
||
let html = `
|
||
<div class="w2ui-map-field" style="margin-bottom: 5px" data-index="${cnt}">
|
||
${field.type == 'map'
|
||
? `<input type="text" ${field.html.key.attr + attr} class="w2ui-input w2ui-map key">
|
||
${field.html.key.text || ''}
|
||
`
|
||
: ''
|
||
}
|
||
<input type="text" ${field.html.value.attr + attr} class="w2ui-input w2ui-map value">
|
||
${field.html.value.text || ''}
|
||
</div>`
|
||
div.append(html)
|
||
}
|
||
field.el.mapRefresh = function(map, div) {
|
||
// generate options
|
||
let keys, $k, $v
|
||
if (field.type == 'map') {
|
||
if (!w2utils.isPlainObject(map)) map = {}
|
||
if (map._order == null) map._order = Object.keys(map)
|
||
keys = map._order
|
||
}
|
||
if (field.type == 'array') {
|
||
if (!Array.isArray(map)) map = []
|
||
keys = map.map((item, ind) => { return ind })
|
||
}
|
||
// delete extra fields (including empty one)
|
||
let all = div.find('.w2ui-map-field')
|
||
for (let i = all.length-1; i >= keys.length; i--) {
|
||
div.find(`div[data-index='${i}']`).remove()
|
||
}
|
||
for (let ind = 0; ind < keys.length; ind++) {
|
||
let key = keys[ind]
|
||
let fld = div.find(`div[data-index='${ind}']`)
|
||
// add if does not exists
|
||
if (fld.length == 0) {
|
||
field.el.mapAdd(field, div, ind)
|
||
fld = div.find(`div[data-index='${ind}']`)
|
||
}
|
||
fld.attr('data-key', key)
|
||
$k = fld.find('.w2ui-map.key')
|
||
$v = fld.find('.w2ui-map.value')
|
||
let val = map[key]
|
||
if (field.type == 'array') {
|
||
let tmp = map.filter((it) => { return it.key == key ? true : false})
|
||
if (tmp.length > 0) val = tmp[0].value
|
||
}
|
||
$k.val(key)
|
||
$v.val(val)
|
||
if (field.disabled === true || field.disabled === false) {
|
||
$k.prop('readOnly', field.disabled ? true : false)
|
||
$v.prop('readOnly', field.disabled ? true : false)
|
||
}
|
||
}
|
||
let cnt = keys.length
|
||
let curr = div.find(`div[data-index='${cnt}']`)
|
||
// if not disabled - add next if needed
|
||
if (curr.length === 0 && (!$k || $k.val() != '' || $v.val() != '')
|
||
&& !($k && ($k.prop('readOnly') === true || $k.prop('disabled') === true))
|
||
) {
|
||
field.el.mapAdd(field, div, cnt)
|
||
}
|
||
if (field.disabled === true || field.disabled === false) {
|
||
curr.find('.key').prop('readOnly', field.disabled ? true : false)
|
||
curr.find('.value').prop('readOnly', field.disabled ? true : false)
|
||
}
|
||
// attach events
|
||
let container = query(field.el).get(0)?.nextSibling // should be div
|
||
query(container).find('input.w2ui-map')
|
||
.off('.mapChange')
|
||
.on('keyup.mapChange', function(event) {
|
||
let $div = query(event.target).closest('.w2ui-map-field')
|
||
let next = $div.get(0).nextElementSibling
|
||
let prev = $div.get(0).previousElementSibling
|
||
if (event.keyCode == 13) {
|
||
let el = keepFocus ?? next
|
||
if (el instanceof HTMLElement) {
|
||
let inp = query(el).find('input')
|
||
if (inp.length > 0) {
|
||
inp.get(0).focus()
|
||
}
|
||
}
|
||
keepFocus = undefined
|
||
}
|
||
let className = query(event.target).hasClass('key') ? 'key' : 'value'
|
||
if (event.keyCode == 38 && prev) { // up key
|
||
query(prev).find(`input.${className}`).get(0).select()
|
||
event.preventDefault()
|
||
}
|
||
if (event.keyCode == 40 && next) { // down key
|
||
query(next).find(`input.${className}`).get(0).select()
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
.on('keydown.mapChange', function(event) {
|
||
if (event.keyCode == 38 || event.keyCode == 40) {
|
||
event.preventDefault()
|
||
}
|
||
})
|
||
.on('input.mapChange', function(event) {
|
||
let fld = query(event.target).closest('div')
|
||
let cnt = fld.data('index')
|
||
let next = fld.get(0).nextElementSibling
|
||
// if last one, add new empty
|
||
if (fld.find('input').val() != '' && !next) {
|
||
field.el.mapAdd(field, div, parseInt(cnt) + 1)
|
||
} else if (fld.find('input').val() == '' && next) {
|
||
let isEmpty = true
|
||
query(next).find('input').each(el => {
|
||
if (el.value != '') isEmpty = false
|
||
})
|
||
if (isEmpty) {
|
||
query(next).remove()
|
||
}
|
||
}
|
||
})
|
||
.on('change.mapChange', function(event) {
|
||
// remember original
|
||
if (self.original == null) {
|
||
if (Object.keys(self.record).length > 0) {
|
||
self.original = w2utils.clone(self.record)
|
||
} else {
|
||
self.original = {}
|
||
}
|
||
}
|
||
// event before
|
||
let { current, previous, original } = self.getFieldValue(field.field)
|
||
let $cnt = query(event.target).closest('.w2ui-map-container')
|
||
if (field.type == 'map') current._order = []
|
||
$cnt.find('.w2ui-map.key').each(el => { current._order.push(el.value) })
|
||
let edata = self.trigger('change', { target: field.field, field: field.field, originalEvent: event,
|
||
value: { current, previous, original }
|
||
})
|
||
if (edata.isCancelled === true) {
|
||
return
|
||
}
|
||
// delete empty
|
||
if (field.type == 'map') {
|
||
current._order = current._order.filter(k => k !== '')
|
||
delete current['']
|
||
}
|
||
if (field.type == 'array') {
|
||
current = current.filter(k => k !== '')
|
||
}
|
||
if (query(event.target).parent().find('input').val() == '') {
|
||
keepFocus = event.target
|
||
}
|
||
self.setValue(field.field, current)
|
||
field.el.mapRefresh(current, div)
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
}
|
||
})(this, field)
|
||
}
|
||
// set value to HTML input field
|
||
this.setFieldValue(field.field, this.getValue(field.name))
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
this.resize()
|
||
return Date.now() - time
|
||
}
|
||
render(box) {
|
||
let time = Date.now()
|
||
let self = this
|
||
if (typeof box == 'string') box = query(box).get(0)
|
||
// event before
|
||
let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
|
||
if (edata.isCancelled === true) return
|
||
// default action
|
||
if (box != null) {
|
||
// clean previous box
|
||
if (query(this.box).find('#form_'+ this.name +'_form').length > 0) {
|
||
query(this.box).removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-form')
|
||
.html('')
|
||
}
|
||
this.box = box
|
||
}
|
||
if (!this.isGenerated && !this.formHTML) return
|
||
if (!this.box) return
|
||
// render form
|
||
let html = '<div class="w2ui-form-box">' +
|
||
(this.header !== '' ? '<div class="w2ui-form-header">' + w2utils.lang(this.header) + '</div>' : '') +
|
||
' <div id="form_'+ this.name +'_toolbar" class="w2ui-form-toolbar" style="display: none"></div>' +
|
||
' <div id="form_'+ this.name +'_tabs" class="w2ui-form-tabs" style="display: none"></div>' +
|
||
this.formHTML +
|
||
'</div>'
|
||
query(this.box).attr('name', this.name)
|
||
.addClass('w2ui-reset w2ui-form')
|
||
.html(html)
|
||
if (query(this.box).length > 0) query(this.box)[0].style.cssText += this.style
|
||
w2utils.bindEvents(query(this.box).find('.w2ui-eaction'), this)
|
||
// init toolbar regardless it is defined or not
|
||
if (typeof this.toolbar.render !== 'function') {
|
||
this.toolbar = new w2toolbar(w2utils.extend({}, this.toolbar, { name: this.name +'_toolbar', owner: this }))
|
||
this.toolbar.on('click', function(event) {
|
||
let edata = self.trigger('toolbar', { target: event.target, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// no default action
|
||
edata.finish()
|
||
})
|
||
}
|
||
if (typeof this.toolbar === 'object' && typeof this.toolbar.render === 'function') {
|
||
this.toolbar.render(query(this.box).find('#form_'+ this.name +'_toolbar')[0])
|
||
}
|
||
// init tabs regardless it is defined or not
|
||
if (typeof this.tabs.render !== 'function') {
|
||
this.tabs = new w2tabs(w2utils.extend({}, this.tabs, { name: this.name +'_tabs', owner: this, active: this.tabs.active }))
|
||
this.tabs.on('click', function(event) {
|
||
self.goto(this.get(event.target, true))
|
||
})
|
||
}
|
||
if (typeof this.tabs === 'object' && typeof this.tabs.render === 'function') {
|
||
this.tabs.render(query(this.box).find('#form_'+ this.name +'_tabs')[0])
|
||
if (this.tabs.active) this.tabs.click(this.tabs.active)
|
||
}
|
||
// event after
|
||
edata.finish()
|
||
// after render actions
|
||
this.resize()
|
||
let url = (typeof this.url !== 'object' ? this.url : this.url.get)
|
||
if (url && this.recid != null) {
|
||
this.request().catch(error => this.refresh()) // even if there was error, still need refresh
|
||
} else {
|
||
this.refresh()
|
||
}
|
||
// observe div resize
|
||
this.last.observeResize = new ResizeObserver(() => { this.resize() })
|
||
this.last.observeResize.observe(this.box)
|
||
// focus on load
|
||
if (this.focus != -1) {
|
||
let setCount = 0
|
||
let setFocus = () => {
|
||
if (query(self.box).find('input, select, textarea').length > 0) {
|
||
self.setFocus()
|
||
} else {
|
||
setCount++
|
||
if (setCount < 20) setTimeout(setFocus, 50) // 1 sec max
|
||
}
|
||
}
|
||
setFocus()
|
||
}
|
||
return Date.now() - time
|
||
}
|
||
setFocus(focus) {
|
||
if (typeof focus === 'undefined'){
|
||
// no argument - use form's focus property
|
||
focus = this.focus
|
||
}
|
||
let $input
|
||
// focus field by index
|
||
if (w2utils.isInt(focus)){
|
||
if (focus < 0) {
|
||
return
|
||
}
|
||
let inputs = query(this.box)
|
||
.find('div:not(.w2ui-field-helper) > input, select, textarea, div > label:nth-child(1) > [type=radio]')
|
||
.filter(':not(.file-input)')
|
||
// find visible (offsetParent == null for any element is not visible)
|
||
while (inputs[focus].offsetParent == null && inputs.length >= focus) {
|
||
focus++
|
||
}
|
||
if (inputs[focus]) {
|
||
$input = query(inputs[focus])
|
||
}
|
||
} else if (typeof focus === 'string') {
|
||
// focus field by name
|
||
$input = query(this.box).find(`[name='${focus}']`)
|
||
}
|
||
if ($input.length > 0){
|
||
$input.get(0).focus()
|
||
}
|
||
return $input
|
||
}
|
||
destroy() {
|
||
// event before
|
||
let edata = this.trigger('destroy', { target: this.name })
|
||
if (edata.isCancelled === true) return
|
||
// clean up
|
||
if (typeof this.toolbar === 'object' && this.toolbar.destroy) this.toolbar.destroy()
|
||
if (typeof this.tabs === 'object' && this.tabs.destroy) this.tabs.destroy()
|
||
if (query(this.box).find('#form_'+ this.name +'_tabs').length > 0) {
|
||
query(this.box)
|
||
.removeAttr('name')
|
||
.removeClass('w2ui-reset w2ui-form')
|
||
.html('')
|
||
}
|
||
this.last.observeResize?.disconnect()
|
||
delete w2ui[this.name]
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2color, w2menu, w2date
|
||
*
|
||
* == TODO ==
|
||
* - upload (regular files)
|
||
* - BUG with prefix/postfix and arrows (test in different contexts)
|
||
* - multiple date selection
|
||
* - month selection, year selections
|
||
* - MultiSelect - Allow Copy/Paste for single and multi values
|
||
* - add routeData to list/enum
|
||
* - ENUM, LIST: should have same as grid (limit, offset, search, sort)
|
||
* - ENUM, LIST: should support wild chars
|
||
* - add selection of predefined times (used for appointments)
|
||
* - options.items - can be an array
|
||
* - options.msgNoItems - can be a function
|
||
* - REMOTE fields
|
||
*
|
||
* == 2.0 changes
|
||
* - removed jQuery dependency
|
||
* - enum options.autoAdd
|
||
* - [numeric, date] - options.autoCorrect to enforce range and validity
|
||
* - silent only left for files, removed form the rest
|
||
* - remote source response items => records or just an array
|
||
* - deprecated "success" field for remote source response
|
||
* - CSP - fixed inline events
|
||
* - remove clear, use reset instead
|
||
* - options.msgSearch
|
||
* - options.msgNoItems
|
||
*/
|
||
|
||
class w2field extends w2base {
|
||
constructor(type, options) {
|
||
super()
|
||
// sanitization
|
||
if (typeof type == 'string' && options == null) {
|
||
options = { type: type }
|
||
}
|
||
if (typeof type == 'object' && options == null) {
|
||
options = w2utils.clone(type)
|
||
}
|
||
if (typeof type == 'string' && typeof options == 'object') {
|
||
options.type = type
|
||
}
|
||
options.type = String(options.type).toLowerCase()
|
||
this.el = options.el ?? null
|
||
this.selected = null
|
||
this.helpers = {} // object or helper elements
|
||
this.type = options.type ?? 'text'
|
||
this.options = w2utils.clone(options)
|
||
this.onSearch = options.onSearch ?? null
|
||
this.onRequest = options.onRequest ?? null
|
||
this.onLoad = options.onLoad ?? null
|
||
this.onError = options.onError ?? null
|
||
this.onClick = options.onClick ?? null
|
||
this.onAdd = options.onAdd ?? null
|
||
this.onNew = options.onNew ?? null
|
||
this.onRemove = options.onRemove ?? null
|
||
this.onMouseEnter= options.onMouseEnter ?? null
|
||
this.onMouseLeave= options.onMouseLeave ?? null
|
||
this.onScroll = options.onScroll ?? null
|
||
this.tmp = {} // temp object
|
||
// clean up some options
|
||
delete this.options.type
|
||
delete this.options.onSearch
|
||
delete this.options.onRequest
|
||
delete this.options.onLoad
|
||
delete this.options.onError
|
||
delete this.options.onClick
|
||
delete this.options.onMouseEnter
|
||
delete this.options.onMouseLeave
|
||
delete this.options.onScroll
|
||
if (this.el) {
|
||
this.render(this.el)
|
||
}
|
||
}
|
||
render(el) {
|
||
if (!(el instanceof HTMLElement)) {
|
||
console.log('ERROR: Cannot init w2field on empty subject')
|
||
return
|
||
}
|
||
if (el._w2field) {
|
||
el._w2field.reset()
|
||
} else {
|
||
el._w2field = this
|
||
}
|
||
this.el = el
|
||
this.init()
|
||
}
|
||
init() {
|
||
let options = this.options
|
||
let defaults
|
||
// only for INPUT or TEXTAREA
|
||
if (!['INPUT', 'TEXTAREA'].includes(this.el.tagName.toUpperCase())) {
|
||
console.log('ERROR: w2field could only be applied to INPUT or TEXTAREA.', this.el)
|
||
return
|
||
}
|
||
switch (this.type) {
|
||
case 'text':
|
||
case 'int':
|
||
case 'float':
|
||
case 'money':
|
||
case 'currency':
|
||
case 'percent':
|
||
case 'alphanumeric':
|
||
case 'bin':
|
||
case 'hex':
|
||
defaults = {
|
||
min: null,
|
||
max: null,
|
||
step: 1,
|
||
autoFormat: true,
|
||
autoCorrect: true,
|
||
currencyPrefix: w2utils.settings.currencyPrefix,
|
||
currencySuffix: w2utils.settings.currencySuffix,
|
||
currencyPrecision: w2utils.settings.currencyPrecision,
|
||
decimalSymbol: w2utils.settings.decimalSymbol,
|
||
groupSymbol: w2utils.settings.groupSymbol,
|
||
arrow: false,
|
||
keyboard: true,
|
||
precision: null,
|
||
prefix: '',
|
||
suffix: ''
|
||
}
|
||
this.options = w2utils.extend({}, defaults, options)
|
||
options = this.options // since object is re-created, need to re-assign
|
||
options.numberRE = new RegExp('['+ options.groupSymbol + ']', 'g')
|
||
options.moneyRE = new RegExp('['+ options.currencyPrefix + options.currencySuffix + options.groupSymbol +']', 'g')
|
||
options.percentRE = new RegExp('['+ options.groupSymbol + '%]', 'g')
|
||
// no keyboard support needed
|
||
if (['text', 'alphanumeric', 'hex', 'bin'].includes(this.type)) {
|
||
options.arrow = false
|
||
options.keyboard = false
|
||
}
|
||
break
|
||
case 'color':
|
||
defaults = {
|
||
prefix : '#',
|
||
suffix : `<div style="width: ${(parseInt(getComputedStyle(this.el)['font-size'])) || 12}px"> </div>`,
|
||
arrow : false,
|
||
advanced : null, // open advanced by default
|
||
transparent : true
|
||
}
|
||
this.options = w2utils.extend({}, defaults, options)
|
||
options = this.options // since object is re-created, need to re-assign
|
||
break
|
||
case 'date':
|
||
defaults = {
|
||
format : w2utils.settings.dateFormat, // date format
|
||
keyboard : true,
|
||
autoCorrect : true,
|
||
start : null,
|
||
end : null,
|
||
blockDates : [], // array of blocked dates
|
||
blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
|
||
colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
|
||
btnNow : true
|
||
}
|
||
this.options = w2utils.extend({ type: 'date' }, defaults, options)
|
||
options = this.options // since object is re-created, need to re-assign
|
||
if (query(this.el).attr('placeholder') == null) {
|
||
query(this.el).attr('placeholder', options.format)
|
||
}
|
||
break
|
||
case 'time':
|
||
defaults = {
|
||
format : w2utils.settings.timeFormat,
|
||
keyboard : true,
|
||
autoCorrect : true,
|
||
start : null,
|
||
end : null,
|
||
btnNow : true,
|
||
noMinutes : false
|
||
}
|
||
this.options = w2utils.extend({ type: 'time' }, defaults, options)
|
||
options = this.options // since object is re-created, need to re-assign
|
||
if (query(this.el).attr('placeholder') == null) {
|
||
query(this.el).attr('placeholder', options.format)
|
||
}
|
||
break
|
||
case 'datetime':
|
||
defaults = {
|
||
format : w2utils.settings.dateFormat + '|' + w2utils.settings.timeFormat,
|
||
keyboard : true,
|
||
autoCorrect : true,
|
||
start : null,
|
||
end : null,
|
||
startTime : null,
|
||
endTime : null,
|
||
blockDates : [], // array of blocked dates
|
||
blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
|
||
colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
|
||
btnNow : true,
|
||
noMinutes : false
|
||
}
|
||
this.options = w2utils.extend({ type: 'datetime' }, defaults, options)
|
||
options = this.options // since object is re-created, need to re-assign
|
||
if (query(this.el).attr('placeholder') == null) {
|
||
query(this.el).attr('placeholder', options.placeholder || options.format)
|
||
}
|
||
break
|
||
case 'list':
|
||
case 'combo':
|
||
defaults = {
|
||
items : [],
|
||
selected : {},
|
||
url : null, // url to pull data from // TODO: implement
|
||
recId : null, // map retrieved data from url to id, can be string or function
|
||
recText : null, // map retrieved data from url to text, can be string or function
|
||
method : null, // default httpMethod
|
||
interval : 350, // number of ms to wait before sending server call on search
|
||
postData : {},
|
||
minLength : 1, // min number of chars when trigger search
|
||
cacheMax : 250,
|
||
maxDropHeight : 350, // max height for drop down menu
|
||
maxDropWidth : null, // if null then auto set
|
||
minDropWidth : null, // if null then auto set
|
||
match : 'begins', // ['contains', 'is', 'begins', 'ends']
|
||
icon : null,
|
||
iconStyle : '',
|
||
align : 'both', // same width as control
|
||
altRows : true, // alternate row color
|
||
renderDrop : null, // render function for drop down item
|
||
compare : null, // compare function for filtering
|
||
filter : true, // weather to filter at all
|
||
hideSelected : false, // hide selected item from drop down
|
||
prefix : '',
|
||
suffix : '',
|
||
msgNoItems : 'No matches',
|
||
msgSearch : 'Type to search...',
|
||
openOnFocus : false, // if to show overlay onclick or when typing
|
||
markSearch : false,
|
||
onSearch : null, // when search needs to be performed
|
||
onRequest : null, // when request is submitted
|
||
onLoad : null, // when data is received
|
||
onError : null // when data fails to load due to server error or other failure modes
|
||
}
|
||
if (typeof options.items == 'function') {
|
||
options._items_fun = options.items
|
||
}
|
||
// need to be first
|
||
options.items = w2utils.normMenu.call(this, options.items)
|
||
if (this.type === 'list') {
|
||
// defaults.search = (options.items && options.items.length >= 10 ? true : false);
|
||
query(this.el).addClass('w2ui-select')
|
||
// if simple value - look it up
|
||
if (!w2utils.isPlainObject(options.selected) && Array.isArray(options.items)) {
|
||
options.items.forEach(item => {
|
||
if (item && item.id === options.selected) {
|
||
options.selected = w2utils.clone(item)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
options = w2utils.extend({}, defaults, options)
|
||
this.options = options
|
||
if (!w2utils.isPlainObject(options.selected)) options.selected = {}
|
||
this.selected = options.selected
|
||
query(this.el)
|
||
.attr('autocapitalize', 'off')
|
||
.attr('autocomplete', 'off')
|
||
.attr('autocorrect', 'off')
|
||
.attr('spellcheck', 'false')
|
||
if (options.selected.text != null) {
|
||
query(this.el).val(options.selected.text)
|
||
}
|
||
break
|
||
case 'enum':
|
||
defaults = {
|
||
items : [], // id, text, tooltip, icon
|
||
selected : [],
|
||
max : 0, // max number of selected items, 0 - unlimited
|
||
url : null, // not implemented
|
||
recId : null, // map retrieved data from url to id, can be string or function
|
||
recText : null, // map retrieved data from url to text, can be string or function
|
||
interval : 350, // number of ms to wait before sending server call on search
|
||
method : null, // default httpMethod
|
||
postData : {},
|
||
minLength : 1, // min number of chars when trigger search
|
||
cacheMax : 250,
|
||
maxItemWidth : 250, // max width for a single item
|
||
maxDropHeight : 350, // max height for drop down menu
|
||
maxDropWidth : null, // if null then auto set
|
||
match : 'contains', // ['contains', 'is', 'begins', 'ends']
|
||
align : '', // align drop down related to search field
|
||
altRows : true, // alternate row color
|
||
openOnFocus : false, // if to show overlay onclick or when typing
|
||
markSearch : false,
|
||
renderDrop : null, // render function for drop down item
|
||
renderItem : null, // render selected item
|
||
compare : null, // compare function for filtering
|
||
filter : true, // alias for compare
|
||
hideSelected : true, // hide selected item from drop down
|
||
style : '', // style for container div
|
||
msgNoItems : 'No matches',
|
||
msgSearch : 'Type to search...',
|
||
onSearch : null, // when search needs to be performed
|
||
onRequest : null, // when request is submitted
|
||
onLoad : null, // when data is received
|
||
onError : null, // when data fails to load due to server error or other failure modes
|
||
onClick : null, // when an item is clicked
|
||
onAdd : null, // when an item is added
|
||
onNew : null, // when new item should be added
|
||
onRemove : null, // when an item is removed
|
||
onMouseEnter : null, // when an item is mouse over
|
||
onMouseLeave : null, // when an item is mouse out
|
||
onScroll : null // when div with selected items is scrolled
|
||
}
|
||
options = w2utils.extend({}, defaults, options, { suffix: '' })
|
||
if (typeof options.items == 'function') {
|
||
options._items_fun = options.items
|
||
}
|
||
options.items = w2utils.normMenu.call(this, options.items)
|
||
options.selected = w2utils.normMenu.call(this, options.selected)
|
||
this.options = options
|
||
if (!Array.isArray(options.selected)) options.selected = []
|
||
this.selected = options.selected
|
||
break
|
||
case 'file':
|
||
defaults = {
|
||
selected : [],
|
||
max : 0,
|
||
maxSize : 0, // max size of all files, 0 - unlimited
|
||
maxFileSize : 0, // max size of a single file, 0 -unlimited
|
||
maxItemWidth : 250, // max width for a single item
|
||
maxDropHeight : 350, // max height for drop down menu
|
||
maxDropWidth : null, // if null then auto set
|
||
readContent : true, // if true, it will readAsDataURL content of the file
|
||
silent : true,
|
||
align : 'both', // same width as control
|
||
altRows : true, // alternate row color
|
||
renderItem : null, // render selected item
|
||
style : '', // style for container div
|
||
onClick : null, // when an item is clicked
|
||
onAdd : null, // when an item is added
|
||
onRemove : null, // when an item is removed
|
||
onMouseEnter : null, // when an item is mouse over
|
||
onMouseLeave : null // when an item is mouse out
|
||
}
|
||
options = w2utils.extend({}, defaults, options)
|
||
this.options = options
|
||
if (!Array.isArray(options.selected)) options.selected = []
|
||
this.selected = options.selected
|
||
if (query(this.el).attr('placeholder') == null) {
|
||
query(this.el).attr('placeholder', w2utils.lang('Attach files by dragging and dropping or Click to Select'))
|
||
}
|
||
break
|
||
}
|
||
// attach events
|
||
query(this.el)
|
||
.css('box-sizing', 'border-box')
|
||
.addClass('w2field w2ui-input')
|
||
.off('.w2field')
|
||
.on('change.w2field', (event) => { this.change(event) })
|
||
.on('click.w2field', (event) => { this.click(event) })
|
||
.on('focus.w2field', (event) => { this.focus(event) })
|
||
.on('blur.w2field', (event) => { if (this.type !== 'list') this.blur(event) })
|
||
.on('keydown.w2field', (event) => { this.keyDown(event) })
|
||
.on('keyup.w2field', (event) => { this.keyUp(event) })
|
||
// suffix and prefix need to be after styles
|
||
this.addPrefix() // only will add if needed
|
||
this.addSuffix() // only will add if needed
|
||
this.addSearch()
|
||
this.addMultiSearch()
|
||
// this.refresh() // do not call refresh, on change will trigger refresh (for list at list)
|
||
// format initial value
|
||
this.change(new Event('change'))
|
||
}
|
||
get() {
|
||
let ret
|
||
if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
|
||
ret = this.selected
|
||
} else {
|
||
ret = query(this.el).val()
|
||
}
|
||
return ret
|
||
}
|
||
set(val, append) {
|
||
if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
|
||
if (this.type !== 'list' && append) {
|
||
if (!Array.isArray(this.selected)) this.selected = []
|
||
this.selected.push(val)
|
||
// update selected array in overlay
|
||
let overlay = w2menu.get(this.el.id + '_menu')
|
||
if (overlay) overlay.options.selected = this.selected
|
||
query(this.el).trigger('input').trigger('change')
|
||
} else {
|
||
if (val == null) val = []
|
||
let it = (this.type === 'enum' && !Array.isArray(val) ? [val] : val)
|
||
this.selected = it
|
||
query(this.el).trigger('input').trigger('change')
|
||
}
|
||
this.refresh()
|
||
} else {
|
||
query(this.el).val(val)
|
||
}
|
||
}
|
||
setIndex(ind, append) {
|
||
if (['list', 'enum'].indexOf(this.type) !== -1) {
|
||
let items = this.options.items
|
||
if (items && items[ind]) {
|
||
if (this.type == 'list') {
|
||
this.selected = items[ind]
|
||
}
|
||
if (this.type == 'enum') {
|
||
if (!append) this.selected = []
|
||
this.selected.push(items[ind])
|
||
}
|
||
let overlay = w2menu.get(this.el.id + '_menu')
|
||
if (overlay) overlay.options.selected = this.selected
|
||
query(this.el).trigger('input').trigger('change')
|
||
this.refresh()
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
refresh() {
|
||
let options = this.options
|
||
let time = Date.now()
|
||
let styles = getComputedStyle(this.el)
|
||
// enum
|
||
if (this.type == 'list') {
|
||
query(this.el).parent().css('white-space', 'nowrap') // needs this for arrow always to appear on the right side
|
||
// hide focus and show text
|
||
if (this.helpers.prefix) this.helpers.prefix.hide()
|
||
if (!this.helpers.search) return
|
||
// if empty show no icon
|
||
if (this.selected == null && options.icon) {
|
||
options.prefix = `
|
||
<span class="w2ui-icon ${options.icon} "style="cursor: pointer; font-size: 14px;
|
||
display: inline-block; margin-top: -1px; color: #7F98AD; ${options.iconStyle}">
|
||
</span>`
|
||
this.addPrefix()
|
||
} else {
|
||
options.prefix = ''
|
||
this.addPrefix()
|
||
}
|
||
// focus helper
|
||
let focus = query(this.helpers.search_focus)
|
||
let icon = query(focus[0].previousElementSibling)
|
||
focus.css({ outline: 'none' })
|
||
if (focus.val() === '') {
|
||
focus.css('opacity', 0)
|
||
icon.css('opacity', 0)
|
||
if (this.selected?.id) {
|
||
let text = this.selected.text
|
||
let ind = this.findItemIndex(options.items, this.selected.id)
|
||
if (text != null) {
|
||
query(this.el)
|
||
.val(w2utils.lang(text))
|
||
.data({
|
||
selected: text,
|
||
selectedIndex: ind[0]
|
||
})
|
||
}
|
||
} else {
|
||
this.el.value = ''
|
||
query(this.el).removeData('selected selectedIndex')
|
||
}
|
||
} else {
|
||
focus.css('opacity', 1)
|
||
icon.css('opacity', 1)
|
||
query(this.el).val('')
|
||
setTimeout(() => {
|
||
if (this.helpers.prefix) this.helpers.prefix.hide()
|
||
if (options.icon) {
|
||
focus.css('margin-left', '17px')
|
||
query(this.helpers.search).find('.w2ui-icon-search')
|
||
.addClass('show-search')
|
||
} else {
|
||
focus.css('margin-left', '0px')
|
||
query(this.helpers.search).find('.w2ui-icon-search')
|
||
.removeClass('show-search')
|
||
}
|
||
}, 1)
|
||
}
|
||
// if readonly or disabled
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
|
||
setTimeout(() => {
|
||
if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '0.6')
|
||
if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '0.6')
|
||
}, 1)
|
||
} else {
|
||
setTimeout(() => {
|
||
if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '1')
|
||
if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '1')
|
||
}, 1)
|
||
}
|
||
}
|
||
let div = this.helpers.multi
|
||
if (['enum', 'file'].includes(this.type) && div) {
|
||
let html = ''
|
||
if (Array.isArray(this.selected)) {
|
||
this.selected.forEach((it, ind) => {
|
||
if (it == null) return
|
||
html += `
|
||
<div class="li-item" index="${ind}" style="max-width: ${parseInt(options.maxItemWidth)}px; ${it.style ? it.style : ''}">
|
||
${
|
||
typeof options.renderItem === 'function'
|
||
? options.renderItem(it, ind, `<div class="w2ui-list-remove" index="${ind}">  </div>`)
|
||
: `
|
||
${it.icon ? `<span class="w2ui-icon ${it.icon}"></span>` : ''}
|
||
<div class="w2ui-list-remove" index="${ind}">  </div>
|
||
${(this.type === 'enum' ? it.text : it.name) ?? it.id ?? it }
|
||
${it.size ? `<span class="file-size"> - ${w2utils.formatSize(it.size)}</span>` : ''}
|
||
`
|
||
}
|
||
</div>`
|
||
})
|
||
}
|
||
let ul = div.find('.w2ui-multi-items')
|
||
if (options.style) {
|
||
div.attr('style', div.attr('style') + ';' + options.style)
|
||
}
|
||
query(this.el).css('z-index', '-1')
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
|
||
setTimeout(() => {
|
||
div[0].scrollTop = 0 // scroll to the top
|
||
div.addClass('w2ui-readonly')
|
||
.find('.li-item').css('opacity', '0.9')
|
||
.parent().find('.li-search').hide()
|
||
.find('input').prop('readOnly', true)
|
||
.closest('.w2ui-multi-items')
|
||
.find('.w2ui-list-remove').hide()
|
||
}, 1)
|
||
} else {
|
||
setTimeout(() => {
|
||
div.removeClass('w2ui-readonly')
|
||
.find('.li-item').css('opacity', '1')
|
||
.parent().find('.li-search').show()
|
||
.find('input').prop('readOnly', false)
|
||
.closest('.w2ui-multi-items')
|
||
.find('.w2ui-list-remove').show()
|
||
}, 1)
|
||
}
|
||
// clean
|
||
if (this.selected?.length > 0) {
|
||
query(this.el).attr('placeholder', '')
|
||
}
|
||
div.find('.w2ui-enum-placeholder').remove()
|
||
ul.find('.li-item').remove()
|
||
// add new list
|
||
if (html !== '') {
|
||
ul.prepend(html)
|
||
} else if (query(this.el).attr('placeholder') != null && div.find('input').val() === '') {
|
||
let style = w2utils.stripSpaces(`
|
||
padding-top: ${styles['padding-top']};
|
||
padding-left: ${styles['padding-left']};
|
||
box-sizing: ${styles['box-sizing']};
|
||
line-height: ${styles['line-height']};
|
||
font-size: ${styles['font-size']};
|
||
font-family: ${styles['font-family']};
|
||
`)
|
||
div.prepend(`<div class="w2ui-enum-placeholder" style="${style}">${query(this.el).attr('placeholder')}</div>`)
|
||
}
|
||
// ITEMS events
|
||
div.off('.w2item')
|
||
.on('scroll.w2item', (event) => {
|
||
let edata = this.trigger('scroll', { target: this.el, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// hide tooltip if any
|
||
w2tooltip.hide(this.el.id + '_preview')
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
.find('.li-item')
|
||
.on('click.w2item', (event) => {
|
||
let target = query(event.target).closest('.li-item')
|
||
let index = target.attr('index')
|
||
let item = this.selected[index]
|
||
if (query(target).hasClass('li-search')) return
|
||
event.stopPropagation()
|
||
let edata
|
||
// default behavior
|
||
if (query(event.target).hasClass('w2ui-list-remove')) {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
// trigger event
|
||
edata = this.trigger('remove', { target: this.el, originalEvent: event, item })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
this.selected.splice(index, 1)
|
||
query(this.el).trigger('input').trigger('change')
|
||
query(event.target).remove()
|
||
} else {
|
||
// trigger event
|
||
edata = this.trigger('click', { target: this.el, originalEvent: event.originalEvent, item })
|
||
if (edata.isCancelled === true) return
|
||
// if file - show image preview
|
||
let preview = item.tooltip
|
||
if (this.type === 'file') {
|
||
if ((/image/i).test(item.type)) { // image
|
||
preview = `
|
||
<div class="w2ui-file-preview">
|
||
<img src="${(item.content ? 'data:'+ item.type +';base64,'+ item.content : '')}"
|
||
style="max-width: 300px">
|
||
</div>`
|
||
}
|
||
preview += `
|
||
<div class="w2ui-file-info">
|
||
<div class="file-caption">${w2utils.lang('Name')}:</div>
|
||
<div class="file-value">${item.name}</div>
|
||
<div class="file-caption">${w2utils.lang('Size')}:</div>
|
||
<div class="file-value">${w2utils.formatSize(item.size)}</div>
|
||
<div class="file-caption">${w2utils.lang('Type')}:</div>
|
||
<div class="file-value file-type">${item.type}</div>
|
||
<div class="file-caption">${w2utils.lang('Modified')}:</div>
|
||
<div class="file-value">${w2utils.date(item.modified)}</div>
|
||
</div>`
|
||
}
|
||
if (preview) {
|
||
let name = this.el.id + '_preview'
|
||
w2tooltip.show({
|
||
name,
|
||
anchor: target.get(0),
|
||
html: preview,
|
||
hideOn: ['doc-click'],
|
||
class: ''
|
||
})
|
||
.show((event) => {
|
||
let $img = query(`#w2overlay-${name} img`)
|
||
$img.on('load', function (event) {
|
||
let w = this.clientWidth
|
||
let h = this.clientHeight
|
||
if (w < 300 & h < 300) return
|
||
if (w >= h && w > 300) query(this).css('width', '300px')
|
||
if (w < h && h > 300) query(this).css('height', '300px')
|
||
})
|
||
.on('error', function (event) {
|
||
this.style.display = 'none'
|
||
})
|
||
})
|
||
}
|
||
edata.finish()
|
||
}
|
||
})
|
||
.on('mouseenter.w2item', (event) => {
|
||
let target = query(event.target).closest('.li-item')
|
||
if (query(target).hasClass('li-search')) return
|
||
let item = this.selected[query(event.target).attr('index')]
|
||
// trigger event
|
||
let edata = this.trigger('mouseEnter', { target: this.el, originalEvent: event, item })
|
||
if (edata.isCancelled === true) return
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
.on('mouseleave.w2item', (event) => {
|
||
let target = query(event.target).closest('.li-item')
|
||
if (query(target).hasClass('li-search')) return
|
||
let item = this.selected[query(event.target).attr('index')]
|
||
// trigger event
|
||
let edata = this.trigger('mouseLeave', { target: this.el, originalEvent: event, item })
|
||
if (edata.isCancelled === true) return
|
||
// event after
|
||
edata.finish()
|
||
})
|
||
// update size for enum, hide for file
|
||
if (this.type === 'enum') {
|
||
let search = this.helpers.multi.find('input')
|
||
search.css({ width: '15px' })
|
||
} else {
|
||
this.helpers.multi.find('.li-search').hide()
|
||
}
|
||
this.resize()
|
||
}
|
||
return Date.now() - time
|
||
}
|
||
// resizing width of list, enum, file controls
|
||
resize() {
|
||
let width = this.el.clientWidth
|
||
// let height = this.el.clientHeight
|
||
// if (this.tmp.current_width == width && height > 0) return
|
||
let styles = getComputedStyle(this.el)
|
||
let focus = this.helpers.search
|
||
let multi = this.helpers.multi
|
||
let suffix = this.helpers.suffix
|
||
let prefix = this.helpers.prefix
|
||
// resize helpers
|
||
if (focus) {
|
||
query(focus).css('width', width)
|
||
}
|
||
if (multi) {
|
||
query(multi).css('width', width - parseInt(styles['margin-left'], 10) - parseInt(styles['margin-right'], 10))
|
||
}
|
||
if (suffix) {
|
||
this.addSuffix()
|
||
}
|
||
if (prefix) {
|
||
this.addPrefix()
|
||
}
|
||
// enum or file
|
||
let div = this.helpers.multi
|
||
if (['enum', 'file'].includes(this.type) && div) {
|
||
// adjust height
|
||
query(this.el).css('height', 'auto')
|
||
let cntHeight = query(div).find(':scope div.w2ui-multi-items').get(0).clientHeight + 5
|
||
if (cntHeight < 20) cntHeight = 20
|
||
// max height
|
||
if (cntHeight > this.tmp['max-height']) {
|
||
cntHeight = this.tmp['max-height']
|
||
}
|
||
// min height
|
||
if (cntHeight < this.tmp['min-height']) {
|
||
cntHeight = this.tmp['min-height']
|
||
}
|
||
let inpHeight = w2utils.getSize(this.el, 'height') - 2
|
||
if (inpHeight > cntHeight) cntHeight = inpHeight
|
||
query(div).css({
|
||
'height': cntHeight + 'px',
|
||
overflow: (cntHeight == this.tmp['max-height'] ? 'auto' : 'hidden')
|
||
})
|
||
query(div).css('height', cntHeight + 'px')
|
||
query(this.el).css({ 'height': cntHeight + 'px' })
|
||
}
|
||
// remember width
|
||
this.tmp.current_width = width
|
||
}
|
||
reset() {
|
||
// restore paddings
|
||
if (this.tmp != null) {
|
||
query(this.el).css('height', 'auto')
|
||
Array('padding-left', 'padding-right', 'background-color', 'border-color').forEach(prop => {
|
||
if (this.tmp && this.tmp['old-'+ prop] != null) {
|
||
query(this.el).css(prop, this.tmp['old-' + prop])
|
||
delete this.tmp['old-' + prop]
|
||
}
|
||
})
|
||
// remove resize watcher
|
||
clearInterval(this.tmp.sizeTimer)
|
||
}
|
||
// remove events and (data)
|
||
query(this.el)
|
||
.val(this.clean(query(this.el).val()))
|
||
.removeClass('w2field')
|
||
.removeData('selected selectedIndex')
|
||
.off('.w2field') // remove only events added by w2field
|
||
// remove helpers
|
||
Object.keys(this.helpers).forEach(key => {
|
||
query(this.helpers[key]).remove()
|
||
})
|
||
this.helpers = {}
|
||
}
|
||
clean(val) {
|
||
// issue #499
|
||
if (typeof val === 'number'){
|
||
return val
|
||
}
|
||
let options = this.options
|
||
val = String(val).trim()
|
||
// clean
|
||
if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
|
||
if (typeof val === 'string') {
|
||
if (options.autoFormat) {
|
||
if (['money', 'currency'].includes(this.type)) {
|
||
val = String(val).replace(options.moneyRE, '')
|
||
}
|
||
if (this.type === 'percent') {
|
||
val = String(val).replace(options.percentRE, '')
|
||
}
|
||
if (['int', 'float'].includes(this.type)) {
|
||
val = String(val).replace(options.numberRE, '')
|
||
}
|
||
}
|
||
val = val.replace(/\s+/g, '')
|
||
.replace(new RegExp(options.groupSymbol, 'g'), '')
|
||
.replace(options.decimalSymbol, '.')
|
||
}
|
||
if (val !== '' && w2utils.isFloat(val)) val = Number(val); else val = ''
|
||
}
|
||
return val
|
||
}
|
||
format(val) {
|
||
let options = this.options
|
||
// auto format numbers or money
|
||
if (options.autoFormat && val !== '') {
|
||
switch (this.type) {
|
||
case 'money':
|
||
case 'currency':
|
||
val = w2utils.formatNumber(val, options.currencyPrecision, true)
|
||
if (val !== '') val = options.currencyPrefix + val + options.currencySuffix
|
||
break
|
||
case 'percent':
|
||
val = w2utils.formatNumber(val, options.precision, true)
|
||
if (val !== '') val += '%'
|
||
break
|
||
case 'float':
|
||
val = w2utils.formatNumber(val, options.precision, true)
|
||
break
|
||
case 'int':
|
||
val = w2utils.formatNumber(val, 0, true)
|
||
break
|
||
}
|
||
// if default group symbol does not match - replase it
|
||
let group = parseInt(1000).toLocaleString(w2utils.settings.locale, { useGrouping: true }).slice(1, 2)
|
||
if (group !== this.options.groupSymbol) {
|
||
val = val.replaceAll(group, this.options.groupSymbol)
|
||
}
|
||
}
|
||
return val
|
||
}
|
||
change(event) {
|
||
// numeric
|
||
if (['int', 'float', 'money', 'currency', 'percent'].indexOf(this.type) !== -1) {
|
||
// check max/min
|
||
let val = query(this.el).val()
|
||
let new_val = this.format(this.clean(query(this.el).val()))
|
||
// if was modified
|
||
if (val !== '' && val != new_val) {
|
||
query(this.el).val(new_val)
|
||
// cancel event
|
||
event.stopPropagation()
|
||
event.preventDefault()
|
||
return false
|
||
}
|
||
}
|
||
// color
|
||
if (this.type === 'color') {
|
||
let color = query(this.el).val()
|
||
if (color.substr(0, 3).toLowerCase() !== 'rgb') {
|
||
color = '#' + color
|
||
let len = query(this.el).val().length
|
||
if (len !== 8 && len !== 6 && len !== 3) color = ''
|
||
}
|
||
let next = query(this.el).get(0).nextElementSibling
|
||
query(next).find('div').css('background-color', color)
|
||
if (query(this.el).hasClass('has-focus')) {
|
||
this.updateOverlay()
|
||
}
|
||
}
|
||
// list, enum
|
||
if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
|
||
this.refresh()
|
||
}
|
||
// date, time
|
||
if (['date', 'time', 'datetime'].indexOf(this.type) !== -1) {
|
||
// convert linux timestamps
|
||
let tmp = parseInt(this.el.value)
|
||
if (w2utils.isInt(this.el.value) && tmp > 3000) {
|
||
if (this.type === 'time') tmp = w2utils.formatTime(new Date(tmp), this.options.format)
|
||
if (this.type === 'date') tmp = w2utils.formatDate(new Date(tmp), this.options.format)
|
||
if (this.type === 'datetime') tmp = w2utils.formatDateTime(new Date(tmp), this.options.format)
|
||
query(this.el).val(tmp).trigger('input').trigger('change')
|
||
}
|
||
}
|
||
}
|
||
click(event) {
|
||
// lists
|
||
if (['list', 'combo', 'enum'].includes(this.type)) {
|
||
if (!query(this.el).hasClass('has-focus')) {
|
||
this.focus(event)
|
||
}
|
||
if (this.type == 'combo') {
|
||
this.updateOverlay()
|
||
}
|
||
// since list has separate search input, in order to keep the overlay open, need to stop
|
||
if (this.type == 'list') {
|
||
this.updateOverlay()
|
||
event.stopPropagation()
|
||
}
|
||
}
|
||
// other fields with drops
|
||
if (['date', 'time', 'datetime', 'color'].includes(this.type)) {
|
||
this.updateOverlay()
|
||
}
|
||
}
|
||
focus(event) {
|
||
if (this.type == 'list' && document.activeElement == this.el) {
|
||
this.helpers.search_focus.focus()
|
||
return
|
||
}
|
||
// color, date, time
|
||
if (['color', 'date', 'time', 'datetime'].indexOf(this.type) !== -1) {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
this.updateOverlay()
|
||
}
|
||
// menu
|
||
if (['list', 'combo', 'enum'].indexOf(this.type) !== -1) {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
|
||
// still add focus
|
||
query(this.el).addClass('has-focus')
|
||
return
|
||
}
|
||
// regenerate items
|
||
if (typeof this.options._items_fun == 'function') {
|
||
this.options.items = w2utils.normMenu.call(this, this.options._items_fun)
|
||
}
|
||
if (this.helpers.search) {
|
||
let search = this.helpers.search_focus
|
||
search.value = ''
|
||
search.select()
|
||
}
|
||
if (this.type == 'enum') {
|
||
// file control in particular need to receive focus after file select
|
||
let search = query(this.el.previousElementSibling).find('.li-search input').get(0)
|
||
if (document.activeElement !== search) {
|
||
search.focus()
|
||
}
|
||
}
|
||
this.resize()
|
||
// update overlay if needed
|
||
if (event.showMenu !== false && (this.options.openOnFocus !== false || query(this.el).hasClass('has-focus'))) {
|
||
setTimeout(() => { this.updateOverlay() }, 100) // execute at the end of event loop
|
||
}
|
||
}
|
||
if (this.type == 'file') {
|
||
let prev = query(this.el).get(0).previousElementSibling
|
||
query(prev).addClass('has-focus')
|
||
}
|
||
query(this.el).addClass('has-focus')
|
||
}
|
||
blur(event) {
|
||
let val = query(this.el).val().trim()
|
||
query(this.el).removeClass('has-focus')
|
||
if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
|
||
if (val !== '') {
|
||
let newVal = val
|
||
let error = ''
|
||
if (!this.isStrValid(val)) { // validity is also checked in blur
|
||
newVal = ''
|
||
} else {
|
||
let rVal = this.clean(val)
|
||
if (this.options.min != null && rVal < this.options.min) {
|
||
newVal = this.options.min
|
||
error = `Should be >= ${this.options.min}`
|
||
}
|
||
if (this.options.max != null && rVal > this.options.max) {
|
||
newVal = this.options.max
|
||
error = `Should be <= ${this.options.max}`
|
||
}
|
||
}
|
||
if (this.options.autoCorrect) {
|
||
query(this.el).val(newVal).trigger('input').trigger('change')
|
||
if (error) {
|
||
w2tooltip.show({
|
||
name: this.el.id + '_error',
|
||
anchor: this.el,
|
||
html: error
|
||
})
|
||
setTimeout(() => { w2tooltip.hide(this.el.id + '_error') }, 3000)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// date or time
|
||
if (['date', 'time', 'datetime'].includes(this.type) && this.options.autoCorrect) {
|
||
if (val !== '') {
|
||
let check = this.type == 'date' ? w2utils.isDate :
|
||
(this.type == 'time' ? w2utils.isTime : w2utils.isDateTime)
|
||
if (!w2date.inRange(this.el.value, this.options)
|
||
|| !check.bind(w2utils)(this.el.value, this.options.format)) {
|
||
// if not in range or wrong value - clear it
|
||
query(this.el).val('').trigger('input').trigger('change')
|
||
}
|
||
}
|
||
}
|
||
// clear search input
|
||
if (this.type === 'enum') {
|
||
query(this.helpers.multi).find('input').val('').css('width', '15px')
|
||
}
|
||
if (this.type == 'file') {
|
||
let prev = this.el.previousElementSibling
|
||
query(prev).removeClass('has-focus')
|
||
}
|
||
if (this.type === 'list') {
|
||
this.el.value = this.selected?.text ?? ''
|
||
}
|
||
}
|
||
keyDown(event, extra) {
|
||
let options = this.options
|
||
let key = event.keyCode || (extra && extra.keyCode)
|
||
let cancel = false
|
||
let val, inc, daymil, dt, newValue, newDT
|
||
// ignore wrong pressed key
|
||
if (['int', 'float', 'money', 'currency', 'percent', 'hex', 'bin', 'color', 'alphanumeric'].includes(this.type)) {
|
||
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
||
if (!this.isStrValid(event.key ?? '1', true) && // valid & is not arrows, dot, comma, etc keys
|
||
![9, 8, 13, 27, 37, 38, 39, 40, 46].includes(event.keyCode)) {
|
||
event.preventDefault()
|
||
if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
// numeric
|
||
if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
|
||
if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
val = parseFloat(query(this.el).val().replace(options.moneyRE, '')) || 0
|
||
inc = options.step
|
||
if (event.ctrlKey || event.metaKey) inc = options.step * 10
|
||
switch (key) {
|
||
case 38: // up
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
newValue = (val + inc <= options.max || options.max == null ? Number((val + inc).toFixed(12)) : options.max)
|
||
query(this.el).val(newValue).trigger('input').trigger('change')
|
||
cancel = true
|
||
break
|
||
case 40: // down
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
newValue = (val - inc >= options.min || options.min == null ? Number((val - inc).toFixed(12)) : options.min)
|
||
query(this.el).val(newValue).trigger('input').trigger('change')
|
||
cancel = true
|
||
break
|
||
}
|
||
if (cancel) {
|
||
event.preventDefault()
|
||
this.moveCaret2end()
|
||
}
|
||
}
|
||
// date/datetime
|
||
if (['date', 'datetime'].includes(this.type)) {
|
||
if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
let is = (this.type == 'date' ? w2utils.isDate : w2utils.isDateTime).bind(w2utils)
|
||
let format = (this.type == 'date' ? w2utils.formatDate : w2utils.formatDateTime).bind(w2utils)
|
||
daymil = 24*60*60*1000
|
||
inc = 1
|
||
if (event.ctrlKey || event.metaKey) inc = 10 // by month
|
||
dt = is(query(this.el).val(), options.format, true)
|
||
if (!dt) { dt = new Date(); daymil = 0 }
|
||
switch (key) {
|
||
case 38: // up
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
if (inc == 10) {
|
||
dt.setMonth(dt.getMonth() + 1)
|
||
} else {
|
||
dt.setTime(dt.getTime() + daymil)
|
||
}
|
||
newDT = format(dt.getTime(), options.format)
|
||
query(this.el).val(newDT).trigger('input').trigger('change')
|
||
cancel = true
|
||
break
|
||
case 40: // down
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
if (inc == 10) {
|
||
dt.setMonth(dt.getMonth() - 1)
|
||
} else {
|
||
dt.setTime(dt.getTime() - daymil)
|
||
}
|
||
newDT = format(dt.getTime(), options.format)
|
||
query(this.el).val(newDT).trigger('input').trigger('change')
|
||
cancel = true
|
||
break
|
||
}
|
||
if (cancel) {
|
||
event.preventDefault()
|
||
this.moveCaret2end()
|
||
this.updateOverlay()
|
||
}
|
||
}
|
||
// time
|
||
if (this.type === 'time') {
|
||
if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
inc = (event.ctrlKey || event.metaKey ? 60 : 1)
|
||
val = query(this.el).val()
|
||
let time = w2date.str2min(val) || w2date.str2min((new Date()).getHours() + ':' + ((new Date()).getMinutes() - 1))
|
||
switch (key) {
|
||
case 38: // up
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
time += inc
|
||
cancel = true
|
||
break
|
||
case 40: // down
|
||
if (event.shiftKey) break // no action if shift key is pressed
|
||
time -= inc
|
||
cancel = true
|
||
break
|
||
}
|
||
if (cancel) {
|
||
event.preventDefault()
|
||
query(this.el).val(w2date.min2str(time)).trigger('input').trigger('change')
|
||
this.moveCaret2end()
|
||
}
|
||
}
|
||
// list/enum
|
||
if (['list', 'enum'].includes(this.type)) {
|
||
switch (key) {
|
||
case 8: // delete
|
||
case 46: // backspace
|
||
if (this.type == 'list') {
|
||
let search = query(this.helpers.search_focus)
|
||
if (search.val() == '') {
|
||
this.selected = null
|
||
w2menu.hide(this.el.id + '_menu')
|
||
query(this.el).val('').trigger('input').trigger('change')
|
||
}
|
||
} else {
|
||
let search = query(this.helpers.multi).find('input')
|
||
if (search.val() == '') {
|
||
w2menu.hide(this.el.id + '_menu')
|
||
this.selected.pop()
|
||
// update selected array in overlay
|
||
let overlay = w2menu.get(this.el.id + '_menu')
|
||
if (overlay) overlay.options.selected = this.selected
|
||
this.refresh()
|
||
}
|
||
}
|
||
break
|
||
case 9: // tab key
|
||
case 16: // shift key (when shift+tab)
|
||
break
|
||
case 27: // escape
|
||
w2menu.hide(this.el.id + '_menu')
|
||
this.refresh()
|
||
break
|
||
default: {
|
||
// let overlay = w2menu.get(this.el.id + '_menu')
|
||
// if (!overlay && !overlay?.displayed) {
|
||
// this.updateOverlay()
|
||
// }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
keyUp(event) {
|
||
if (this.type == 'list') {
|
||
let search = query(this.helpers.search_focus)
|
||
if (search.val() !== '') {
|
||
query(this.el).attr('placeholder', '')
|
||
} else {
|
||
query(this.el).attr('placeholder', this.tmp.pholder)
|
||
}
|
||
if (event.keyCode == 13) {
|
||
setTimeout(() => {
|
||
search.val('')
|
||
w2menu.hide(this.el.id + '_menu')
|
||
this.refresh()
|
||
}, 1)
|
||
} else {
|
||
// tab, shift+tab, esc, delete, backspace
|
||
if ([8, 9, 16, 27, 46].includes(event.keyCode)) {
|
||
w2menu.hide(this.el.id + '_menu')
|
||
} else {
|
||
this.updateOverlay()
|
||
}
|
||
}
|
||
this.refresh()
|
||
}
|
||
if (this.type == 'combo') {
|
||
this.updateOverlay()
|
||
}
|
||
if (this.type == 'enum') {
|
||
let search = this.helpers.multi.find('input')
|
||
let styles = getComputedStyle(search.get(0))
|
||
let width = w2utils.getStrWidth(search.val(),
|
||
`font-family: ${styles['font-family']}; font-size: ${styles['font-size']};`)
|
||
search.css({ width: (width + 15) + 'px' })
|
||
this.resize()
|
||
}
|
||
}
|
||
findItemIndex(items, id, parents) {
|
||
let inds = []
|
||
if (!parents) parents = []
|
||
items.forEach((item, ind) => {
|
||
if (item.id === id) {
|
||
inds = parents.concat([ind])
|
||
this.options.index = [ind]
|
||
}
|
||
if (inds.length == 0 && item.items && item.items.length > 0) {
|
||
parents.push(ind)
|
||
inds = this.findItemIndex(item.items, id, parents)
|
||
parents.pop()
|
||
}
|
||
})
|
||
return inds
|
||
}
|
||
updateOverlay(indexOnly) {
|
||
let options = this.options
|
||
let params
|
||
// color
|
||
if (this.type === 'color') {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
w2color.show(w2utils.extend({
|
||
name: this.el.id + '_color',
|
||
anchor: this.el,
|
||
transparent: options.transparent,
|
||
advanced: options.advanced,
|
||
color: this.el.value,
|
||
liveUpdate: true
|
||
}, this.options))
|
||
.select(event => {
|
||
let color = event.detail.color
|
||
query(this.el).val(color).trigger('input').trigger('change')
|
||
})
|
||
.liveUpdate(event => {
|
||
let color = event.detail.color
|
||
query(this.helpers.suffix).find(':scope > div').css('background-color', '#' + color)
|
||
})
|
||
}
|
||
// list
|
||
if (['list', 'combo', 'enum'].includes(this.type)) {
|
||
let el = this.el
|
||
let input = this.el
|
||
if (this.type === 'enum') {
|
||
el = this.helpers.multi.get(0)
|
||
input = query(el).find('input').get(0)
|
||
}
|
||
if (this.type === 'list') {
|
||
let sel = this.selected
|
||
if (w2utils.isPlainObject(sel) && Object.keys(sel).length > 0) {
|
||
let ind = this.findItemIndex(options.items, sel.id)
|
||
if (ind.length > 0) {
|
||
options.index = ind
|
||
}
|
||
}
|
||
input = this.helpers.search_focus
|
||
}
|
||
if (query(this.el).hasClass('has-focus') && !this.el.readOnly && !this.el.disabled) {
|
||
let msgNoItems = w2utils.lang(options.msgNoItems)
|
||
if (options.url != null && String(query(input).val()).length < options.minLength && this.tmp.emptySet !== true) {
|
||
msgNoItems = w2utils.lang('${count} letters or more...', { count: options.minLength })
|
||
}
|
||
if (options.url != null && query(input).val() === '' && this.tmp.emptySet !== true) {
|
||
msgNoItems = w2utils.lang(options.msgSearch)
|
||
}
|
||
// TODO: remote url
|
||
// if (options.url == null && options.items.length === 0) msgNoItems = w2utils.lang('Empty list')
|
||
// if (options.msgNoItems != null) {
|
||
// let eventData = {
|
||
// search: query(input).val(),
|
||
// options: w2utils.clone(options)
|
||
// }
|
||
// if (options.url) {
|
||
// eventData.remote = {
|
||
// url: options.url,
|
||
// empty: this.tmp.emptySet ? true : false,
|
||
// error: this.tmp.lastError,
|
||
// minLength: options.minLength
|
||
// }
|
||
// }
|
||
// msgNoItems = (typeof options.msgNoItems === 'function'
|
||
// ? options.msgNoItems(eventData)
|
||
// : options.msgNoItems)
|
||
// }
|
||
// if (this.tmp.lastError) {
|
||
// msgNoItems = this.tmp.lastError
|
||
// }
|
||
// if (msgNoItems) {
|
||
// msgNoItems = '<div class="no-matches" style="white-space: normal; line-height: 1.3">' + msgNoItems + '</div>'
|
||
// }
|
||
params = w2utils.extend({}, options, {
|
||
name: this.el.id + '_menu',
|
||
anchor: input,
|
||
selected: this.selected,
|
||
search: false,
|
||
render: options.renderDrop,
|
||
anchorClass: '',
|
||
offsetY: 5,
|
||
maxHeight: options.maxDropHeight, // TODO: check
|
||
maxWidth: options.maxDropWidth, // TODO: check
|
||
minWidth: options.minDropWidth, // TODO: check
|
||
msgNoItems: msgNoItems,
|
||
})
|
||
this.tmp.overlay = w2menu.show(params)
|
||
.select(event => {
|
||
if (['list', 'combo'].includes(this.type)) {
|
||
this.selected = event.detail.item
|
||
query(input).val('')
|
||
query(this.el).val(this.selected.text).trigger('input').trigger('change')
|
||
this.focus({ showMenu: false })
|
||
} else {
|
||
let selected = this.selected
|
||
let newItem = event.detail?.item
|
||
if (newItem) {
|
||
// trigger event
|
||
let edata = this.trigger('add', { target: this.el, item: newItem, originalEvent: event })
|
||
if (edata.isCancelled === true) return
|
||
// default behavior
|
||
if (selected.length >= options.max && options.max > 0) selected.pop()
|
||
delete newItem.hidden
|
||
selected.push(newItem)
|
||
query(this.el).trigger('input').trigger('change')
|
||
query(this.helpers.multi).find('input').val('')
|
||
// updaet selected array in overlays
|
||
let overlay = w2menu.get(this.el.id + '_menu')
|
||
if (overlay) overlay.options.selected = this.selected
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
// date
|
||
if (['date', 'time', 'datetime'].includes(this.type)) {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
w2date.show(w2utils.extend({
|
||
name: this.el.id + '_date',
|
||
anchor: this.el,
|
||
value: this.el.value,
|
||
}, this.options))
|
||
.select(event => {
|
||
let date = event.detail.date
|
||
if (date != null) {
|
||
query(this.el).val(date).trigger('input').trigger('change')
|
||
}
|
||
})
|
||
}
|
||
}
|
||
/*
|
||
* INTERNAL FUNCTIONS
|
||
*/
|
||
isStrValid(ch, loose) {
|
||
let isValid = true
|
||
switch (this.type) {
|
||
case 'int':
|
||
if (loose && ['-', this.options.groupSymbol].includes(ch)) {
|
||
isValid = true
|
||
} else {
|
||
isValid = w2utils.isInt(ch.replace(this.options.numberRE, ''))
|
||
}
|
||
break
|
||
case 'percent':
|
||
ch = ch.replace(/%/g, '')
|
||
case 'float':
|
||
if (loose && ['-', '', this.options.decimalSymbol, this.options.groupSymbol].includes(ch)) {
|
||
isValid = true
|
||
} else {
|
||
isValid = w2utils.isFloat(ch.replace(this.options.numberRE, ''))
|
||
}
|
||
break
|
||
case 'money':
|
||
case 'currency':
|
||
if (loose && ['-', this.options.decimalSymbol, this.options.groupSymbol, this.options.currencyPrefix,
|
||
this.options.currencySuffix].includes(ch)) {
|
||
isValid = true
|
||
} else {
|
||
isValid = w2utils.isFloat(ch.replace(this.options.moneyRE, ''))
|
||
}
|
||
break
|
||
case 'bin':
|
||
isValid = w2utils.isBin(ch)
|
||
break
|
||
case 'color':
|
||
case 'hex':
|
||
isValid = w2utils.isHex(ch)
|
||
break
|
||
case 'alphanumeric':
|
||
isValid = w2utils.isAlphaNumeric(ch)
|
||
break
|
||
}
|
||
return isValid
|
||
}
|
||
addPrefix() {
|
||
if (!this.options.prefix) {
|
||
return
|
||
}
|
||
let helper
|
||
let styles = getComputedStyle(this.el)
|
||
if (this.tmp['old-padding-left'] == null) {
|
||
this.tmp['old-padding-left'] = styles['padding-left']
|
||
}
|
||
// remove if already displayed
|
||
if (this.helpers.prefix) query(this.helpers.prefix).remove()
|
||
query(this.el).before(`<div class="w2ui-field-helper">${this.options.prefix}</div>`)
|
||
helper = query(this.el).get(0).previousElementSibling
|
||
query(helper)
|
||
.css({
|
||
'color' : styles.color,
|
||
'font-family' : styles['font-family'],
|
||
'font-size' : styles['font-size'],
|
||
'height' : this.el.clientHeight + 'px',
|
||
'padding-top' : styles['padding-top'],
|
||
'padding-bottom' : styles['padding-bottom'],
|
||
'padding-left' : this.tmp['old-padding-left'],
|
||
'padding-right' : 0,
|
||
'margin-top' : (parseInt(styles['margin-top'], 10) + 2) + 'px',
|
||
'margin-bottom' : (parseInt(styles['margin-bottom'], 10) + 1) + 'px',
|
||
'margin-left' : styles['margin-left'],
|
||
'margin-right' : 0,
|
||
'z-index' : 1,
|
||
})
|
||
// only if visible
|
||
query(this.el).css('padding-left', helper.clientWidth + 'px !important')
|
||
// remember helper
|
||
this.helpers.prefix = helper
|
||
}
|
||
addSuffix() {
|
||
if (!this.options.suffix && !this.options.arrow) {
|
||
return
|
||
}
|
||
let helper
|
||
let self = this
|
||
let styles = getComputedStyle(this.el)
|
||
if (this.tmp['old-padding-right'] == null) {
|
||
this.tmp['old-padding-right'] = styles['padding-right']
|
||
}
|
||
let pr = parseInt(styles['padding-right'] || 0)
|
||
if (this.options.arrow) {
|
||
// remove if already displayed
|
||
if (this.helpers.arrow) query(this.helpers.arrow).remove()
|
||
// add fresh
|
||
query(this.el).after(
|
||
'<div class="w2ui-field-helper" style="border: 1px solid transparent"> '+
|
||
' <div class="w2ui-field-up" type="up">'+
|
||
' <div class="arrow-up" type="up"></div>'+
|
||
' </div>'+
|
||
' <div class="w2ui-field-down" type="down">'+
|
||
' <div class="arrow-down" type="down"></div>'+
|
||
' </div>'+
|
||
'</div>')
|
||
helper = query(this.el).get(0).nextElementSibling
|
||
query(helper).css({
|
||
'color' : styles.color,
|
||
'font-family' : styles['font-family'],
|
||
'font-size' : styles['font-size'],
|
||
'height' : this.el.clientHeight + 'px',
|
||
'padding' : 0,
|
||
'margin-top' : (parseInt(styles['margin-top'], 10) + 1) + 'px',
|
||
'margin-bottom' : 0,
|
||
'border-left' : '1px solid silver',
|
||
'width' : '16px',
|
||
'transform' : 'translateX(-100%)'
|
||
})
|
||
.on('mousedown', function(event) {
|
||
if (query(event.target).hasClass('arrow-up')) {
|
||
self.keyDown(event, { keyCode: 38 })
|
||
}
|
||
if (query(event.target).hasClass('arrow-down')) {
|
||
self.keyDown(event, { keyCode: 40 })
|
||
}
|
||
})
|
||
pr += helper.clientWidth // width of the control
|
||
query(this.el).css('padding-right', pr + 'px !important')
|
||
this.helpers.arrow = helper
|
||
}
|
||
if (this.options.suffix !== '') {
|
||
// remove if already displayed
|
||
if (this.helpers.suffix) query(this.helpers.suffix).remove()
|
||
// add fresh
|
||
query(this.el).after(`<div class="w2ui-field-helper">${this.options.suffix}</div>`)
|
||
helper = query(this.el).get(0).nextElementSibling
|
||
query(helper)
|
||
.css({
|
||
'color' : styles.color,
|
||
'font-family' : styles['font-family'],
|
||
'font-size' : styles['font-size'],
|
||
'height' : this.el.clientHeight + 'px',
|
||
'padding-top' : styles['padding-top'],
|
||
'padding-bottom' : styles['padding-bottom'],
|
||
'padding-left' : 0,
|
||
'padding-right' : styles['padding-right'],
|
||
'margin-top' : (parseInt(styles['margin-top'], 10) + 2) + 'px',
|
||
'margin-bottom' : (parseInt(styles['margin-bottom'], 10) + 1) + 'px',
|
||
'transform' : 'translateX(-100%)'
|
||
})
|
||
query(this.el).css('padding-right', helper.clientWidth + 'px !important')
|
||
this.helpers.suffix = helper
|
||
}
|
||
}
|
||
// Only used for list
|
||
addSearch() {
|
||
if (this.type !== 'list') return
|
||
// clean up & init
|
||
if (this.helpers.search) query(this.helpers.search).remove()
|
||
// remember original tabindex
|
||
let tabIndex = parseInt(query(this.el).attr('tabIndex'))
|
||
if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex
|
||
if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex']
|
||
if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0
|
||
// if there is id, add to search with "_search"
|
||
let searchId = ''
|
||
if (query(this.el).attr('id') != null) {
|
||
searchId = 'id="' + query(this.el).attr('id') + '_search"'
|
||
}
|
||
// build helper
|
||
let html = `
|
||
<div class="w2ui-field-helper">
|
||
<span class="w2ui-icon w2ui-icon-search"></span>
|
||
<input ${searchId} type="text" tabIndex="${tabIndex}" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"/>
|
||
</div>`
|
||
query(this.el).attr('tabindex', -1).before(html)
|
||
let helper = query(this.el).get(0).previousElementSibling
|
||
this.helpers.search = helper
|
||
this.helpers.search_focus = query(helper).find('input').get(0)
|
||
let styles = getComputedStyle(this.el)
|
||
query(helper).css({
|
||
width : this.el.clientWidth + 'px',
|
||
'margin-top' : styles['margin-top'],
|
||
'margin-left' : styles['margin-left'],
|
||
'margin-bottom' : styles['margin-bottom'],
|
||
'margin-right' : styles['margin-right']
|
||
})
|
||
.find('input')
|
||
.css({
|
||
cursor : 'default',
|
||
width : '100%',
|
||
opacity : 1,
|
||
padding : styles.padding,
|
||
margin : styles.margin,
|
||
border : '1px solid transparent',
|
||
'background-color' : 'transparent'
|
||
})
|
||
// INPUT events
|
||
query(helper).find('input')
|
||
.off('.helper')
|
||
.on('focus.helper', event => {
|
||
query(event.target).val('')
|
||
this.tmp.pholder = query(this.el).attr('placeholder') ?? ''
|
||
this.focus(event)
|
||
event.stopPropagation()
|
||
})
|
||
.on('blur.helper', event => {
|
||
query(event.target).val('')
|
||
if (this.tmp.pholder != null) query(this.el).attr('placeholder', this.tmp.pholder)
|
||
this.blur(event)
|
||
event.stopPropagation()
|
||
})
|
||
.on('keydown.helper', event => { this.keyDown(event) })
|
||
.on('keyup.helper', event => { this.keyUp(event) })
|
||
// MAIN div
|
||
query(helper).on('click', event => {
|
||
query(event.target).find('input').focus()
|
||
})
|
||
}
|
||
// Used in enum/file
|
||
addMultiSearch() {
|
||
if (!['enum', 'file'].includes(this.type)) {
|
||
return
|
||
}
|
||
// clean up & init
|
||
query(this.helpers.multi).remove()
|
||
// build helper
|
||
let html = ''
|
||
let styles = getComputedStyle(this.el)
|
||
let margin = w2utils.stripSpaces(`
|
||
margin-top: 0px;
|
||
margin-bottom: 0px;
|
||
margin-left: ${styles['margin-left']};
|
||
margin-right: ${styles['margin-right']};
|
||
width: ${(w2utils.getSize(this.el, 'width') - parseInt(styles['margin-left'], 10)
|
||
- parseInt(styles['margin-right'], 10))}px;
|
||
`)
|
||
if (this.tmp['min-height'] == null) {
|
||
let min = this.tmp['min-height'] = parseInt((styles['min-height'] != 'none' ? styles['min-height'] : 0) || 0)
|
||
let current = parseInt(styles.height)
|
||
this.tmp['min-height'] = Math.max(min, current)
|
||
}
|
||
if (this.tmp['max-height'] == null && styles['max-height'] != 'none') {
|
||
this.tmp['max-height'] = parseInt(styles['max-height'])
|
||
}
|
||
// if there is id, add to search with "_search"
|
||
let searchId = ''
|
||
if (query(this.el).attr('id') != null) {
|
||
searchId = `id="${query(this.el).attr('id')}_search"`
|
||
}
|
||
// remember original tabindex
|
||
let tabIndex = parseInt(query(this.el).attr('tabIndex'))
|
||
if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex
|
||
if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex']
|
||
if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0
|
||
if (this.type === 'enum') {
|
||
html = `
|
||
<div class="w2ui-field-helper w2ui-list" style="${margin}">
|
||
<div class="w2ui-multi-items">
|
||
<div class="li-search">
|
||
<input ${searchId} type="text" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
|
||
tabindex="${tabIndex}"
|
||
${query(this.el).prop('readOnly') ? 'readonly': '' }
|
||
${query(this.el).prop('disabled') ? 'disabled': '' }>
|
||
</div>
|
||
</div>
|
||
</div>`
|
||
}
|
||
if (this.type === 'file') {
|
||
html = `
|
||
<div class="w2ui-field-helper w2ui-list" style="${margin}">
|
||
<div class="w2ui-multi-file">
|
||
<input name="attachment" class="file-input" type="file" tabindex="-1"'
|
||
style="width: 100%; height: 100%; opacity: 0" title=""
|
||
${this.options.max !== 1 ? 'multiple' : ''}
|
||
${query(this.el).prop('readOnly') || query(this.el).prop('disabled') ? 'disabled': ''}
|
||
${query(this.el).attr('accept') ? ' accept="'+ query(this.el).attr('accept') +'"': ''}>
|
||
</div>
|
||
<div class="w2ui-multi-items">
|
||
<div class="li-search" style="display: none">
|
||
<input ${searchId} type="text" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
|
||
tabindex="${tabIndex}"
|
||
${query(this.el).prop('readOnly') ? 'readonly': '' }
|
||
${query(this.el).prop('disabled') ? 'disabled': '' }>
|
||
</div>
|
||
</div>
|
||
</div>`
|
||
}
|
||
// old bg and border
|
||
this.tmp['old-background-color'] = styles['background-color']
|
||
this.tmp['old-border-color'] = styles['border-color']
|
||
query(this.el)
|
||
.before(html)
|
||
.css({
|
||
'border-color': 'transparent',
|
||
'background-color': 'transparent'
|
||
})
|
||
let div = query(this.el.previousElementSibling)
|
||
this.helpers.multi = div
|
||
query(this.el).attr('tabindex', -1)
|
||
// click anywhere on the field
|
||
div.on('click', event => { this.focus(event) })
|
||
// search field
|
||
div.find('input:not(.file-input)')
|
||
.on('click', event => { this.click(event) })
|
||
.on('focus', event => { this.focus(event) })
|
||
.on('blur', event => { this.blur(event) })
|
||
.on('keydown', event => { this.keyDown(event) })
|
||
.on('keyup', event => { this.keyUp(event) })
|
||
// file input
|
||
if (this.type === 'file') {
|
||
div.find('input.file-input')
|
||
.off('.drag')
|
||
.on('click.drag', (event) => {
|
||
event.stopPropagation()
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
this.focus(event)
|
||
})
|
||
.on('dragenter.drag', (event) => {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
div.addClass('w2ui-file-dragover')
|
||
})
|
||
.on('dragleave.drag', (event) => {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
div.removeClass('w2ui-file-dragover')
|
||
})
|
||
.on('drop.drag', (event) => {
|
||
if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
|
||
div.removeClass('w2ui-file-dragover')
|
||
let files = Array.from(event.dataTransfer.files)
|
||
files.forEach(file => { this.addFile(file) })
|
||
this.focus(event)
|
||
// cancel to stop browser behaviour
|
||
event.preventDefault()
|
||
event.stopPropagation()
|
||
})
|
||
.on('dragover.drag', (event) => {
|
||
// cancel to stop browser behaviour
|
||
event.preventDefault()
|
||
event.stopPropagation()
|
||
})
|
||
.on('change.drag', (event) => {
|
||
if (typeof event.target.files !== 'undefined') {
|
||
Array.from(event.target.files).forEach(file => { this.addFile(file) })
|
||
}
|
||
this.focus(event)
|
||
})
|
||
}
|
||
this.refresh()
|
||
}
|
||
addFile(file) {
|
||
let options = this.options
|
||
let selected = this.selected
|
||
let newItem = {
|
||
name : file.name,
|
||
type : file.type,
|
||
modified : file.lastModifiedDate,
|
||
size : file.size,
|
||
content : null,
|
||
file : file
|
||
}
|
||
let size = 0
|
||
let cnt = 0
|
||
let errors = []
|
||
if (Array.isArray(selected)) {
|
||
selected.forEach(item => {
|
||
if (item.name == file.name && item.size == file.size) {
|
||
errors.push(w2utils.lang('The file "${name}" (${size}) is already added.', {
|
||
name: file.name, size: w2utils.formatSize(file.size) }))
|
||
}
|
||
size += item.size
|
||
cnt++
|
||
})
|
||
}
|
||
if (options.maxFileSize !== 0 && newItem.size > options.maxFileSize) {
|
||
errors.push(w2utils.lang('Maximum file size is ${size}', { size: w2utils.formatSize(options.maxFileSize) }))
|
||
}
|
||
if (options.maxSize !== 0 && size + newItem.size > options.maxSize) {
|
||
errors.push(w2utils.lang('Maximum total size is ${size}', { size: w2utils.formatSize(options.maxSize) }))
|
||
}
|
||
if (options.max !== 0 && cnt >= options.max) {
|
||
errors.push(w2utils.lang('Maximum number of files is ${count}', { count: options.max }))
|
||
}
|
||
// trigger event
|
||
let edata = this.trigger('add', { target: this.el, file: newItem, total: cnt, totalSize: size, errors })
|
||
if (edata.isCancelled === true) return
|
||
// if errors and not silent
|
||
if (options.silent !== true && errors.length > 0) {
|
||
w2tooltip.show({
|
||
anchor: this.el,
|
||
html: 'Errors: ' + errors.join('<br>')
|
||
})
|
||
console.log('ERRORS (while adding files): ', errors)
|
||
return
|
||
}
|
||
// check params
|
||
selected.push(newItem)
|
||
// read file as base64
|
||
if (typeof FileReader !== 'undefined' && options.readContent === true) {
|
||
let reader = new FileReader()
|
||
let self = this
|
||
// need a closure
|
||
reader.onload = (function onload() {
|
||
return function closure(event) {
|
||
let fl = event.target.result
|
||
let ind = fl.indexOf(',')
|
||
newItem.content = fl.substr(ind + 1)
|
||
self.refresh()
|
||
query(self.el).trigger('input').trigger('change')
|
||
// event after
|
||
edata.finish()
|
||
}
|
||
})()
|
||
reader.readAsDataURL(file)
|
||
} else {
|
||
this.refresh()
|
||
query(this.el).trigger('input').trigger('change')
|
||
edata.finish()
|
||
}
|
||
}
|
||
// move cursror to end
|
||
moveCaret2end() {
|
||
setTimeout(() => {
|
||
this.el.setSelectionRange(this.el.value.length, this.el.value.length)
|
||
}, 0)
|
||
}
|
||
}
|
||
/**
|
||
* Part of w2ui 2.0 library
|
||
* - Dependencies: jQuery, w2ui.*
|
||
*
|
||
* This file provided compatibility for projects that conntinue to use jQuery. It extends jQuery with
|
||
* w2ui support, such as fn.w2grid, fn.w2form, ... fn.w2render, fn.w2destroy, fn.w2tag, etc
|
||
*
|
||
* It is not needed for projects that use ES6 module loading.
|
||
*
|
||
* == 2.0 changes
|
||
* - CSP - fixed inline events
|
||
*/
|
||
|
||
// Register jQuery plugins
|
||
(function($) {
|
||
// register globals if needed
|
||
let w2globals = function() {
|
||
(function (win, obj) {
|
||
Object.keys(obj).forEach(key => {
|
||
win[key] = obj[key]
|
||
})
|
||
})(window, {
|
||
w2ui, w2utils, query, w2locale, w2event, w2base,
|
||
w2popup, w2alert, w2confirm, w2prompt, Dialog,
|
||
w2tooltip, w2menu, w2color, w2date, Tooltip,
|
||
w2toolbar, w2sidebar, w2tabs, w2layout, w2grid, w2form, w2field
|
||
})
|
||
}
|
||
// if url has globals at the end, then register globals
|
||
let param = String(undefined).split('?')[1] || ''
|
||
if (param == 'globals' || param.substr(0, 8) == 'globals=') {
|
||
w2globals()
|
||
}
|
||
// if jQuery is not defined, then exit
|
||
if (!$) return
|
||
$.w2globals = w2globals
|
||
$.fn.w2render = function(name) {
|
||
if ($(this).length > 0) {
|
||
if (typeof name === 'string' && w2ui[name]) w2ui[name].render($(this)[0])
|
||
if (typeof name === 'object') name.render($(this)[0])
|
||
}
|
||
}
|
||
$.fn.w2destroy = function(name) {
|
||
if (!name && this.length > 0) name = this.attr('name')
|
||
if (typeof name === 'string' && w2ui[name]) w2ui[name].destroy()
|
||
if (typeof name === 'object') name.destroy()
|
||
}
|
||
$.fn.w2field = function(type, options) {
|
||
// if without arguments - return the object
|
||
if (arguments.length === 0) {
|
||
let obj = $(this).data('w2field')
|
||
return obj
|
||
}
|
||
return this.each((index, el) => {
|
||
let obj = $(el).data('w2field')
|
||
// if object is not defined, define it
|
||
if (obj == null) {
|
||
obj = new w2field(type, options)
|
||
obj.render(el)
|
||
return obj
|
||
} else { // fully re-init
|
||
obj = new w2field(type, options)
|
||
obj.render(el)
|
||
return obj
|
||
}
|
||
return null
|
||
})
|
||
}
|
||
$.fn.w2form = function(options) { return proc.call(this, options, 'w2form') }
|
||
$.fn.w2grid = function(options) { return proc.call(this, options, 'w2grid') }
|
||
$.fn.w2layout = function(options) { return proc.call(this, options, 'w2layout') }
|
||
$.fn.w2sidebar = function(options) { return proc.call(this, options, 'w2sidebar') }
|
||
$.fn.w2tabs = function(options) { return proc.call(this, options, 'w2tabs') }
|
||
$.fn.w2toolbar = function(options) { return proc.call(this, options, 'w2toolbar') }
|
||
function proc(options, type) {
|
||
if ($.isPlainObject(options)) {
|
||
let obj
|
||
if (type == 'w2form') {
|
||
obj = new w2form(options)
|
||
if (this.find('.w2ui-field').length > 0) {
|
||
obj.formHTML = this.html()
|
||
}
|
||
}
|
||
if (type == 'w2grid') obj = new w2grid(options)
|
||
if (type == 'w2layout') obj = new w2layout(options)
|
||
if (type == 'w2sidebar') obj = new w2sidebar(options)
|
||
if (type == 'w2tabs') obj = new w2tabs(options)
|
||
if (type == 'w2toolbar') obj = new w2toolbar(options)
|
||
if ($(this).length !== 0) {
|
||
obj.render(this[0])
|
||
}
|
||
return obj
|
||
} else {
|
||
let obj = w2ui[$(this).attr('name')]
|
||
if (!obj) return null
|
||
if (arguments.length > 0) {
|
||
if (obj[options]) obj[options].apply(obj, Array.prototype.slice.call(arguments, 1))
|
||
return this
|
||
} else {
|
||
return obj
|
||
}
|
||
}
|
||
}
|
||
$.fn.w2popup = function(options) {
|
||
if (this.length > 0 ) {
|
||
w2popup.template(this[0], null, options)
|
||
} else if (options.url) {
|
||
w2popup.load(options)
|
||
}
|
||
}
|
||
$.fn.w2marker = function() {
|
||
let str = Array.from(arguments)
|
||
if (Array.isArray(str[0])) str = str[0]
|
||
return $(this).each((index, el) => {
|
||
w2utils.marker(el, str)
|
||
})
|
||
}
|
||
$.fn.w2tag = function(text, options) {
|
||
return this.each((index, el) => {
|
||
if (text == null && options == null) {
|
||
w2tooltip.hide()
|
||
return
|
||
}
|
||
if (typeof text == 'object') {
|
||
options = text
|
||
} else {
|
||
options = options ?? {}
|
||
options.html = text
|
||
}
|
||
w2tooltip.show(el, options)
|
||
})
|
||
}
|
||
$.fn.w2overlay = function(html, options) {
|
||
return this.each((index, el) => {
|
||
if (html == null && options == null) {
|
||
w2tooltip.hide()
|
||
return
|
||
}
|
||
if (typeof html == 'object') {
|
||
options = html
|
||
} else {
|
||
options.html = html
|
||
}
|
||
Object.assign(options, {
|
||
class: 'w2ui-white',
|
||
hideOn: ['doc-click']
|
||
})
|
||
w2tooltip.show(el, options)
|
||
})
|
||
}
|
||
$.fn.w2menu = function(menu, options) {
|
||
return this.each((index, el) => {
|
||
if (typeof menu == 'object') {
|
||
options = menu
|
||
}
|
||
if (typeof menu == 'object') {
|
||
options = menu
|
||
} else {
|
||
options.items = menu
|
||
}
|
||
w2menu.show(el, options)
|
||
})
|
||
}
|
||
$.fn.w2color = function(options, callBack) {
|
||
return this.each((index, el) => {
|
||
let tooltip = w2color.show(el, options)
|
||
if (typeof callBack == 'function') {
|
||
tooltip.select(callBack)
|
||
}
|
||
})
|
||
}
|
||
})(window.jQuery)
|
||
|
||
// Compatibility with CommonJS and AMD modules
|
||
!(function(global, w2ui) {
|
||
if (typeof define == 'function' && define.amd) {
|
||
return define(() => w2ui)
|
||
}
|
||
if (typeof exports != 'undefined') {
|
||
if (typeof module != 'undefined' && module.exports) {
|
||
return exports = module.exports = w2ui
|
||
}
|
||
global = exports
|
||
}
|
||
if (global) {
|
||
Object.keys(w2ui).forEach(key => {
|
||
global[key] = w2ui[key]
|
||
})
|
||
}
|
||
})(self, {
|
||
w2ui, w2utils, query, w2locale, w2event, w2base,
|
||
w2popup, w2alert, w2confirm, w2prompt, Dialog,
|
||
w2tooltip, w2menu, w2color, w2date, Tooltip,
|
||
w2toolbar, w2sidebar, w2tabs, w2layout, w2grid, w2form, w2field
|
||
}) |