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
    }
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
}