 


/*
 Method: jsonAppObj
Description: Functionality for jsonAppObj.
@param {any} options - Description for options
@returns {any} - Result of the operation
 */
var jsonAppObj = function (options) {

    var self = this;
    console.log();

    this.json = {
        setup: {},
        blocks: {},  
        parts: {},

        functions: {},
        do: {},
        on: {},
        shortcuts: {},
        texts: {},
        data: {},
        var: {}
    };
    this.app = this.json;

    var alertObj = {};
    var alertValues = {};
    this.params = {};  

    this.javascriptReservedWord = ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', 'yield', 'Array', 'Date', 'eval', 'function', 'hasOwnProperty', 'Infinity', 'isFinite', 'isNaN', 'isPrototypeOf', 'length', 'Math', 'NaN', 'name', 'Number', 'Object', 'prototype', 'String', 'toString', 'undefined', 'valueOf', 'getClass', 'java', 'JavaArray', 'javaClass', 'JavaObject', 'JavaPackage', 'alert', 'all', 'anchor', 'anchors', 'area', 'assign', 'blur', 'button', 'checkbox', 'clearInterval', 'clearTimeout', 'clientInformation', 'close', 'closed', 'confirm', 'constructor', 'crypto', 'decodeURI', 'decodeURIComponent', 'defaultStatus', 'document', 'element', 'elements', 'embed', 'embeds', 'encodeURI', 'encodeURIComponent', 'escape', 'event', 'fileUpload', 'focus', 'form', 'forms', 'frame', 'innerHeight', 'innerWidth', 'layer', 'layers', 'link', 'location', 'mimeTypes', 'navigate', 'navigator', 'frames', 'frameRate', 'hidden', 'history', 'image', 'images', 'offscreenBuffering', 'open', 'opener', 'option', 'outerHeight', 'outerWidth', 'packages', 'pageXOffset', 'pageYOffset', 'parent', 'parseFloat', 'parseInt', 'password', 'pkcs11', 'plugin', 'prompt', 'propertyIsEnum', 'radio', 'reset', 'screenX', 'screenY', 'scroll', 'secure', 'select', 'self', 'setInterval', 'setTimeout', 'status', 'submit', 'taint', 'text', 'textarea', 'top', 'unescape', 'untaint', 'window', 'onblur', 'onclick', 'onerror', 'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', 'onmouseover', 'onload', 'onmouseup', 'onmousedown', 'onsubmit'];

    var nodes = {

        extend: ['setup', 'blocks', 'parts', 'css', 'actions', 'texts', 'data', 'functions'],  
        params: ['style', 'on', 'matchMedia', 'action', 'code', 'roles', 'plugins', 'css'],  
        exclude: ['setup', 'container', 'selector', 'info', 'note', 'comment', 'lang', 'tag', 'plugins', 'template']
    };

    const FORMAT_REGEX = {
        email: /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/,
        url: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
        ip: /^(?:(?: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]?)$/,

        date: /^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$/,

        color: /^#(?:[0-9a-fA-F]{3}){1,2}$|#(?:[0-9a-fA-F]{3,4}){1,2}$/,
    };

    
/*
 Method: jsonToSchema
Description: Functionality for jsonToSchema.
@param {any} json - Description for json
@param {any} options = {} - Description for options = {}
@returns {any} - Result of the operation
 */
this.jsonToSchema = function (json, options = {}) {

        if (typeof json === 'function') {
            throw new TypeError('Can not convert a function');
        }

        if (json === undefined) {
            return {};
        }

        if (typeof json === 'string') {

            if (FORMAT_REGEX.url.test(json)) return { type: 'string', format: 'url' };
            else if (FORMAT_REGEX.email.test(json)) return { type: 'string', format: 'email' };
            else if (FORMAT_REGEX.ip.test(json)) return { type: 'string', format: 'ip' };
            else if (FORMAT_REGEX.date.test(json)) return { type: 'string', format: 'date' };
            else if (FORMAT_REGEX.color.test(json)) return { type: 'string', format: 'color' };
            else return { type: 'string' };
        }

        else if (typeof json === 'boolean') return { type: 'boolean' };

        else if (typeof json === 'number') {
            if (Number.isInteger(json)) {
                return { type: 'integer' };
            } else {
                return { type: 'number' };
            }
        }

        else if (Array.isArray(json)) {
            let schema = { type: 'array' };

            if (!json.length) {
                schema.items = {};
                return schema;
            }

            let schemas = json.map(self.jsonToSchema);

            if (schemas.every(s => self.isEqual(s, schemas[0]))) {
                schema.items = schemas[0];

            } else {
                schema.items = { oneOf: unique(schemas) };
            }

            return schema;
        }

        if (json === null) {
            return { type: 'null' };
        }

        let schema = { type: 'object' };

        if (!Object.keys(json).length) {
            return schema;
        }

        schema.properties = Object.keys(json).reduce((properties, key) => {
            properties[key] = self.jsonToSchema(json[key]);
            return properties;
        }, {});

        return schema;
    };

    
/*
 Method: isEqual
Description: Functionality for isEqual.
@param {any} a - Description for a
@param {any} b - Description for b
@returns {any} - Result of the operation
 */
this.isEqual = function (a, b) {
        return (JSON.stringify(a) == JSON.stringify(b));
    };

    this.dataStorage = {
        _storage: new WeakMap(),
        set: function (element, key, obj) {
            if (!this._storage.has(element)) {
                this._storage.set(element, new Map());
            }
            this._storage.get(element).set(key, obj);
        },
        get: function (element, key) {
            return this._storage.get(element).get(key);
        },
        has: function (element, key) {
            return this._storage.has(element) && this._storage.get(element).has(key);
        },
        remove: function (element, key) {
            var ret = this._storage.get(element).delete(key);
            if (!this._storage.get(element).size === 0) {
                this._storage.delete(element);
            }
            return ret;
        }
    };

    
/*
 Method: extend
Description: Functionality for extend.
@param {any} out - Description for out
@returns {any} - Result of the operation
 */
this.extend = function (out) {

        if (typeof arguments[0] == 'boolean' && arguments[0] == true)
            return deepExtend(out);
        else {
            out = out || {};

            for (var i = 1; i < arguments.length; i++) {
                if (!arguments[i])
                    continue;

                for (var key in arguments[i]) {
                    if (arguments[i].hasOwnProperty(key))
                        out[key] = arguments[i][key];
                }
            }
            return out;
        }
    };

    
/*
 Method: deepExtend
Description: Functionality for deepExtend.
@param {any} out - Description for out
@returns {any} - Result of the operation
 */
var deepExtend = function (out) {
        out = out || {};

        for (var i = 1; i < arguments.length; i++) {
            var obj = arguments[i];

            if (!obj) continue;

            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    if (typeof obj[key] === "object" && obj[key] !== null) {
                        if (obj[key] instanceof Array) out[key] = obj[key].slice(0);
                        else out[key] = deepExtend(out[key], obj[key]);
                    } else out[key] = obj[key];
                }
            }
        }

        return out;
    };

    
/*
 Method: var
Description: Functionality for var.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.var = function (params, args) {

        if (Array.isArray(params)) {
            for (var index in params)
                self.var(params[index], args);
        } else {

            var name;
            var value;
            var varValue;

            if (params.path && !params.name)
                name = self.path(params.path, args, '.');

            if (typeof params == 'string') {

                name = self.replaceProperties(params, args);

                varValue = self.docElement('self.json.var.' + name);

            } else {

                var paramsReplaced = self.replaceProperties(params, args);

                name = paramsReplaced.name;
                value = paramsReplaced.value;
                var operator = paramsReplaced.operator;
                var type = paramsReplaced.type;

                if (name) {
                    var pathString = 'self.json.var.' + name;
                    var pathLast = pathString.substr(pathString.lastIndexOf(".") + 1);
                    var pathBase = pathString.substr(0, pathString.lastIndexOf("."));
                    var varObj = self.docElement(pathBase);

                    if (value !== undefined) {

                        value = self.actionResult(value, args);

                        switch (type) {
                            case 'string': value = String(value); break;
                            case 'number': value = Number(value); break;
                            case 'boolean': value = Boolean(value); break;
                            case 'array': value = JSON.parse(value); break;
                            case 'object': value = JSON.parse(value); break;
                        }

                        if (varObj && pathLast) {
                            switch (operator) {
                                case '+=': varObj[pathLast] += value; break;
                                case '-=': varObj[pathLast] -= value; break;
                                case '/=': varObj[pathLast] /= value; break;
                                case '*=': varObj[pathLast] *= value; break;
                                default: varObj[pathLast] = value; break;
                            }

                            varValue = varObj[pathLast];
                        } else {

                            self.docElement(pathString, value);

                        }

                    } else if (paramsReplaced.push) {
                        if (varObj && pathLast && varObj[pathLast]) {
                            varObj[pathLast] = varObj[pathLast].push(paramsReplaced.push);
                            varValue = varObj[pathLast];
                        }
                    } else if (paramsReplaced.extend) {
                        if (varObj && pathLast && varObj[pathLast]) {
                            varObj[pathLast] = self.extend({}, varObj[pathLast], paramsReplaced.extend);
                            varValue = varObj[pathLast];
                        }
                    } else {

                        varValue = varObj[pathLast];

                    }

                } else {
                    self.log('var name undefined');
                    self.log(params);
                    varValue = undefined;
                }

                if (params.do && params.do.length > 0) {
                    self.log('this.var params.do');
                    self.log(params.do);

                    self.do(params.do, undefined, args);
                }

            }

            return varValue;

        }
    };

    
/*
 Method: find
Description: Functionality for find.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.find = function (params, args) {
        if (params.in) {
            var list = self.replaceProperties(params.in, args);
            return list.filter(function (item) {
                if (params.value) {
                    if (typeof params.value == 'string') {
                        if (JSON.stringify(item).indexOf(params.value) >= 0)
                            return item;
                    } else {  
                        for (var key in params.value) {
                            if (params.value[key] == item[key])
                                return item;
                        }
                    }
                }
            });
        }
    };

    
/*
 Method: array
Description: Functionality for array.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.array = function (params, args) {

        if (args)
            params = self.replacePropertyWithPrefix(params, 'result', args);

        for (var par in params) {
            var paramReplaced = self.replaceProperties(params[par], args);

            params[par] = paramReplaced;
        }

        var value = self.element({ path: params.name }) || [];

        if (params.push) {
            if (Array.isArray(params.push))
                value.push(...params.push);

            else
                value.push(params.push);

        }
        if (params.unshift) value.unshift(...params.unshift);
        if (params.pop) return value.pop(params.pop);
        if (params.shift) return value.shift(params.shift);

        self.element({ path: params.name, value: value });

    };

    
/*
 Method: replace
Description: Functionality for replace.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.replace = function (params, args) {

        params = self.replacePropertyWithPrefix(params, 'result', args);

        var value = self.element({ path: params.path }) || [];
        var to = params.to || '';

        var type = typeof value;

        if (type !== "string") value = JSON.stringify(value);

        if (params.from) value = self.replaceAll(value, params.from, to);

        switch (type) {
            case "number":
                value = Number(value);
                break;
            case "boolean":
                value = Boolean(value);
                break;
            case "object":
                value = JSON.parse(value);
                break;
        }

        return self.element({ path: params.path, value: value });

    };

    
/*
 Method: unshift
Description: Functionality for unshift.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.unshift = function (params, args) {
        for (var param in params) {
            var path = self.replaceProperties(param, args);
            var value = self.replaceProperties(params[param], args);
            if (value) {
                var obj = self.element({ path: path }) || [];
                obj.unshift(value);
                self.element({ path: path, value: obj });
            }
        }
    };

    
/*
 Method: set
Description: Functionality for set.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.set = function (params, args) {

        if (Array.isArray(params)) {  
            for (var obj of params)
                self.set(obj, args);
        } else {
            for (var param in params) {

                var value = params[param];

                if (value) value = self.replaceProperties(value, args, false);  
                if (value && args) value = self.replacePropertyWithPrefix(value, 'result', args);  

                self.element({ path: param, value: value });
            }
        }

    };

    
/*
 Method: get
Description: Functionality for get.
@param {any} element - Description for element
@returns {any} - Result of the operation
 */
this.get = function (element) {

        if (element.startsWith('\''))
            return String(element.match(/\'([^\']*)\'/)[1]); // stringa racchiusa tra apici (serve?)
        else if (element.startsWith('Boolean'))
            return Boolean(element.match(/Boolean\(([^\)]*)\)/)[0]); // Boolean(x)
        else if (element.startsWith('Number'))
            return Number(element.match(/Number\(([^\)]*)\)/)[0]); // Number(x)
        else if (element.startsWith('String'))
            return String(element.match(/String\(([^\)]*)\)/)[0]); // String(x)

        else
            return (self.docElement(element)); // ora le tipizzazioni sono già in docElement grazie a eval
    };

    
/*
 Method: getJSON
Description: Functionality for getJSON.
@param {any} url - Description for url
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.getJSON = function (url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'json';
        xhr.onload = function () {
            var status = xhr.status;
            if (status === 200) {
                callback(null, xhr.response);
            } else {
                callback(status, xhr.response);
            }
        };
        xhr.send();
    };

    /*---------------------
        var
    ---------------------*/
    var database, auth, storage;
    this.listeners = {};
    this.resizeActions = {};

    /* var iconsFile;

    this.device = false; // touch device
    this.touch = 'mousedown'; if (this.device) {this.touch = 'touchstart';}

    var animations = {};
    var tweenLoading;

    var tasksIntervals = {};
    var timedTasks = {'yearly':{},'monthly':{},'daily':{},'hourly':{}, '10mins':{}, '5mins':{}, '30mins':{}, '1min':{}, '30sec': {}, '10sec': {}, '1sec': {}};  */
    /* this.dateSelection; // = {day:0, month:0, year:0};
    this.timeSelection = {hour:0, min:0, sec:0, ampm:'AM', allday:true};
    this.timeSelectionCallback = null; */
    /*--------------------
        FIREBASE
    --------------------*/
    //var firebaseInitialized = false;
    
/*
 Method: firebaseVerifyUser
Description: Functionality for firebaseVerifyUser.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.firebaseVerifyUser = function (data) {
        self.log('firebaseVerifyUser');
        //self.log(data);
        //self.log(data.code);
        if (self.params.oobCode) {
            var oobCode = self.params.oobCode;

            auth.applyActionCode(oobCode).then(function (resp) {
                // self.log('resp');
                // self.log(resp);
                self.alert({
                    //toast: true,
                    icon: "success",
                    title: "titVerifyEmail",
                    html: "msgVerifyEmail",
                    confirmButtonText: "btnOk",
                    //showCancelButton: false,
                    confirm: function () {
                        //self.reload();
                        // self.do(data);
                        window.location.href = "/app/index.html";
                    }
                });
            }).catch(function (error) {
                self.log('error');
                self.alert({
                    icon: "error",
                    title: "titWarning",
                    html: error.code + " - " + error.message,
                    confirmButtonText: "btnOk",
                    //showCancelButton: false,
                    confirm: function () {
                        // self.reload();
                        // self.do(data);
                        window.location.href = "/app/index.html";
                    }
                });
            });
            // verifica su auth
        }
        //data.code / data.params.code
        //self.do(data);
        self.do(data);
    };

    /*     
/*
 Method: firebaseInit
Description: Functionality for firebaseInit.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.firebaseInit = function (data) {
            self.log("firebaseInit");

            if (json.setup && json.setup.firebase) {
                //var.databaseURL = json.setup.firebase.databaseURL;

                firebase.initializeApp(json.setup.firebase);
                database = firebase.database();
                auth = firebase.auth();
                // if (self.json.plugins['firebase-storage'].active) OR find in self.json.plugins name: 'firebase-storage'
                //storage = firebase.storage();

                auth.onAuthStateChanged(user => {
                    self.json.var.user = user;
                    if (!user) {
                        // No user logged in
                        self.log('onAuthStateChanged:GUEST\n');
                        // non bisognerebbe chiedere sempre di fare login
                    } else {
                        // User logged in
                        self.log('onAuthStateChanged:USER\n');
                        self.log('name: '+self.user('displayName') + '\nemail: '+user.email + '\nuid: '+ user.uid);
                    }
                    //self.log('firebaseInitialized');
                    //self.log(firebaseInitialized);
                    if (!firebaseInitialized) {
                        firebaseInitialized = true;
                        self.do(data);
                    }
                });
            } else {
                self.do(data);
            }
        } */
    
/*
 Method: sendMail
Description: Functionality for sendMail.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.sendMail = function (data) {
        self.log('sendMail');

        var emailParams = {
            Host: "smtp.mailgun.com",
            Username: "vito@nuvolaria.com",
            Password: "bee02191be84c784a665fa98c61e03ce-915161b7-d32cb9d0",
            From: data.from,
            To: data.to,
            Subject: data.subject,
            Body: data.html
        };

        Email.send(emailParams).then(
            message => self.alert({
                icon: 'success',
                title: 'titEmailSuccess',
                confirmButtonText: 'btnClose'
            })
        );

        /*
        "from": "gianfrancoguglielmi@gmail.com",
        "to": "vito.minchilli@gmail.com",
        "subject": "Prova oggetto",
        "html": "Prova testo",
        "text": "Prova testo"
*/
        /*
                var SERVER_NODEJS_BASEURL='https: 

                var params = {
                    rom: data.from,
                    to: data.to,
                    subject: data.subject,
                    html: data.html,
                    text: data.text
                };

                $.ajax({
                    type: "POST",
                    'beforeSend': function(xhr) {
                        xhr.setRequestHeader('Accept', 'application/json');
                        xhr.setRequestHeader('Content-Type', 'application/json');
                    },
                    data: JSON.stringify(params),
                    url: SERVER_NODEJS_BASEURL + '/sendMail',
                    success: function(data) {
                        self.log('sendMail SUCCESS');
                        self.log(data);
                        self.do(data);
                    },
                    error: function(error) {
                        self.log('sendMail ERROR');
                        self.log(error);
                        self.do(data);
                    }
                });
        */
    };

    var x = [{ if: 3 }, function () { }, 3, []];

    
/*
 Method: firebaseAddListener
Description: Functionality for firebaseAddListener.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseAddListener = function (params) {

        if (database) {

            if (params.action) {
                if (!self.listeners[params.path]) self.listeners[params.path] = [];

                self.listeners[params.path].push(params.action);
            }

            var dbRef = database.ref(params.path);
            dbRef.off();
            if (params.orderByChild) dbRef = dbRef.orderByChild(params.orderByChild);
            if (params.orderByKey) dbRef = dbRef.orderByKey(params.orderByKey);
            if (params.orderByValue) dbRef = dbRef.orderByValue(params.orderByValue);
            if (params.equalTo) dbRef = dbRef.equalTo(params.equalTo);
            if (params.startAt) dbRef = dbRef.startAt(params.startAt);
            if (params.startAfter) dbRef = dbRef.startAfter(params.startAfter);
            if (params.endAt) dbRef = dbRef.endAt(params.endAt);
            if (params.endBefore) dbRef = dbRef.endBefore(params.endBefore);
            if (params.limitToFirst) dbRef = dbRef.limitToFirst(params.limitToFirst);
            if (params.limitToLast) dbRef = dbRef.limitToLast(params.limitToLast);
            if (params.where) dbRef = dbRef.where(params.where[0], params.where[1], params.where[2]);  

            var feedback = dbRef.on('value', firebaseEvent);

        } else {
            self.log('database undefined');

        }
    };

    
/*
 Method: firebaseEvent
Description: Functionality for firebaseEvent.
@param {any} snapshot - Description for snapshot
@returns {any} - Result of the operation
 */
var firebaseEvent = function (snapshot) {
        console.log(snapshot.val());
        var ref = snapshot.ref;
        if (snapshot.val() !== undefined) {
            var dbVal = snapshot.val();
            var path = snapshot.ref.toString();

            var pathArr = path.split(/\.\w+\ 
            var dbPath = pathArr[1];
            self.log('firebaseEvent: ' + dbPath);

            if (dbVal)
                self.localDbUpdate(dbVal, dbPath);
            else {
                var dbObj = self.element({ path: 'self.json.var.db.' + self.replaceAll(dbPath, '/', '.') });
                if (dbObj)
                    dbObj = null;

            }

        }
    };

    
/*
 Method: localDbUpdate
Description: Functionality for localDbUpdate.
@param {any} newObj - Description for newObj
@param {any} dbPath - Description for dbPath
@returns {any} - Result of the operation
 */
this.localDbUpdate = function (newObj, dbPath) {

        newObj = Object.fromEntries(Object.entries(newObj).reverse());

        self.element({ path: 'self.json.var.db.' + self.replaceAll(dbPath, '/', '.'), value: newObj });
        if (self.listeners[dbPath] !== undefined && self.listeners[dbPath] !== null && self.listeners[dbPath].length > 0) {
            newObj.path = dbPath;
            if (self.listeners[dbPath]) {
                self.methods.firebaseEvent.action(newObj);

                self.do(self.listeners[dbPath], undefined, newObj);
            }
        }

    };

    
/*
 Method: firebaseRemoveListener
Description: Functionality for firebaseRemoveListener.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseRemoveListener = function (params) {

        self.log('firebaseRemoveListener');
        if (self.listeners[params.path]) {
            delete self.listeners[params.path];
            var dbRef = database.ref(params.path);
            dbRef.off();
        }
    };

    
/*
 Method: firebaseRemove
Description: Functionality for firebaseRemove.
@param {any} path - Description for path
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.firebaseRemove = function (path, callback) {
        self.log('firebaseRemove');
        self.log(path);
        if (database) {
            var result, dbRef;
            if (path) {
                dbRef = database.ref().child(path);
            } else {
                dbRef = database.ref();
            }
            return dbRef.remove(function (error) {

                if (error == null) {
                    self.log('set SUCCESS');
                    result = { success: true };
                } else {
                    self.log('set ERROR');

                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    
/*
 Method: firebaseSet
Description: Functionality for firebaseSet.
@param {any} path - Description for path
@param {any} value - Description for value
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.firebaseSet = function (path, value, callback) {

        if (database) {
            var result, dbRef;
            if (path) {
                dbRef = database.ref().child(path);
            } else {
                dbRef = database.ref();
            }
            return dbRef.set(value, function (error) {

                if (error == null) {
                    self.log('set SUCCESS');
                    result = { success: true };
                } else {
                    self.log('set ERROR');

                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    
/*
 Method: firebaseUpdate
Description: Functionality for firebaseUpdate.
@param {any} path - Description for path
@param {any} value - Description for value
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.firebaseUpdate = function (path, value, callback) {
        self.log('firebaseUpdate');
        self.log('path');
        self.log(path);
        self.log('value');
        self.log(value);

        if (database) {
            var result, dbRef;
            if (path) {
                dbRef = database.ref().child(path);
            } else {
                dbRef = database.ref();
            }
            return dbRef.update(value, function (error) {

                if (error == null) {
                    self.log('update SUCCESS');
                    result = { success: true };
                } else {
                    self.log('update ERROR');

                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    
/*
 Method: firebasePush
Description: Functionality for firebasePush.
@param {any} path - Description for path
@param {any} value - Description for value
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.firebasePush = function (path, value, callback) {
        self.log('firebasePush');
        self.log(path);

        if (database) {
            var result, dbRef;
            if (path) {
                dbRef = database.ref().child(path);
            } else {
                dbRef = database.ref();
            }
            return dbRef.push(value, function (error) {

                if (error == null) {
                    self.log('push SUCCESS');
                    result = { success: true };
                } else {
                    self.log('push ERROR');

                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    
/*
 Method: firebaseGetPart
Description: Functionality for firebaseGetPart.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseGetPart = function (params) {
        self.log('firebaseGetPart');
        let remotePath = self.replaceAll(params.localPath, '.', '/');

        self.log('localPath:' + params.localPath);

        if (self.partContainers[params.localPath]) {
            self.partContainers[params.localPath].push(params);
        } else {
            self.partContainers[params.localPath] = [];
            self.partContainers[params.localPath].push(params);

            dbRef = database.ref().child(remotePath);

            return dbRef.once('value').then(function (object) {

                if (object) {
                    self.log('firebaseGetPart SUCCESS');
                    self.log(object);

                    if (object.val() !== undefined) {
                        window.dbVal = object.val();
                        var dbVal = object.val();
                        self.log('dbVal');
                        self.log(dbVal);
                        if (dbVal !== null) {
                            let fullPath = object.ref.toString();
                            let pathArr = fullPath.split(/\.\w+\ 
                            let dbPath = pathArr[1];  
                            let localPath = self.replaceAll(dbPath, '/', '.');

                            self.log('dbPath: ' + dbPath);
                            self.log('localPath: ' + localPath);

                            self.element({ path: localPath, value: dbVal });
                            for (partObj of self.partContainers[localPath]) {

                                let part = self.replacePropertyWithPrefix(dbVal, 'setup', partObj.setup);
                                part = self.replacePropertyWithPrefix(part, 'arg', partObj.arg);
                                self.extendJsonFromElement(part);  

                                self.html(part, partObj.container);

                            }
                        }
                    }
                } else {
                    self.log('firebaseGetPart ERROR');
                    result = { success: false, error: 8 };
                }

            });
        }
    };

    
/*
 Method: firebaseIncrease
Description: Functionality for firebaseIncrease.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseIncrease = function (params) {
        console.log('firebaseIncrease');
        self.firebaseGet(params.path, function (result) {
            console.log(result);
            if (result.success) {
                if (result.data === null || result.data === undefined || typeof result.data == 'number') {
                    var counter = Number(result.data) + 1;
                    console.log('counter:' + counter);
                    self.firebaseSet(params.path, counter, function (result) {
                        if (result.success) {
                            self.log('firebaseIncrease SUCCESS');
                            if (params.on && params.on.success) self.do(params.on.success, undefined, result);
                        } else {
                            if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                        }
                    });
                } else {
                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                }
            } else {
                if (params.on && params.on.error) self.do(params.on.error, undefined, result);
            }
        });
    };

    
/*
 Method: firebaseGet
Description: Functionality for firebaseGet.
@param {any} path - Description for path
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.firebaseGet = function (path, callback) {
        self.log('firebaseGet:' + path);
        var result = { success: false, error: 7 };
        if (path) {

            dbRef = database.ref().child(path);
        } else {
            dbRef = database.ref();
        }

        return dbRef.once('value').then(function (object) {

            if (object) {
                self.log('get SUCCESS');
                result = { success: true, error: null, data: object.val() };

                if (object.val() !== undefined) {
                    var dbVal = object.val();
                    var path = object.ref.toString();
                    if (!dbVal) dbVal = {};
                    var pathArr = path.split(/\.\w+\ 
                    var dbPath = pathArr[1];  

                    self.log('dbPath: ' + dbPath);
                    self.localDbUpdate(dbVal, dbPath);
                }
            } else {
                self.log('get ERROR');

                result = { success: false, error: 8 };
            }

            callback(result);
        });

    };

    
/*
 Method: stringToKey
Description: Functionality for stringToKey.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
this.stringToKey = function (str) {
        return str.replace(/\./g, '%2E');
    };

    
/*
 Method: keyToString
Description: Functionality for keyToString.
@param {any} key - Description for key
@returns {any} - Result of the operation
 */
this.keyToString = function (key) {
        return key.replace(/%2E/g, '.');
    };

    this.firebaseKey = 
/*
 Method: getKey
Description: Functionality for getKey.
@param {any} path - Description for path
@returns {any} - Result of the operation
 */
this.getKey = function (path) {
        var dbRef = database.ref(path);
        return dbRef.child(path).push().getKey();
    };

    
/*
 Method: exist
Description: Functionality for exist.
@param {any} selector - Description for selector
@returns {any} - Result of the operation
 */
this.exist = function (selector) {
        console.log('selector');
        console.log(selector);
        console.log(Boolean(self.query(selector)));
        return Boolean(self.query(selector));
    };

    
/*
 Method: count
Description: Functionality for count.
@param {any} selector - Description for selector
@returns {any} - Result of the operation
 */
this.count = function (selector) {

        return self.queryAll(selector).length;

    };

    
/*
 Method: log
Description: Functionality for log.
@param {any} id - Description for id
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
var log = function (id, value) {
        self.log(id);
        self.log(value);
    };

    
/*
 Method: classSelector
Description: Functionality for classSelector.
@param {any} selClass - Description for selClass
@returns {any} - Result of the operation
 */
this.classSelector = function (selClass) {
        if (selClass.indexOf('.') == 0) { selClass = String(selClass).substr(1); }  
        let classes = selClass.split(' ');
        selClass = classes[0];

        var regex = new RegExp('[\-][\[]');
        if (selClass && selClass.match(regex)) selClass = classes[1];
        if (selClass && selClass.match(regex)) selClass = classes[2];
        return selClass;
    };

    
/*
 Method: selector
Description: Functionality for selector.
@param {any} params - Description for params
@param {any} containerParams - Description for containerParams
@param {any} selectAll - Description for selectAll
@returns {any} - Result of the operation
 */
this.selector = function (params, containerParams, selectAll) {

        var selector;

        if (params) {

            if (typeof params == 'string') {

                return params;
            } else if (Array.isArray(params)) {
                return params;
            } else {
                var selContainer;

                if (params.selector || params.container)
                    selContainer = params.selector || params.container;

                else if (containerParams)
                    selContainer = containerParams.selector || containerParams.container;

                var attr = (params.attr) ? params.attr : {};
                var selId = (attr.id) ? attr.id : params.id;
                var selClass = (attr.class) ? attr.class : params.class;
                var selValue = (attr.value) ? attr.value : params.value;
                var selDataValue = (attr['data-value']) ? attr['data-value'] : params['data-value'];
                selValue = selDataValue || selValue;
                if (selId) selId = self.replaceProperties(selId);
                if (selValue) selValue = self.replaceProperties(selValue);
                if (selClass) selClass = self.replaceProperties(selClass);

                if (selClass)
                    selClass = self.classSelector(selClass);

                if (selId) {
                    selector = '#' + selId;
                } else if (selValue && selClass) {
                    selector = '.' + selClass + '[data-value="' + selValue + '"]';
                } else if (selValue && params.tag) {
                    selector = params.tag + '[data-value="' + selValue + '"]';
                } else if (selClass && selectAll) {
                    selector = '.' + selClass;

                } else if (params.tag) {

                    selector = params.tag;
                    if (selContainer) {
                        selector = selContainer + ' > ' + params.tag;
                        if (!selectAll) {

                            selector += ':eq(' + self.count(selector) + ')';

                        }
                    } else {
                        if (!selectAll) {
                            selector += ':eq(' + self.count(selector) + ')';

                        }
                    }
                } else {
                    selector = selContainer;
                };

            }
            return selector;
        }
    };

    
/*
 Method: stringify
Description: Functionality for stringify.
@param {any} obj - Description for obj
@returns {any} - Result of the operation
 */
this.stringify = function (obj) {
        if (typeof obj == 'object')
            try {
                return JSON.stringify(obj);
            } catch (error) {
                return obj;
            }

        else
            return obj;

    };

    
/*
 Method: parse
Description: Functionality for parse.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
this.parse = function (str) {
        if (typeof str == 'string')
            try {
                return JSON.parse(str);  
            } catch (error) {
                return str;  
            }

        else
            return str;
    };

    self.setupId = 0;

    
/*
 Method: replacePropertyWithPrefix
Description: Functionality for replacePropertyWithPrefix.
@param {any} params - Description for params
@param {any} prefix - Description for prefix
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.replacePropertyWithPrefix = function (params, prefix, args) {

        var paramsString;

        if (typeof params == 'object')
            paramsString = self.stringify(params);

        else
            paramsString = String(params);

        if (paramsString && paramsString.indexOf('{' + prefix) >= 0) {
            paramsString = paramsString.replace(new RegExp('"{' + prefix + '[:\\s]?([\\w\\d\\s\\.\\|]*)}"', 'g'), function (match, path) {
                if (path) {
                    let value = self.replaceUndefined({ match: match, path: path, root: args, removeUndefined: true });

                    if (value === undefined) {
                        return '""';
                    } else if (typeof value == 'object')
                        return JSON.stringify(value);
                    else if (typeof value == 'string')
                        return '"' + value + '"';

                    else
                        return value;
                } else {
                    if (typeof args == 'object')
                        return JSON.stringify(args);
                    else if (typeof args == 'string')
                        return '"' + args + '"';

                    else
                        return args;
                }
            });

            paramsString = paramsString.replace(new RegExp('{' + prefix + '[:\\s]?([\\w\\d\\s\\.]*)}', 'g'), function (match, path) {
                if (path) {
                    let value = self.replaceUndefined({ match: match, path: path, root: args, removeUndefined: true });

                    if (value === undefined)
                        return '';
                    else if (typeof value == 'object')  
                        return JSON.stringify(value);

                    else
                        return value;
                } else {
                    if (typeof args == 'object')
                        return JSON.stringify(args);

                    else
                        return args;
                }
            });

            if (self.isJsonString(paramsString)) {
                var paramsObj = self.parse(paramsString);
                return paramsObj;
            } else {
                return paramsString;
            }
        } else {
            return params;
        }

    };

    
/*
 Method: replaceResult
Description: Functionality for replaceResult.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.replaceResult = function (params, args) {

        var paramOn, paramSuccess, paramError;

        if (typeof params == 'object') {
            var paramsObj = self.cloneObject(params);
            if (paramsObj && paramsObj.on) {
                paramOn = self.cloneObject(paramsObj.on);
                delete paramsObj.on;
            }
            if (paramsObj && paramsObj.success) {
                paramSuccess = self.cloneObject(paramsObj.success);
                delete paramsObj.success;
            }

            params = self.stringify(params);
        }

        if (typeof args !== 'string') {
            params = self.replaceAll(params, '"{result}"', JSON.stringify(args));
        } else {
            params = self.replaceAll(params, '{result}', args);
        }

        if (self.isJson(params)) {  
            var paramsObj = self.parse(params);
            if (paramOn) paramsObj.on = paramOn;
            if (paramSuccess) paramsObj.success = paramSuccess;
            if (paramError) paramsObj.error = paramError;
            return paramsObj;
        } else {

            return params;
        }

    };

    
/*
 Method: langText
Description: Functionality for langText.
@param {any} textId - Description for textId
@returns {any} - Result of the operation
 */
this.langText = function (textId) {

        if (textId) {
            var textValue = textId;
            if (typeof textId == 'string') {
                if (self.json.texts[textId]) {
                    textValue = self.json.texts[textId];
                    if (typeof textValue == 'object')
                        textValue = textValue[self.json.setup.language] || textValue['en'] || textValue[Object.keys[0]];
                }
                else
                    textValue = textId;
            } else if (typeof textId == 'object') {
                textValue = textId[self.json.setup.language] || textId['en'] || textId[Object.keys[0]];

            }
            return textValue;
        } else {
            return textId;
        }

    };

    
/*
 Method: lang
Description: Functionality for lang.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.lang = function (params, args) {
        if (args) {
            var value = self.replaceProperties(params, args);
            self.json.setup.language = value;

            self.log('lang');
            self.log(value);

            document.querySelectorAll('[data-text]').forEach(function (element, index) {

                var dataParams = self.dataStorage.get(element, 'params');
                var selector = self.dataStorage.get(element, 'selector');

                self.html(dataParams, { selector: selector });

            });

        } else {
            return self.json.setup.language;
        }
    };

    
/*
 Method: isClipboardWritingAllowed
Description: Functionality for isClipboardWritingAllowed.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
var isClipboardWritingAllowed = function () {
        return new Promise(function (resolve, reject) {
            try {
                navigator.permissions.query({ name: "clipboard-write" }).then(function (status) {

                    self.log(status);

                    resolve((status.state == "granted"));
                });
            } catch (error) {

                reject(error);
            }
        });
    };

    
/*
 Method: codeSearch
Description: Functionality for codeSearch.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.codeSearch = function (params) {
        let selector = params.selector || params.container;

        if (selector && params.word) {
            var element = self.query(selector);
            var editor = ace.edit(element);

            var range = editor.find(params.word, {
                wrap: true,
                caseSensitive: true,
                wholeWord: true,
                regExp: false,
                preventScroll: false  
            });
            editor.session.selection.clearSelection();
            editor.setOption("highlightActiveLine", true);

        }
    };

    
/*
 Method: ace
Description: Functionality for ace.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.ace = function (params, selectorParams) {

        var selector = params.selector || params.container;
        var container = selector || self.selector(selectorParams);

        var value = params.value;

        if (params.blocks && self.json.blocks)
            if (self.json.blocks[params.blocks])
                value = self.json.blocks[params.blocks];

            else
                self.log('"code" method can\'t find in "blocks" the element ' + params.blocks);

        if (params.module) {
            let moduleName = self.replaceProperties(params.module);
            if (moduleName && self.modules[moduleName] !== undefined) {
                value = self.modules[moduleName];

            }
            else
                self.log('"code" method can\'t find the file ' + params.file);
        }
        if (params.var)
            if (self.json.var)
                value = self.var(params.var);

            else
                self.log('"code" method can\'t find the variable ' + params.blocks);

        var element = self.query(container);

        if (element) {

            var editor = ace.edit(element);

            if (!params.do || (params.do == 'set')) {

                if (params.theme)
                    editor.setTheme(params.theme);  
                var editorMode = params.mode || "ace/mode/json";
                editor.getSession().setMode(editorMode);

                if (params.options)
                    editor.setOptions(params.options);

                var editorStyle = self.extend({

                }, params.style);

                self.css({
                    style: editorStyle
                }, {
                    selector: container
                });

                var codeString;
                if (typeof value == 'string')
                    if (editorMode == "ace/mode/json")
                        codeString = JSON.stringify(JSON.parse(value), null, '\t');  

                    else
                        codeString = value;
                else {
                    if (editorMode == "ace/mode/json")
                        codeString = JSON.stringify(value, null, '\t');
                    else {
                        var functionObj = self.docElement(value.function);  
                        var functionParams = value.params;
                        codeString = functionObj(functionParams);
                    }
                }

                if (codeString) {
                    var codeRows = codeString.split(/\r\n|\r|\n/).length;

                    element.style.height = Number(21 * codeRows) + 'px';

                    editor.setValue(codeString, -1);  

                    self.uiUpdate();  

                }

                if (params.on) {

                    var containerId = self.attribute(element, 'id');  
                    self.codeChangeFunctions[containerId] = params.on;

                    if (params.on.change) {
                        editor.on('change', function (event, obj) {

                            if (self.isJson(obj.getValue())) {
                                var code = JSON.parse(obj.getValue());
                                var action = self.codeChangeFunctions[obj.container.id].change;

                                var methods = self.replaceResult(action, code);

                                self.do(methods);

                            } else {

                            }

                        });
                    }

                }

            } else if (params.do == 'get') {
                return JSON.parse(editor.getValue());

            } else if (params.do == 'update') {

                self.uiUpdate();  

            } else if (params.do == 'search') {
                if (params.word) {
                    var range = editor.find(params.word, {
                        wrap: true,
                        caseSensitive: true,
                        wholeWord: true,
                        regExp: false,
                        preventScroll: false  
                    });
                    editor.session.selection.clearSelection();
                    editor.setOption("highlightActiveLine", true);

                }
            } else if (params.do == 'find') {

                editor.execCommand('find');

            } else if (params.do == 'copy') {

                editor.selectAll();

                let copyText = editor.getCopyText();

                isClipboardWritingAllowed().then(function (allowed) {

                    if (allowed) {
                        navigator.clipboard.writeText(copyText).then(function () {
                            editor.getSession().selection.clearSelection();

                        });
                    }
                }).catch(function (err) {
                    self.log("Cannot copy to clipboard", err);
                });
            } else {
                self.log('"code" function requires value param');
            }
        } else {
            self.log('"code" method can\'t select the container');
            self.log('container');
            self.log(container);
            self.log('element');
            self.log(element);
        }

    };

    this.codeChangeFunctions = {};
    this.editorChangeFunctions = {};
    this.quillElements = {};

    
/*
 Method: text
Description: Functionality for text.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.text = function (params, selectorParams, args) {

        var textKey;
        var textCase;
        var textArgs;
        var textLang;
        var langId;
        var selector, element;

        if (typeof params == 'object') {

            if (selectorParams && selectorParams.isConnected) {  
                element = selectorParams;

                selector = self.elementToSelector(element);  
            } else {
                selector = self.selector(self.extend({}, params, selectorParams));
                element = self.query(selector);
            }

            textCase = params.case;  
            textArgs = params.args;  

            if (typeof params == 'object') {

                textKey = params.key || params.string || params;  

            } else {
                textKey = params;
            }

        } else if (typeof params == 'string') {
            textKey = params;

            if (selectorParams) {
                if (selectorParams.isConnected) {  
                    element = selectorParams;
                    selector = self.dataStorage.get(element, 'selector');
                } else {
                    selector = self.selector(selectorParams);
                    element = self.query(selector);
                }
            } else {

            }
        }

        if (typeof textKey == 'string') {

            textKey = self.replaceProperties(textKey, textArgs);

        }

        if (params && params.lang) {  

            textLang = self.langText(params.lang);  

            textLang = self.replaceProperties(textLang, textArgs);

            if (textCase) textLang = self.textCase(textLang, textCase);

            if (element) {
                element.textContent = textLang;
                self.attribute(element, 'data-text', true);  
                self.dataStorage.set(element, 'params', params);

            }

        } else {

            if (textKey) {

                if (textKey.lang)  
                    textLang = self.langText(textKey.lang);  

                else
                    textLang = textKey;

                textLang = self.replaceProperties(textLang, textArgs);  
                if (textCase) textLang = self.textCase(textLang, textCase);

                if (element) {  

                    if (self.json.texts[textKey]) {

                        self.attribute(element, 'data-text', true);
                        self.dataStorage.set(element, 'params', params);
                    }

                    if (params.style)
                        self.css({
                            style: params.style
                        }, {
                            selector: selector
                        });

                    element.textContent = textLang;  

                } else {  
                    return textLang;
                }

            } else {

                return params;
            }

        }

    };

    
/*
 Method: caseUp
Description: Functionality for caseUp.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var caseUp = function (string) {
        return string.toUpperCase();
    };

    
/*
 Method: caseDown
Description: Functionality for caseDown.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var caseDown = function (string) {
        return string.toLowerCase();
    };

    
/*
 Method: capitalize
Description: Functionality for capitalize.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var capitalize = function (string) {
        return self.capitalize(string);
    };

    
/*
 Method: textCase
Description: Functionality for textCase.
@param {any} textParam - Description for textParam
@param {any} caseParam - Description for caseParam
@returns {any} - Result of the operation
 */
this.textCase = function (textParam, caseParam) {
        if (textParam) {
            var text = textParam;
            switch (caseParam) {
                case 'up':
                    text = text.toUpperCase();
                    break;

                case 'down':
                    text = text.toLowerCase();
                    break;

                case 'capitalize':
                    text = self.capitalize(text);
                    break;
            }
            return text;
        } else {
            self.log('textParam undefined in textCase');
        }
    };

    
/*
 Method: replaceUndefined
Description: Functionality for replaceUndefined.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.replaceUndefined = function (params) {
        let value, defaultValue;

        if (typeof params.path !== undefined) {
            if (params.path.indexOf('|') > 0)
                [params.path, defaultValue] = params.path.split('|');
            value = self.element({ path: params.path, root: params.root });
        }
        if (value == undefined || value == null) {  
            if (typeof defaultValue !== undefined)
                value = defaultValue;
            else {
                if (params.removeUndefined) {  
                    value = '';  
                } else {
                    value = params.match;
                    self.log(value + ' not found', 'yellow');
                }
            }
        } else {

        }
        return value;
    };

    
/*
 Method: replaceProperties
Description: Functionality for replaceProperties.
@param {any} params - Description for params
@param {any} args - Description for args
@param {any} removeUndefined - Description for removeUndefined
@returns {any} - Result of the operation
 */
this.replaceProperties = function (params, args, removeUndefined) {

        var paramsString;
        var paramOn, paramDo, paramSuccess, paramError, paramEvents, paramConfirm;
        if (typeof params == 'string') {
            paramsString = params;
        } else {

            var paramsObj = self.cloneObject(params);
            if (paramsObj) {
                if (paramsObj.on) { paramOn = paramsObj.on; delete paramsObj.on; }
                if (paramsObj.success) { paramSuccess = paramsObj.success; delete paramsObj.success; }
                if (paramsObj.error) { paramError = paramsObj.error; delete paramsObj.error; }
                if (paramsObj.events) { paramEvents = paramsObj.events; delete paramsObj.events; }  
                if (paramsObj.confirm) { paramConfirm = paramsObj.confirm; delete paramsObj.confirm; }  
                if (paramsObj.do) { paramDo = paramsObj.do; delete paramsObj.do; }
            }
            paramsString = JSON.stringify(paramsObj);
        }

        if (paramsString && paramsString.indexOf('{') >= 0) {

            paramsString = paramsString.replace(/\"\{([\w\s\.]+)\}\"/g, function (match, p1, offset, string) {
                var value;
                if (p1) {
                    //self.log('p1:' + p1);
                    //value = self.element({path: p1}); // || match;
                    value = self.replaceUndefined({ match: '{' + match + '}', path: p1, removeUndefined: removeUndefined });
                    if (value) {
                        if (value.isConnected) return self.elementToSelector(value); // DOM element to CSS selector
                        if (typeof value !== 'string')
                            return self.stringify(value); // object or array to string (TO DO: check numbers)
                        else {
                            //console.warn('we are here');
                            return '"' + value + '"';
                        }
                    }
                    else
                        return '""';
                } else {
                    return '""';
                }
            });

            paramsString = paramsString.replace(/\{(\w+\:)?([\w\s\->|\/=\.:#&;,\[\]\']+)\}/g, function (match, p1, p2, offset, string) {
                /*
                        Firebase references may contain any unicode characters except:
                        . (period)
                        $ (dollar sign)
                        [ (left square bracket)
                        ] (right square bracket)
                        # (hash or pound sign)
                        / (forward slash)
                    */

                /* self.log("paramstring");
                self.log(match, p1, p2); */
                // self.log(match, p1, p2);
                var value;
                if (p1) {
                    if (p1.endsWith(':')) {
                        p1 = p1.slice(0, -1); // remove the final ":"

                        // check if is a 
/*
 Method: name
Description: Functionality for name.
@param {any} in this.json.functions - Description for in this.json.functions
@returns {any} - Result of the operation
 */
function name (in this.json.functions)
                        if (self.functions[p1]) {
                            //value = self.functions[p1](JSON.parse(p2));
                            let funcArgs = p2.split('|');
                            /* console.warn('p2');
                            console.warn(p2);
                            console.warn('funcArgs');
                            console.warn(funcArgs); */
                            // if Number(funcArgs[i]) == funcArgs[i] -> funcArgs[i] = Number(funcArgs[i]);
                            value = self.function({ name: p1, arguments: [funcArgs] });
                        } else {
                            switch (p1) {

                                /* case 'hasClass':
                                    var element = document.querySelector(p2);
                                    if (p3)
                                        value = self.hasClass(el,p3);
                                    else
                                        value = false; el:selector.class (esiste l'elemento selector.class o l'elemento selector ha la classe class)
                                break; */
                                // 'isVisible', 'isHidden', 'isDisplay', 'inViewport', 'outViewport', 'notClass', 'hasClass'
                                // hasClass da aggiungere come funzione css
                                // html markups
                                // 'b:string' 'i:string' 'u:string' 'a:STRING|URL'
                                /* case 'i': case 'b': case 'u': case 'strong': case 'em': case 'mark': case 'del': case 'ins': case 'sub': case 'sup':
                                    value = '<'+p1+'>'+p2+'</'+p1+'>';
                                break;

                                case 'a':
                                // example: "{a:text|href:....|target:_blank}"
                                    let props = p2.split('|'); // or indexOf(':') prima occorrenza
                                    let string = props.shift();
                                    var propsString = '';
                                    for (var prop of props) {
                                        //let propArr = prop.split(':'), propName = propArr[0], propValue = propArr[1];
                                        propsString += ' ' + prop;
                                    }
                                    if (propsString == '') propsString = ' href="'+string+'"';
                                    value = '<a'+propsString+'>'+string+'</a>';

                                    // https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
                                    // https://www.w3schools.com/tags/tag_a.asp
                                break;
                                */
                                // DOM properties
                                case 'visible':
                                    var element = document.querySelector(p2); // self.query(p2);
                                    value = (element && self.isVisible(element));
                                    //value = ($(p2).is(':visible'));
                                    break;

                                case 'number':
                                    value = Number(p2);
                                    break;

                                case 'string':
                                    value = String(p2);
                                    break;

                                case 'js': // deprecated (too much special characters like {})
                                    try {
                                        value = eval(p2);
                                    } catch (error) {
                                        value = '';
                                        self.log('javascript error on ' + p2);
                                        self.log(error);
                                    }
                                    break;

                                /* case 'item':
                                    if (fieldsObj) {
                                        //self.log('fieldsObj['+p2+']:'+fieldsObj[p2]);
                                        value = fieldsObj[p2] || ''; //|| p2+'=NULL';
                                    } else {
                                        //self.log('p2:'+p2);
                                        //self.log('fieldsObj == undefined');
                                        value = '{'+p1+':'+p2+'}';
                                    }
                                break; */
                                /* case 'item':
                                case 'items':
                                    value = self.docElement('self.json.items.'+p2);
                                break; */
                                /*  case 'frame':
                                        var element = self.query(p2);
                                        element.contentWindow
                                    break; */
                                /* case 'field':
                                    if (fieldsObj) {
                                        //self.log('fieldsObj['+p2+']:'+fieldsObj[p2]);
                                        value = fieldsObj[p2] || ''; // || p2+'=NULL';
                                    } else {
                                        //self.log('p2:'+p2);
                                        //self.log('fieldsObj == undefined');
                                        value = '';
                                    }
                                break; */
                                /* case 'setup':
                                value = self.docElement('self.json.setup.'+p2) || match;
                                break; */
                                /*
                                                            case 'setup':
                                                                if (args) {
                                                                    self.partSetup = args;
                                                                    value = self.docElement('self.partSetup.'+p2);
                                                                    delete self)Setup;
                                                                } else {
                                                                    value = '';
                                                                }
                                                            break; */
                                case 'wp':
                                case 'wordpress':
                                    // TO DO: should get/set data with jsonic.get/setWordpressData
                                    // setWordpressData can be done automatically in the init
                                    value = self.docElement('self.json.setup.wordpress.' + p2) ?? match;
                                    //alert(value);
                                    //if (value.indexOf('"') >= 0) 
                                    //    value = self.replaceAll(value, '"', '\\"');
                                    // the first should replace only " and not \" but it doesn't works

                                    break;

                                case 'plugins':
                                    value = self.json.resources.pluginsFunctions[p2];
                                    if (value && !Array.isArray(value)) value = [value];
                                    else value = [];

                                    break;

                                case 'database':
                                case 'db':
                                    value = self.docElement('self.json.var.db.' + self.replaceProperties(p2, args));
                                    break;

                                case 'media':  
                                    if (self.json.setup && self.json.data.media)
                                        value = self.docElement('self.json.data.media.' + p2) || match;

                                    else
                                        value = match;
                                    break;

                                case 'color':  
                                    if (self.json.data && self.json.data.colors)
                                        value = self.docElement('self.json.data.colors.' + p2) || match;

                                    else
                                        value = match;
                                    break;

                                case 'class':  
                                    if (self.json.data && self.json.data.class)
                                        value = self.docElement('self.json.data.class.' + p2) || match;

                                    else
                                        value = match;
                                    break;

                                case 'text':
                                    value = self.langText(p2);
                                    break;

                                case 'param':
                                    value = self.params[p2] || '';  
                                    break;

                                case 'height':

                                    var element = document.querySelector(p2);  
                                    if (element)  

                                        value = element.offsetHeight + 'px';

                                    else
                                        value = '0px';
                                    break;

                                case 'offsetWidth':
                                    var element = document.querySelector(p2);  
                                    if (element)  

                                        value = element.offsetWidth;

                                    else
                                        value = '0';
                                    break;

                                case 'width':
                                    var element = document.querySelector(p2);  
                                    if (element)  

                                        value = element.offsetWidth + 'px';

                                    else
                                        value = '0px';
                                    break;

                                case 'local':
                                    value = self.methods.local.get(p2);

                                    break;

                                case 'ace':
                                    var element = document.querySelector(p2);
                                    if (element) {
                                        var editor = ace.edit(element);
                                        value = JSON.parse(editor.getValue());
                                    }

                                    break;

                                case 'shuffle':
                                    value = self.element({ path: p2 });
                                    value = self.methods.shuffle(value);
                                    break;

                                case 'select':
                                case 'input':
                                case 'value':  

                                    var element = document.querySelector(p2);  
                                    value = (element) ? String(element.value || '') : '';

                                    break;

                                case 'user':
                                    if (p2 == 'in') {
                                        value = self.userIn();
                                    } else if (p2 == 'out')
                                        value = self.userOut();

                                    else
                                        value = self.user(p2) || match;  
                                    break;

                                case 'length':
                                    var varObj = self.element({ path: p2 });  

                                    if (varObj)
                                        if (typeof varObj == 'object')
                                            value = Object.keys(varObj).length;

                                        else
                                            value = varObj.length;

                                    else
                                        self.log('' + p2 + ' is undefined');
                                    break;

                                case 'Date':
                                case 'time':
                                    if (p2 == 'timeZone') value = new Intl.DateTimeFormat().resolvedOptions().timeZone;
                                    else if (p2 == 'weekday') value = self.weekday();
                                    else if (p2 == 'stamp') value = self.getTimestamp();
                                    else if (p2 == 'timestamp') value = self.getTimestamp();
                                    else if (p2 == 'getTimestamp') value = self.getTimestamp();
                                    else if (p2 == 'getDay') value = new Date().getDay();
                                    else if (p2 == 'getDate') value = new Date().getDate();
                                    else if (p2 == 'getMonth') value = new Date().getMonth();
                                    else if (p2 == 'getFullYear') value = new Date().getFullYear();
                                    else if (p2 == 'getTime') value = new Date().getTime();  
                                    else if (p2 == 'getSeconds') value = new Date().getSeconds();
                                    else if (p2 == 'getMinutes') value = new Date().getMinutes();
                                    else if (p2 == 'getHours') value = new Date().getHours();

                                    else value = p2 + '=NULL';
                                    break;

                                case 'now':

                                    break;

                                case 'month':
                                    var month = (p2 == 'today' || p2 == undefined) ? new Date().getMonth() : Number(p2);
                                    value = self.month(month);
                                    break;

                                case 'weekday':  
                                    var day = (p2 == 'today' || p2 == undefined) ? new Date().getDay() : Number(p2);
                                    value = self.calendar({ index: Number(day), unit: 'weekday', format: 'long' });
                                    break;

                                case 'weekdayNarrow':  
                                    var day = (p2 == 'today' || p2 == undefined) ? new Date().getDay() : Number(p2);
                                    value = self.calendar({ index: Number(day), unit: 'weekday', format: 'narrow' });
                                    break;

                                case 'weekdayShort':  
                                    var day = (p2 == 'today' || p2 == undefined) ? new Date().getDay() : Number(p2);
                                    value = self.calendar({ index: Number(day), unit: 'weekday', format: 'short' });
                                    break;

                                case 'moment':
                                    var timeFormat = (p2) ? p2 : '';
                                    value = moment().format(timeFormat);
                                    break;

                                case 'dayjs':  
                                    var timeFormat = (p2) ? p2 : '';
                                    value = dayjs().format(timeFormat);
                                    break;

                                case 'Math':
                                    value = self.Math(p2);
                                    break;

                                case 'action':
                                case 'do':
                                    self.log('do');
                                    self.log('p2');
                                    self.log(p2);

                                    value = self.do(p2);  

                                    break;

                                case 'method':
                                    var method = self.element({ path: p2, root: self.methods });
                                    if (method) {
                                        value = method(args);  
                                    } else {
                                        value = match;
                                    }
                                    break;

                                case 'function':  
                                    var func = self.element({ path: p2, root: self.functions });
                                    if (func) {
                                        value = func(args);  
                                    } else {
                                        value = match;
                                    }
                                    break;

                                case 'on':

                                    if (args) {
                                        self.onResult = args;
                                        value = self.docElement('self.onResult.' + p2);

                                        delete self.onResult;
                                    } else {

                                        value = match;
                                    }

                                    break;

                                case 'ajax':
                                    if (args) {
                                        self.ajaxResult = args;  

                                        value = self.docElement('self.ajaxResult.' + p2);

                                        delete self.ajaxResult;
                                    } else {

                                        value = match;
                                    }

                                    break;

                                case 'if':  
                                    p2 = self.replaceProperties(p2);
                                    var ifArr = p2.split('|');
                                    if (self.js(ifArr[0])) value = true;
                                    if (ifArr.length > 1 && value) value = ifArr[1];
                                    if (ifArr.length > 2 && !value) value = ifArr[2];
                                    break;
                                case 'random':

                                    value = String(Math.floor(Number(p2) * Math.random()));

                                    break;

                                case 'getKey':
                                case 'firebaseKey':
                                    value = self.firebaseKey(p2);

                                    break;

                                case 'window':

                                    value = self.docElement(p2) || match;
                                    break;

                                default:

                                    break;

                            }
                        }  
                    }
                } else if (p2) {

                    let keyPrefix = p2.split(' ')[0];  
                    if (self.functions[keyPrefix]) {
                        value = self.function({ name: keyPrefix });
                        value = self.element({ path: p2, root: value });  
                        console.log(keyPrefix);
                        console.log(value);
                    }
                    else
                        value = self.replaceUndefined({ match: match, path: p2, removeUndefined: removeUndefined });

                }

                if (value && value.isConnected) value = self.elementToSelector(value);  

                if (!value) {

                    if (removeUndefined)
                        value = '';
                    else {
                        if (typeof value == undefined)
                            self.log(match + ' is undefined', 'yellow');
                    }
                }
                if (typeof value !== 'string') value = self.stringify(value);  

                return value;
            });

            if (paramsString.startsWith('{js:') && paramsString.endsWith('}'))
                paramsString = self.js(paramsString.substr(4, paramsString.length - 1 - 4));

            if (self.isJsonString(paramsString)) {  
                var paramsObj = self.parse(paramsString);

                if (paramOn) paramsObj.on = paramOn;
                if (paramDo) paramsObj.do = paramDo;
                if (paramConfirm) paramsObj.confirm = paramConfirm;
                if (paramSuccess) paramsObj.success = paramSuccess;
                if (paramError) paramsObj.success = paramError;
                if (paramEvents) paramsObj.events = paramEvents;  
                return paramsObj;
            } else {

                return paramsString;
            }
        } else {

            if (paramOn) params.on = paramOn;
            if (paramDo) params.do = paramDo;
            if (paramConfirm) params.confirm = paramConfirm;
            if (paramSuccess) params.success = paramSuccess;
            if (paramError) params.error = paramError;
            if (paramEvents) params.events = paramEvents;  
            return params;
        }

    };

    
/*
 Method: isJsonString
Description: Functionality for isJsonString.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
this.isJsonString = function (str) {
        try {
            JSON.parse(str);
        } catch (e) {
            return false;
        }
        return true;
    };

    
/*
 Method: isJson
Description: Functionality for isJson.
@param {any} item - Description for item
@returns {any} - Result of the operation
 */
this.isJson = function (item) {
        item = typeof item !== "string" ? JSON.stringify(item) : item;
        try {
            item = JSON.parse(item);
        } catch (e) {
            return false;
        }

        if (typeof item === "object" && item !== null) {
            return true;
        }

        return false;
    };

    
/*
 Method: replaceStringInObject
Description: Functionality for replaceStringInObject.
@param {any} obj - Description for obj
@param {any} stringToReplace - Description for stringToReplace
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
this.replaceStringInObject = function (obj, stringToReplace, value) {
        var paramsString, valueString;
        if (typeof obj == 'string') paramsString = obj; else paramsString = JSON.stringify(obj);
        if (typeof value == 'string') valueString = value; else valueString = JSON.stringify(value);
        paramsString = self.replaceAll(paramsString, stringToReplace, valueString);
        if (self.isJson(paramsString))
            return self.parse(paramsString);

        else
            return paramsString;
    };

    
/*
 Method: replaceItems
Description: Functionality for replaceItems.
@param {any} params - Description for params
@param {any} item - Description for item
@param {any} name - Description for name
@returns {any} - Result of the operation
 */
this.replaceItems = function (params, item, name) {

        var paramsString;
        var specialPattern;
        var itemName = name || 'item';

        if (typeof params == 'string') paramsString = params; else paramsString = JSON.stringify(params);

        if (typeof item == 'object' || Array.isArray(item)) {  
            paramsString = self.replaceAll(paramsString, '"{' + itemName + '}"', JSON.stringify(item));
            for (let key in item) {
                if (typeof item[key] == 'object')
                    paramsString = self.replaceAll(paramsString, '"{' + itemName + ':' + key + '}"', JSON.stringify(item[key]));

                else
                    paramsString = self.replaceAll(paramsString, '{' + itemName + ':' + key + '}', item[key]);
            }
        } else {
            paramsString = self.replaceAll(paramsString, '{' + itemName + '}', String(item));
        }

        paramsString = paramsString.replace(/(\"\s*)\{item\:(\w+)\}(\s*\")/g, function (match, p1, p2, p3, offset, string) {

            var value;

            if (item)
                value = item[p2] || '';

            else
                value = '';

            if (typeof value == 'object')
                value = self.stringify(value);
            else if (Array.isArray(value))
                value = value;

            else
                value = p1 + value + p3; // p1 and p3 are the " matched by (\"\s*) and (\s*\")

            return value;
        });

        /*
        specialPattern = new RegExp('\{'+itemName+'\:([^\}]*)\}', 'g');
        paramsString = paramsString.replace(specialPattern, function(match, p2, offset, string) {
        //paramsString = paramsString.replace(/\{item\:([^\}]*)\}/g, function(match, p2, offset, string) {

            var value;

            if (item)
                value = item[p2] || '';
            else
                value = '';

            if (typeof value == 'object')
                value = self.stringify(value);

            return value;
        }); */
        if (self.isJson(paramsString))
            return self.parse(paramsString);

        else
            return paramsString;
        //if (typeof params == 'object') return self.parse(paramsString); else return paramsString;
    };

    /* 
/*
 Method: replaceItems
Description: Functionality for replaceItems.
@param {any} objWithNames - Description for objWithNames
@param {any} name - Description for name
@returns {any} - Result of the operation
 */
this.replaceItems = function (objWithNames, name) {
        //self.log('replaceItems');
        var paramString = self.stringify(objWithNames);
        //self.log(paramString);
        if (!name) name = '';
        paramString = self.replaceAll(paramString, '{item}', name);
        //self.log(paramString);
        //paramString = self.replaceTags({text:paramString});
        //self.log(paramString);
        //paramString = self.replaceProperties(paramString);
        //self.log(paramString);
        if (typeof objWithNames == 'object') return self.parse(paramString); else return paramString;
    } */
    
/*
 Method: color
Description: Functionality for color.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.color = function (params, args) {
        var paramsReplaced = self.replaceProperties(params, args);
        //if (self.json.styles.colors)
        //    return self.json.styles.colors[paramsReplaced];
        if (self.json.data && self.json.data.colors)
            return self.json.data.colors[paramsReplaced];
    };

    // ----------------------------
    // ELEMENT METHODS
    // ----------------------------
    
/*
 Method: empty
Description: Functionality for empty.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.empty = function (params) {
        /* self.log('empty');
        self.log(params); */
        var el = self.query(params);
        if (el) {
            while (el.firstChild)
                el.removeChild(el.firstChild);
        } else {
            self.log('"empty" is unable to select element');
            self.log(params);
        }
    };

    
/*
 Method: isArray
Description: Functionality for isArray.
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
this.isArray = function (value) {
        return Array.isArray(self.js(value));
    };

    
/*
 Method: isVisible
Description: Functionality for isVisible.
@param {any} el - Description for el
@returns {any} - Result of the operation
 */
this.isVisible = function (el) {
        return (el.style.display !== 'none' && !self.hasClass(el, 'd-none'));
    };

    
/*
 Method: isHidden
Description: Functionality for isHidden.
@param {any} el - Description for el
@returns {any} - Result of the operation
 */
this.isHidden = function (el) {
        return (el.offsetParent === null);
    };

    
/*
 Method: isDisplay
Description: Functionality for isDisplay.
@param {any} el - Description for el
@param {any} displayValue - Description for displayValue
@returns {any} - Result of the operation
 */
this.isDisplay = function (el, displayValue) {
        //var style = window.getComputedStyle(el);
        /* self.log('isDisplay');
        self.log('el.style');
        self.log(el.style);
        self.log('displayValue');
        self.log(displayValue); */
        return (el.style.display === displayValue);
    };

    
/*
 Method: inViewport
Description: Functionality for inViewport.
@param {any} el - Description for el
@returns {any} - Result of the operation
 */
this.inViewport = function (el) {
        const rect = el.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)

        );
    };

    
/*
 Method: outViewport
Description: Functionality for outViewport.
@param {any} el - Description for el
@returns {any} - Result of the operation
 */
this.outViewport = function (el) {
        return !self.outViewport(el);
    };

    
/*
 Method: notClass
Description: Functionality for notClass.
@param {any} el - Description for el
@param {any} className - Description for className
@returns {any} - Result of the operation
 */
this.notClass = function (el, className) {
        return !self.hasClass(el, className);
    };

    
/*
 Method: hasClass
Description: Functionality for hasClass.
@param {any} el - Description for el
@param {any} className - Description for className
@returns {any} - Result of the operation
 */
this.hasClass = function (el, className) {
        if (el.classList)
            return el.classList.contains(className);

        else
            return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
    };

    
/*
 Method: addClass
Description: Functionality for addClass.
@param {any} el - Description for el
@param {any} className - Description for className
@returns {any} - Result of the operation
 */
this.addClass = function (el, className) {
        if (el.classList)
            el.classList.add(className);
        else if (!self.hasClass(el, className)) el.className += " " + className;
    };

    
/*
 Method: removeClass
Description: Functionality for removeClass.
@param {any} el - Description for el
@param {any} className - Description for className
@returns {any} - Result of the operation
 */
this.removeClass = function (el, className) {
        if (el.classList)
            el.classList.remove(className);
        else if (self.hasClass(el, className)) {
            var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
            el.className = el.className.replace(reg, ' ');
        }
    };

    
/*
 Method: toggleClass
Description: Functionality for toggleClass.
@param {any} el - Description for el
@param {any} className - Description for className
@returns {any} - Result of the operation
 */
this.toggleClass = function (el, className) {
        if (self.hasClass(el, className))
            self.removeClass(el, className);

        else
            self.addClass(el, className);
    };

    this.choose = 
/*
 Method: select
Description: Functionality for select.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.select = function (params, selectorParams, args) {
        // "choose OR options / "select" is deprecated because select is an html tag

        if (Array.isArray(params)) {
            for (var obj of params)
                self.choose(obj, args);
        } else {

            var selId, selClass, selValue;

            selValue = params['data-value'];  
            if (params.id) selId = self.replaceProperties(params.id, args);
            if (params.class) selClass = self.replaceProperties(params.class, args);
            if (selValue) selValue = self.replaceProperties(selValue, args);

            if (params.selection) {

                var selectionClass = params.selection;

                var selector = self.selector({ class: selClass });
                var elements = document.querySelectorAll('.' + selClass);  
                elements.forEach(function (element, index) {
                    self.removeClass(element, selectionClass);
                });

                if (selId || selValue) {
                    var selector = self.selector({ id: selId, class: selClass, 'data-value': selValue });

                    var elements = document.querySelectorAll(selector);
                    elements.forEach(function (element, index) {
                        self.addClass(element, selectionClass);
                    });
                }
            } else {

                self.hide({
                    "selector": {
                        "class": selClass
                    }
                });
                self.show({
                    "selector": {
                        "id": selId,
                        "class": selClass,
                        "data-value": selValue
                    }
                });

            }

            self.uiUpdate();  

        }

    };

    
/*
 Method: toggle
Description: Functionality for toggle.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.toggle = function (params, selectorParams, args) {
        self.log('toggle');
        self.log(params);
        if (Array.isArray(params)) {
            for (var index in params)
                self.toggle(params[index], selectorParams, args);
        } else {
            var selector;
            if (typeof params == 'string')
                selector = params;

            else
                selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
            var element = self.query(selector);

            if (self.isVisible(element))

                self.out(params, selectorParams, args);

            else

                self.in(params, selectorParams, args);

        }
    };

    
/*
 Method: in
Description: Functionality for in.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.in = function (params, selectorParams, args) {

        if (Array.isArray(params)) {
            for (var index in params)
                self.in(params[index], selectorParams, args);
        } else {
            var selector;
            if (typeof params == 'string')
                selector = params;

            else
                selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);

            var transition = params.transition || 'fadeIn';
            var duration = params.duration || '500ms';

            var start = [{
                "show": {
                    "selector": selector,
                }
            }];
            var end = [
                {
                    "do": self.resizeEvent
                }
            ];
            if (params.on && params.on.start)
                start.push(params.on.start);
            if (params.on && params.on.end)
                end.push(params.on.end);

            var animation = {
                "animate": {
                    "selector": selector,
                    "transition": transition,
                    "duration": duration,
                    "on": {
                        "start": start,
                        "end": end
                    }
                }
            };

            self.do(animation, undefined, args);

        }

    };

    
/*
 Method: out
Description: Functionality for out.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.out = function (params, selectorParams, args) {

        if (Array.isArray(params)) {
            for (var index in params)
                self.out(params[index], selectorParams);
        } else {

            var selector;
            if (typeof params == 'string')
                selector = params;

            else
                selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);

            var transition = params.transition || 'fadeOut';
            var duration = params.duration || '500ms';

            var start = [];
            var end = [
                {
                    "hide": {
                        "selector": selector,
                    }
                },
                {
                    "do": self.resizeEvent
                }
            ];
            if (params.on && params.on.start)
                start.push(params.on.start);
            if (params.on && params.on.end)
                end.push(params.on.end);

            var animation = {
                "animate": {
                    "selector": selector,
                    "transition": transition,
                    "duration": duration,
                    "on": {
                        "start": start,
                        "end": end
                    }
                }
            };

            self.do(animation, undefined, args);
        }
    };

    
/*
 Method: show
Description: Functionality for show.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.show = function (params, selectorParams, args) {
        if (Array.isArray(params)) {
            for (var index in params)
                self.show(params[index], selectorParams);
        } else {
            var selector;
            if (typeof params == 'string')
                selector = params;

            else
                selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
            var elements = self.queryAll(selector);
            elements.forEach(function (element, index) {
                self.removeClass(element, 'hidden');
                self.removeClass(element, 'opacity-0');
                self.removeClass(element, 'invisible');
                self.removeClass(element, 'd-none');  

                element.style.visibility = 'visible';

            });
        }
    };

    
/*
 Method: hide
Description: Functionality for hide.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.hide = function (params, selectorParams, args) {
        if (Array.isArray(params)) {
            for (var index in params)
                self.hide(params[index], selectorParams);
        } else {
            var selector;
            if (typeof params == 'string')
                selector = params;

            else
                selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
            var elements = self.queryAll(selector);
            elements.forEach(function (element, index) {
                self.addClass(element, 'hidden');

            });
        }
    };

    
/*
 Method: matchMedia
Description: Functionality for matchMedia.
@param {any} mediaAction - Description for mediaAction
@param {any} mediaEvent - Description for mediaEvent
@returns {any} - Result of the operation
 */
this.matchMedia = function (mediaAction, mediaEvent) {
        const mediaQuery = window.matchMedia(mediaEvent);
        mediaQuery.addListener(function (e) {

            if (e.matches) {
                self.do(mediaAction);

            }
        });

    };

    
/*
 Method: addFirebaseTag
Description: Functionality for addFirebaseTag.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.addFirebaseTag = function (params) {

        var dataObj = params.database;

        if (dataObj) {

            dataObj = self.replaceProperties(dataObj);

            var path, success;
            if (typeof dataObj == 'string') {
                path = dataObj;
            } else {
                path = dataObj.path;
                success = dataObj.success;
            }

            if (path) {
                var selector = self.selector(params, undefined, false);

                var elements = self.queryAll(selector);

                if (elements)
                    elements.forEach(function (element, index) {

                        var listenerObj = {
                            path: path

                        };

                        var elementsDataPath = document.querySelectorAll('[data-firebase="' + path + '"]');

                        if (elementsDataPath.length == 0) {
                            listenerObj.action = [

                            ];

                            if (dataObj.success)
                                listenerObj.action.push(dataObj.success);

                        }

                        if (element) {
                            element.setAttribute('data-firebase', path);
                            self.dataStorage.set(element, 'data-template', params);
                        } else {
                            self.log('!element selector:' + selector + ' / firebase path:' + path);
                            self.log('params');
                            self.log(params);
                        }

                        if (dataObj.orderByChild !== undefined) listenerObj.orderByChild = dataObj.orderByChild;
                        if (dataObj.equalTo !== undefined) listenerObj.equalTo = dataObj.equalTo;

                        self.firebaseAddListener(listenerObj);
                    });

            } else {
                self.log('data object requires path parameter');
                self.log(params);
            }
        }
    };

    
/*
 Method: dbObject
Description: Functionality for dbObject.
@param {any} path - Description for path
@returns {any} - Result of the operation
 */
this.dbObject = function (path) {
        var pathString = 'self.json.var.db.' + self.replaceAll(path, '/', '.');
        return self.docElement(pathString);
    };

    
/*
 Method: only
Description: Functionality for only.
@param {any} permissions - Description for permissions
@param {any} user - Description for user
@returns {any} - Result of the operation
 */
this.only = function (permissions, user) {

        if (user) {
            return (user == self.user('uid'));
        } else {
            var permission = (permissions) ? String(permissions).split(',') : false;
            return (!permission || (permissions.indexOf(self.userRole()) >= 0));
        }
    };

    
/*
 Method: extendJsonFromElement
Description: Functionality for extendJsonFromElement.
@param {any} p - Description for p
@returns {any} - Result of the operation
 */
this.extendJsonFromElement = function (p) {

        var params = self.cloneObject(p);

        if (params.resources) self.extendJson(self.json, { "resources": params.resources });
        if (params.plugins) self.extendJson(self.json, { "plugins": params.plugins });
        if (params.parts) self.extendJson(self.json, { "parts": params.parts });
        if (params.blocks) self.extendJson(self.json, { "blocks": params.parts });  
        if (params.shortcuts) self.extendJson(self.json, { "shortcuts": params.shortcuts });
        if (params.css) self.extendJson(self.json, { "css": params.css });
        if (params.setup) self.extendJson(self.json, { "setup": params.setup });
        if (params.data) self.extendJson(self.json, { "data": params.data });
        if (params.var) self.extendJson(self.json, { "var": params.var });
        if (params.texts) self.extendJson(self.json, { "texts": params.texts });  

    };

    
/*
 Method: query
Description: Functionality for query.
@param {any} selector - Description for selector
@param {any} element - Description for element
@returns {any} - Result of the operation
 */
this.query = function (selector, element) {

        if (selector) {
            if (typeof selector == 'object')
                selector = self.selector(selector, undefined, true);

            var routes = selector.split(/[ >]+/);
            if (!element) element = document;

            for (var index in routes) {

                if (element && element[0]) element = element[0];

                if (element && element.isConnected) {

                    var route = routes[index];

                    var routeParts = route.match(/(.*):eq\((\d*)\)/);

                    if (routeParts) {
                        var routeElement = String(routeParts[1]);
                        var routeIndex = Number(routeParts[2]);

                        if (routeElement.startsWith('.'))
                            routeElement = '.' + self.classSelector(routeElement);

                        element = element.querySelectorAll(routeElement)[routeIndex];

                    } else {

                        if (route.startsWith('.'))
                            route = '.' + self.classSelector(route);
                        element = element.querySelectorAll(route)[0];
                    }
                } else {
                    element = undefined;
                }
            }

            if (!element)
                self.log('can\'t find the element "' + selector + '" ("query" function)');

            return element;
        } else {

        }
    };

    
/*
 Method: queryAll
Description: Functionality for queryAll.
@param {any} selector - Description for selector
@param {any} element - Description for element
@returns {any} - Result of the operation
 */
this.queryAll = function (selector, element) {

        if (selector) {

            if (Array.isArray(selector)) {
                let elements = self.queryAll(selector[0]);
                selector.shift();
                for (let str of selector) {
                    elements = Array.prototype.slice.call(elements).concat(Array.prototype.slice.call(self.queryAll(str)));

                }

                return elements;

            } else {

                var routes = selector.split(/[ >]+/);
                if (!element) element = document;

                for (var index in routes) {

                    if (element && element[0]) element = element[0];

                    if (element && element.isConnected) {

                        var route = routes[index];

                        var routeParts = route.match(/([^:]*):eq\((\d*)\)/);

                        if (routeParts) {
                            var routeElement = String(routeParts[1]);
                            var routeIndex = Number(routeParts[2]);

                            if (routeElement.startsWith('.'))
                                routeElement = '.' + self.classSelector(routeElement);

                            element = element.querySelectorAll(routeElement)[routeIndex];

                            if (index == (routes.length - 1) && element) {

                                element = [element];
                            }

                        } else {

                            if (route.startsWith('.'))
                                routeElement = '.' + self.classSelector(route);

                            element = element.querySelectorAll(route);
                        }
                    } else {

                        element = [];
                    }
                }

                if (!element)
                    self.log('No element selected by ' + selector);

                return element;
            }
        } else {
            console.log('%cDOM queryAll requires selector argument', 'color: orange');

        }
    };

    
/*
 Method: jitcss
Description: Functionality for jitcss.
@param {any} classes - Description for classes
@returns {any} - Result of the operation
 */
this.jitcss = function (classes) {

        if (classes.indexOf('-[') > -1) {
            let classesArr = classes.split(' ');  

            self.log('jitcss');

            for (cl of classesArr) {
                self.log(cl);
                let par = [...cl.matchAll(new RegExp('([^-]+)[\-][\[](.+)\]', 'g'))];
                self.log('jitcss');
                self.log(par);
            }
        }

    };

    
/*
 Method: compileClasses
Description: Functionality for compileClasses.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
this.compileClasses = function (string) {

        var classesString = string;

        if (self.json.shortcuts && self.json.shortcuts.class) {
            for (shortcut in self.json.shortcuts.class) {
                if (string.indexOf(shortcut) > -1) {
                    classesString = self.replaceAll(string, shortcut, self.json.shortcuts.class[shortcut]);
                }
            }
        }

        if (string.indexOf('(') >= 0) {

            classesString = '';
            let classesArr = string.split(' ');
            for (cl of classesArr) {

                if (cl.indexOf('(') >= 0 && cl.endsWith(')')) {
                    let classArr = cl.split('(');
                    classArr[1] = classArr[1].slice(0, -1);  

                    if (Boolean(self.js(classArr[1]))) {
                        if (classesString !== '') classesString += ' ';
                        classesString += classArr[0];
                    }

                } else {
                    if (classesString !== '') classesString += ' ';
                    classesString += cl;
                }
            }

            if (string == classesString) {
                self.log('wrong dynamic class syntax');
                self.log(string);
            }

        }

        return classesString;

    };

    
/*
 Method: createElement
Description: Functionality for createElement.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.createElement = function (params, selectorParams) {

        let selector = params.selector || params.container;
        var container = selector || self.selector(selectorParams);
        var containerElement = self.query(container);
        var newSelector = self.selector(params, selectorParams);  

        var newElements = [];

        if (containerElement) {

            if (!params) params = {};

            var element = document.createElement(params.tag);

            containerElement.appendChild(element);

            if (params.attr) {

                if (params.attr.class) {

                    params.attr.class = self.compileClasses(params.attr.class);
                }
            }

            self.dataStorage.set(element, 'code', params);
            self.dataStorage.set(element, 'key', params.key);

            newElements.push(element);

            if (params.attr) self.setAttributes(newElements, params.attr);
            if (params.style) self.css(params, newSelector);  

        }

        return newElements;

    };

    this.do = this.action = 
/*
 Method: execute
Description: Functionality for execute.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.execute = function (params, selectorParams, args) {

        if (params !== undefined && params !== null) {  

            if (typeof params == 'string')
                params = self.replaceProperties(params, args);

            let pluginsRequired = self.pluginsRequired(params);

            if (pluginsRequired.length > 0)

                self.pluginsLoader(pluginsRequired, self.do, [params, selectorParams, args]);
            else {

                if (Array.isArray(params))
                    for (var obj of params)
                        self.do(obj, selectorParams, args);  
                else if (typeof params == 'object') {

                    if (params.if == undefined || self.if(params.if)) {  

                        if (self.only(params.roles, params.user)) {

                            if (params.tag) {

                                var selector = self.selector(params, selectorParams);

                                self.extendJsonFromElement(params);  

                                var newElements = self.createElement(params, selectorParams);

                                if (params.matchMedia) {
                                    for (var mediaEvent in params.matchMedia) {
                                        self.matchMedia(params.matchMedia[mediaEvent], mediaEvent);
                                    }
                                }

                                if (params.html) {

                                    self.html(params.html, selector, args);

                                } else {

                                    for (var par in params)  
                                        if ((nodes.exclude.indexOf(par) < 0) && (nodes.params.indexOf(par) < 0))
                                            self.nested({ key: par, obj: params[par], selector: { selector: selector }, args: args });

                                }

                                self.on(params.on, { selector: selector });

                            } else {

                                var selector = self.selector(params, selectorParams);

                                for (var par in params)
                                    if ((nodes.exclude.indexOf(par) < 0) && (nodes.params.indexOf(par) < 0))
                                        self.nested({ key: par, obj: params[par], selector: selector, args: args });

                                if (params.on)
                                    self.on(params.on, { selector: selector });

                            }

                        }  

                    }

                } else if (typeof params == 'function')
                    return params(selectorParams);

                else
                    self.log('can\'t execute: ' + params);
            }

        }
    };

    
/*
 Method: callback
Description: Functionality for callback.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.callback = function (params) {

        if (params) {
            var path = params.to;
            if (params.var) path = 'var ' + params.var;  
            let selector;
            let element = params.element;  
            if (element && element.isConnected) params.value = self.elementToSelector(element);
            if (path) {
                var context = self.context(path);
                self.element({ path: path, value: params.value, root: context });
            }
            if (params.do)
                self.do(params.do, selector);
        }
    };

    
/*
 Method: nested
Description: Functionality for nested.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.nested = function (params) {

        let pluginsRequired = self.pluginsRequired(params.key);
        if (pluginsRequired.length > 0) {
            self.pluginsLoader(pluginsRequired, self.nested, [params]);
        } else {

            if (params.key.startsWith('set ')) {
                self.setData({ key: params.key.substr(4), value: params.obj });
            } else if (params.key.startsWith('var ') || params.key.startsWith('data ') || params.key.startsWith('set ')) {

                console.log('%cdeprecated key: "' + params.key + '". Use "set key": "value"}', 'color:orange');

                self.setData({ key: params.key, value: params.obj });

            } else {

                var method = self.element({ path: params.key, root: self.methods });

                if (method) {  
                    self.log('METHOD: ' + params.key, 'grey');
                    var methodArgs = params.obj;

                    method(methodArgs, params.selector, params.args);

                } else {

                    let nested;
                    let part;

                    var jsonicPart = false;
                    if (self.json.parts) {  
                        part = self.element({ path: self.replaceAll(params.key, ':', '.'), root: self.json.parts });

                    }

                    if (part) {
                        self.log('PART: ' + params.key, 'grey');

                        var partObj = params.obj;

                        partObj = self.replaceProperties(partObj);

                        partObj.selector = partObj.selector || params.selector;

                        if (partObj.setup) partObj = partObj.setup;

                        nested = self.replacePropertyWithPrefix(part, 'setup', partObj);  
                        nested = self.replacePropertyWithPrefix(nested, 'arguments', partObj);  

                        self.extendJsonFromElement(nested);  

                        self.do(nested, params.selector, partObj);

                    } else {  

                        if (self.json && self.json.functions && params.key && self.json.functions[params.key]) {

                            self.log('FUNCTION: ' + params.key, 'grey');

                            let args = self.replaceProperties(params.obj, undefined, true);  

                            if (!args || !Array.isArray(args)) args = [args];

                            if (self.json.functions[params.key].requires) {
                                let pluginsRequired = [];
                                for (let plugin of self.json.functions[params.key].requires) {
                                    if (!self.pluginsLoaded[plugin])
                                        pluginsRequired.push({ "name": plugin, version: "" });
                                }

                                if (pluginsRequired.length > 0)

                                    self.pluginsLoader(pluginsRequired, self.function, [{ name: params.key, arguments: args }]);
                                else {
                                    self.log('Plugins already loaded: ' + params.key, 'grey');
                                    self.function({ name: params.key, arguments: args });

                                }
                            } else {

                                self.log('Function without plugin required: ' + params.key, 'grey');
                                self.function({ name: params.key, arguments: args });  

                            }

                        } else {
                            var windowFunction = self.element({ path: params.key, root: window });
                            if (typeof windowFunction === 'function') {
                                self.log('WINDOW function: ' + params.key, 'grey');
                                windowFunction(self.replaceProperties(params.obj, undefined, true));  

                            } else if (windowFunction !== undefined) {
                                self.log('window.' + params.key + ' is not a function', 'red');

                            } else {

                                if (params.selector) {  
                                    self.log('TAG: ' + params.key, 'grey');

                                    let pluginsRequired = self.pluginsRequiredByTag(params.obj, params.key);
                                    if (pluginsRequired.length > 0) {
                                        let htmlTagParams = [params.obj, params.selector, params.key];
                                        self.pluginsLoader(pluginsRequired, self.htmlTag, htmlTagParams);
                                    } else {
                                        self.htmlTag(params.obj, params.selector, params.key);  

                                    }
                                } else {
                                    self.log(params.key + ' not found', 'red');
                                }
                            }
                        }
                    }
                }
            }

        }
    };

    
/*
 Method: setData
Description: Functionality for setData.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.setData = function (params) {

        let setObj = {};
        setObj[params.key] = params.value;
        self.set(setObj);

    };

    this.partContainers = {};

    
/*
 Method: pluginsRequiredByTag
Description: Functionality for pluginsRequiredByTag.
@param {any} params - Description for params
@param {any} tagKey - Description for tagKey
@returns {any} - Result of the operation
 */
this.pluginsRequiredByTag = function (params, tagKey) {

        if (self.json.resources) {

            const pluginsTags = self.json.resources.pluginsTags;
            const pluginsAttr = self.json.resources.pluginsAttr;
            var pluginsRequired = [];

            if (pluginsTags[tagKey] && !self.pluginsLoaded[pluginsTags[tagKey].name])
                pluginsRequired.push(pluginsTags[tagKey]);

            if (typeof params == 'string') {

            } else if (typeof params == 'object') {

                var objArray;
                if (Array.isArray(params)) objArray = params; else objArray = [params];

                for (var obj of objArray) {
                    if (obj.attr) {  
                        var attr = 'data-icon';
                        if (obj.attr[attr]) {
                            if (pluginsAttr[attr] && !self.pluginsLoaded[pluginsAttr[attr].name])
                                pluginsRequired.push(pluginsAttr[attr]);
                        }
                    }
                }
            }
            return pluginsRequired;
        } else {

            return false;
        }
    };

    
/*
 Method: calendar
Description: Functionality for calendar.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.calendar = function (params, args) {
        const localeName = (self.json.setup.language !== 'en') ? self.json.setup.language : 'en-US';
        let calendarArr, todayIndex;

        if (params.unit) {
            switch (params.unit) {
                case 'month':
                    const monthFormat = params.format || 'long';
                    var format = new Intl.DateTimeFormat(localeName, { month: monthFormat }).format;
                    calendarArr = [...Array(12).keys()].map((m) => format(new Date(Date.UTC(2021, m))));
                    todayIndex = new Date().getMonth();
                    break;
                case 'weekday':
                    const weekdayFormat = params.format || 'long';
                    var format = new Intl.DateTimeFormat(localeName, { weekday: weekdayFormat }).format;
                    calendarArr = [...Array(7).keys()].map((day) => format(new Date(Date.UTC(2021, 5, day))));
                    todayIndex = self.weekday();
                    break;
                default:
                    self.log('"calendar" function can\'t recognize "unit" parameter');
                    self.log(params.unit);
                    break;
            }

            if (calendarArr) {
                if (params.index !== undefined) {
                    params.index = self.js(params.index);
                    let index = Number(self.replaceProperties(params.index, args));
                    return calendarArr[index];
                } else {
                    return calendarArr[todayIndex];
                }
            } else {
                self.log('"calendar" function can\'t recognize "unit" or "format" parameters');
            }

        } else {
            self.log('"calendar" function requires "unit" parameter');
        }

    };

    
/*
 Method: month
Description: Functionality for month.
@param {any} params - Description for params
@param {any} monthFormat = 'long' - Description for monthFormat = 'long'
@returns {any} - Result of the operation
 */
this.month = function (params, monthFormat = 'long') {
        const localeName = (self.json.setup.language !== 'en') ? self.json.setup.language : 'en-GB';
        const format = new Intl.DateTimeFormat(localeName, { month: monthFormat }).format;
        const weekdayArr = [...Array(12).keys()].map((m) => format(new Date(Date.UTC(2021, m))));
        if (params !== undefined) {
            var index = Number(self.replaceProperties(params));
            return weekdayArr[index];
        }
        else
            return weekdayArr;
    };

    
/*
 Method: weekday
Description: Functionality for weekday.
@param {any} params - Description for params
@param {any} weekdayFormat - Description for weekdayFormat
@returns {any} - Result of the operation
 */
this.weekday = function (params, weekdayFormat) {

        const localeName = (self.json.setup.language !== 'en') ? self.json.setup.language : 'en-GB';
        const format = new Intl.DateTimeFormat(localeName, { weekday: weekdayFormat }).format;
        const weekdayArr = [...Array(7).keys()].map((day) => format(new Date(Date.UTC(2021, 5, day))));
        var result;
        if (params !== undefined) {
            var index = Number(self.replaceProperties(params));
            result = weekdayArr[index];
        }
        else
            result = weekdayArr;

        return result;

    };

    
/*
 Method: method
Description: Functionality for method.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.method = function (params, args) {

        var value;

        if (typeof params == 'string') {
            return self.docElement(params);
        } else {

            if ((params.if == undefined) || self.if(params.if, undefined, args)) {

                if (self.notEmptyObject(params)) {
                    var actionKey = Object.keys(params)[0];  
                    var actionFunction = self.docElement(actionKey);  
                    var actionValue = params[actionKey];

                    if (!Array.isArray(actionValue))
                        actionValue = [actionValue];
                    if (actionFunction) {
                        if (typeof actionFunction == 'function')
                            return actionFunction(...actionValue);

                        else
                            return actionFunction;
                    } else {
                        self.log('unknown function');
                        self.log(params);
                        return params;  
                    }
                } else {

                    return undefined;
                }
            }
        }

    };

    
/*
 Method: Number
Description: Functionality for Number.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.Number = function (params, args) {
        self.log('Number');
        return self.method({ "Number": params }, args);
    };

    
/*
 Method: String
Description: Functionality for String.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.String = function (params, args) {
        self.log('String');
        return self.method({ "String": params }, args);

    };

    
/*
 Method: Math
Description: Functionality for Math.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.Math = function (params, args) {
        return self.method(params, args, 'Math');

    };

    
/*
 Method: dayjs
Description: Functionality for dayjs.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.dayjs = function (params, args) {

        return self.method(params, args, 'dayjs');

    };

    
/*
 Method: moment
Description: Functionality for moment.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.moment = function (params, args) {

        return self.method(params, args, 'moment');

    };

    
/*
 Method: dynamicCss
Description: Functionality for dynamicCss.
@param {any} selector - Description for selector
@param {any} property - Description for property
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
this.dynamicCss = function (selector, property, value) {
        if (value.indexOf('{') >= 0) {
            var resizeData = self.dataStorage.get(element, "resize");

            if (!resizeData) resizeData = {};
            resizeData[property] = value;
            var resizeData = self.dataStorage.set(element, "resize", resizeData);

            if (!self.resizeActions[selector]) self.resizeActions[selector] = {};
            self.resizeActions[selector][property] = value;
            var result = self.replaceProperties(value) || '';

        } else {
            var style = {};
            style[property] = value;
            self.css({
                style: style
            });

        }
    };

    
/*
 Method: li
Description: Functionality for li.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.li = function (params, selectorParams) {
        if (Array.isArray(params)) {
            for (var index in params)
                self.li(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = (typeof params.items == 'string') ? self.json.items[params.items] : params.items;

                delete params.items;
                for (var index in itemsArray) {
                    if (self.only(itemsArray[index].roles))
                        self.li(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') { params = { html: params }; };
                params.tag = 'li';

                self.do(params, selectorParams);
            }
        }
    };

    
/*
 Method: remove
Description: Functionality for remove.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.remove = function (params, selectorParams, args) {
        var selector = self.selector(params.selector || self.selector(selectorParams));
        if (selector) {
            var elements = self.queryAll(selector);
            if (elements) {
                elements.forEach(function (element, index) {
                    element.remove();
                });
            }
        }
    };

    
/*
 Method: dispatchEvent
Description: Functionality for dispatchEvent.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.dispatchEvent = function (params, selectorParams, args) {
        var selector = self.selector(params.selector || self.selector(selectorParams));
        if (selector) {
            var elements = self.queryAll(selector);
            if (elements) {
                elements.forEach(function (element, index) {
                    element.dispatchEvent(new Event(params.name));
                });
            }
        }
    };

    
/*
 Method: html
Description: Functionality for html.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.html = function (params, selectorParams, args) {

        if (params) {

            if (Array.isArray(params)) {

                for (var obj of params)
                    self.html(obj, selectorParams, args);
            } else {

                var container = self.selector(params.selector || params.container) || self.selector(selectorParams);

                if (container) {
                    var element = self.query(container);

                    if (element) {

                        if (typeof params == 'string') {

                            var paramsCompiled = self.replaceProperties(params, args);

                            if (paramsCompiled) {
                                if (typeof paramsCompiled == 'object') {

                                    self.do(paramsCompiled, { selector: container }, args);

                                } else if (typeof paramsCompiled == 'string') {
                                    element.innerHTML += paramsCompiled;  
                                }
                            }

                        } else {

                            if (params.empty || params.do == 'empty') {
                                element.innerHTML = '';
                                delete params.empty;
                            }

                            if (params.tag) {
                                self.do(params, container, args);

                            }
                            else if (params.html) {

                                var htmlCompiled = self.replaceProperties(params.html, args);

                                self.html(htmlCompiled, { selector: container });
                            } else if (params.lang) {

                                var textLang = params.lang[self.json.setup.language] || params.lang['en'] || params.lang[Object.keys[0]];
                                if (Array.isArray(textLang)) {
                                    var paragraphs = [];
                                    for (var paragraph of textLang)
                                        paragraphs.push({ p: paragraph });
                                    self.html(paragraphs, selectorParams, args);
                                }
                                else
                                    element.innerHTML = self.replaceProperties(textLang, args);

                                if (element) {
                                    self.attribute(element, 'data-text', true);  
                                    self.dataStorage.set(element, 'params', params);
                                }

                            } else if (params.text) {  

                                element.textContent = self.replaceProperties(params.text, args);

                            } else if (params.append) {  

                                element.innerHTML += self.replaceProperties(params.append, args);

                            } else if (params.prepend) {  

                                element.innerHTML = self.replaceProperties(params.prepend, args) + element.innerHTML;

                            } else if (params.blocks) {  
                                self.blocks(params.blocks, { selector: container }, args);
                            } else {

                                let nestedParams = self.cloneObject(params);
                                if (nestedParams.style) delete nestedParams.style;
                                if (nestedParams.attr) delete nestedParams.attr;

                                console.warn('UNKNOWN TAG');
                                console.warn(params);

                                self.do(nestedParams, { selector: container }, args);

                                if (element.isConnected) {

                                    if (params.attr) self.setAttributes(element, params.attr, args);
                                    if (params.style) self.css(params, selectorParams, args);  

                                }

                            }

                        }
                    } else {  
                    }
                } else {
                    self.log('"html" object requires selector property', 'red');
                    self.log(params);

                }

            }
        } else {
            self.log('"html" object without params');
        }

    };

    
/*
 Method: setAttributes
Description: Functionality for setAttributes.
@param {any} elements - Description for elements
@param {any} attrs - Description for attrs
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.setAttributes = function (elements, attrs, args) {
        for (var attribute in attrs)
            if (attrs[attribute])
                self.attribute(elements, attribute, self.replaceResult(attrs[attribute], args));
            else
                self.attribute(elements, attribute, "");

    };

    
/*
 Method: attribute
Description: Functionality for attribute.
@param {any} elements - Description for elements
@param {any} attrId - Description for attrId
@param {any} attrValue - Description for attrValue
@returns {any} - Result of the operation
 */
this.attribute = function (elements, attrId, attrValue) {
        if (typeof attrValue !== undefined) {
            if (!Array.isArray(elements)) elements = [elements];
            elements.forEach(function (element, index) {
                if (typeof attrValue == 'boolean' && attrValue) {  
                    element.setAttribute(attrId, "");
                } else {
                    element.setAttribute(attrId, self.replaceProperties(attrValue));
                }
            });
        } else {
            if (!Array.isArray(elements))
                return elements.getAttribute(attrId);  

            else
                return elements[0].getAttribute(attrId);
        }
    };

    
/*
 Method: attr
Description: Functionality for attr.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.attr = function (params, selectorParams, args) {

        self.css(self.replaceProperties(params), selectorParams, args);
    };

    
/*
 Method: styleElement
Description: Functionality for styleElement.
@param {any} params - Description for params
@param {any} element - Description for element
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.styleElement = function (params, element, args) {
        for (var styleName in params) {
            if (styleName.startsWith('--'))
                element.style.setProperty(styleName, self.replaceProperties(params[styleName], args));

            else
                element.style[styleName] = self.replaceProperties(params[styleName], args);
        }
    };

    
/*
 Method: css
Description: Functionality for css.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.css = function (params, selectorParams, args) {

        if (Array.isArray(params)) {
            for (var obj of params)
                self.css(obj, selectorParams, args);

        } else {

            var selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
            var elements = self.queryAll(selector);

            if (elements) {
                elements.forEach(function (element, index) {

                    if (element) {

                        if (typeof params.style == 'object') {
                            self.styleElement(params.style, element, args);
                        }

                        if (params.addClass)
                            self.addClass(element, params.addClass);
                        if (params.removeClass)
                            self.removeClass(element, params.removeClass);
                        if (params.toggleClass)
                            self.toggleClass(element, params.toggleClass);

                    } else {
                        self.log('the function "css" can\'t find the selector ' + selector);
                        self.log(params);
                        self.log(selectorParams);
                    }
                });
            } else {
                self.log('"css" function is unable to select: ' + selector);
                self.log('params');
                self.log(params);
                self.log('selectorParams');
                self.log(selectorParams);
            }
        }
    };

    
/*
 Method: numberToHour
Description: Functionality for numberToHour.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.numberToHour = function (data) {
        var fraction = data.value / data.max;
        var dayMin = Math.round(24 * 60 * fraction);
        var hour = parseInt(dayMin / 60);
        var min = dayMin - hour * 60;
        var hourString = String(hour < 10 ? '0' : '') + String(hour);
        var minString = String(min < 10 ? '0' : '') + String(min);
        return hourString + ':' + minString;
    };

    
/*
 Method: hourToNumber
Description: Functionality for hourToNumber.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.hourToNumber = function (data) {
        var timeArr = data.value.split(':');
        var dayMin = (60 * Number(timeArr[0])) + Number(timeArr[1]);
        var fraction = dayMin / (24 * 60);
        return Math.round(fraction * data.max);
    };

    
/*
 Method: pannellum
Description: Functionality for pannellum.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.pannellum = function (params, selectorParams, args) {
        self.log('pannellum');
        var container = params.selector || params.container;
        var selector = self.selector(container) || self.selector(selectorParams);
        var element = self.query(selector);
        pannellum.viewer(element, params);
    };

    
/*
 Method: qrcode
Description: Functionality for qrcode.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.qrcode = function (params, selectorParams, args) {
        self.log('qrcode');
        var container = params.selector || params.container;
        var selector = self.selector(container) || self.selector(selectorParams);
        var element = self.query(selector);

        self.log(selector);
        self.log('element');
        self.log(element);

        var QRCodeOptions = {
            text: self.replaceProperties(params.text, args) || 'jsonic.io',
            width: params.size || 128,
            height: params.size || 128,
            useSVG: true,
            colorDark: params.colorDark || '#000000',
            colorLight: params.colorLight || '#ffffff',
            correctLevel: QRCode.CorrectLevel.H
        };
        self.log('QRCodeOptions');
        self.log(QRCodeOptions);

        element.textContent = '';
        var qrcode = new QRCode(element, QRCodeOptions);
        self.log('qrcode');
        self.log(qrcode);
        return qrcode;

    };

    
/*
 Method: weekdaysShort
Description: Functionality for weekdaysShort.
@param {any} length - Description for length
@param {any} caseFunction - Description for caseFunction
@returns {any} - Result of the operation
 */
this.weekdaysShort = function (length, caseFunction) {
        if (!caseFunction) caseFunction = caseUp;
        var language = 'en';
        var fixoLang = fixo.getFixoLanguage();
        var userLang = fixo.getUserLanguage();
        if (fixo.device) language = fixoLang; else language = userLang;
        var weekDays = self.cloneObject(moment.localeData(language).weekdays());

        weekDays[7] = weekDays[0];
        weekDays.shift();

        for (var i = 0; i < 7; i++) {
            if (length) weekDays[i] = weekDays[i].substr(0, length);
            if (caseFunction) weekDays[i] = caseFunction(weekDays[i]);
        }
        return weekDays;
    };

    
/*
 Method: weekDaysToString
Description: Functionality for weekDaysToString.
@param {any} days - Description for days
@returns {any} - Result of the operation
 */
this.weekDaysToString = function (days) {
        var text = '';
        for (var i in days) {
            if (days[i]) {
                if (text !== '') text += '/';
                text += self.weekdaysShort(3, capitalize)[i];
            }
        }
        return text;
    };

    var sortField;

    
/*
 Method: dynamicSort
Description: Functionality for dynamicSort.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
var dynamicSort = function (data) {
        var sortOrder = 1;
        if (data.reverse) { sortOrder = -1; }
        sortField = data.field;
        return function (a, b) {
            var result = (a[sortField] < b[sortField]) ? -1 : (a[sortField] > b[sortField]) ? 1 : 0;
            return result * sortOrder;
        };
    };

    
/*
 Method: sort
Description: Functionality for sort.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.sort = function (data) {

        if (data.object !== undefined) {
            if (Array.isArray(data.object)) {
                return data.object.sort(dynamicSort({
                    field: data.field,
                    reverse: data.reverse
                }));
            } else {
                var arr = self.objectToArray({
                    object: data.object,
                    add: data.add,
                });
                return arr.sort(dynamicSort({
                    field: data.field,
                    reverse: data.reverse
                }));
            }
        } else {
            self.log('sort of undefined');
            return [];
        }
    };

    
/*
 Method: objectToArray
Description: Functionality for objectToArray.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.objectToArray = function (data) {

        var arr = [];
        var obj = data.object;
        if (obj !== undefined) {
            for (var key in obj) {
                obj[key].id = key;  
                if (data.add) obj[key] = Object.assign(obj[key], data.add);
                arr.push(obj[key]);
            }
        }
        return arr;
    };

    
/*
 Method: equal
Description: Functionality for equal.
@param {any} obj1 - Description for obj1
@param {any} obj2 - Description for obj2
@returns {any} - Result of the operation
 */
this.equal = function (obj1, obj2) {
        if (obj1 && obj2) {
            return (JSON.stringify(obj1).replace('"', '') == JSON.stringify(obj2).replace('"', ''));
        } else {
            return (obj1 == obj2);
        }
    };

    
/*
 Method: escapeRegExp
Description: Functionality for escapeRegExp.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
var escapeRegExp = function (str) {
        return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
    };
    
/*
 Method: replaceAll
Description: Functionality for replaceAll.
@param {any} str - Description for str
@param {any} find - Description for find
@param {any} replace - Description for replace
@returns {any} - Result of the operation
 */
this.replaceAll = function (str, find, replace) {

        if (str !== undefined) {
            let result;
            if (typeof str == 'string') result = str; else result = JSON.stringify(str);

            if (str) {
                if (find) {  

                    result = String(str).replace(new RegExp(escapeRegExp(find), 'g'), replace);

                    if (typeof str == 'object' && result) return JSON.parse(result);
                    else
                        return result;
                } else {

                    return str;
                }
            }
        } else {
            return str;
        }
    };

    
/*
 Method: capitalize
Description: Functionality for capitalize.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
this.capitalize = function (str) {
        return str.replace(/\b\w/g, l => l.toUpperCase());
    };

    
/*
 Method: getPageName
Description: Functionality for getPageName.
@param {any} url - Description for url
@returns {any} - Result of the operation
 */
this.getPageName = function (url) {
        var index = url.lastIndexOf("/") + 1;
        var filenameWithExtension = url.substr(index);
        var filename = filenameWithExtension.split(".")[0];  
        return filename;  
    };

    
/*
 Method: goToMainUrl
Description: Functionality for goToMainUrl.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.goToMainUrl = function () {
        window.location = self.mainUrl;
    };

    
/*
 Method: getParameterByName
Description: Functionality for getParameterByName.
@param {any} name - Description for name
@param {any} url - Description for url
@returns {any} - Result of the operation
 */
this.getParameterByName = function (name, url) {
        if (!url) url = window.location.href;
        name = name.replace(/[\[\]]/g, "\\$&");
        var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/\+/g, " "));
    };

    
/*
 Method: paramsToObject
Description: Functionality for paramsToObject.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
var paramsToObject = function (params) {
        params = params.split('#')[0];
        return JSON.parse('{"' + params.replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}');
    };

    
/*
 Method: objectToParams
Description: Functionality for objectToParams.
@param {any} object - Description for object
@returns {any} - Result of the operation
 */
var objectToParams = function (object) {
        var str = '';
        for (var key in object) str += '&' + key + '=' + object[key];
        if (str) str = str.substr(1);
        return encodeURI(str);
    };

    
/*
 Method: getParams
Description: Functionality for getParams.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.getParams = function () {
        var params = {};
        var url = window.location.href.split('#')[0];
        if (url.indexOf('?') > -1) {
            var urlParams = decodeURI(url.split('?')[1]);
            self.log(urlParams);
            if (urlParams) {
                var k = self.getParameterByName('k', url);
                //self.log(k);
                if (k) {
                    params = self.atob(k);
                } else {
                    params = paramsToObject(urlParams);
                }
            }
        }
        return params;
    };

    
/*
 Method: atob
Description: Functionality for atob.
@param {any} k - Description for k
@returns {any} - Result of the operation
 */
this.atob = function (k) {
        self.log('atob');
        var params = paramsToObject(atob(k)); // base64 -> query -> object

        // decompress params
        if (params.h) // hotel
            params.h = String(parseInt(String(params.h), 36)); // base36
        if (params.c) // customer
            params.c = String(parseInt(String(params.c), 36)); // base36
        if (params.n) { // phone
            params.n = String(parseInt(String(params.n), 36)); // base36
            params.n = '+' + params.n;
        }
        //if (params.s) params.s  = var.services.id[params.s];
        self.log(params);
        return params;
    };

    
/*
 Method: btoa
Description: Functionality for btoa.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.btoa = function (params) {
        self.log('btoa');
        //if (params.s) params.s = var.services.id.indexOf(params.s);
        if (params.h) // hotel
            params.h = Number(params.h).toString(36); // base36
        if (params.c) // customer
            params.c = Number(params.c).toString(36); // base36

        if (params.n) { // phone
            if (params.n.startsWith('+')) params.n = params.n.substr(1);
            params.n = params.n.replace(/ /g, '');
            params.n = params.n.replace(/-/g, '');
            params.n = Number(params.n).toString(36); // base36
        }
        self.log(params);
        return btoa(objectToParams(params)); // query -> base64
    };

    /*
        
/*
 Method: encodeNum
Description: Functionality for encodeNum.
@param {any} num - Description for num
@returns {any} - Result of the operation
 */
var encodeNum = function(num) { // 0..1296 es:customer
            var hex = c.toString(36);
            return c.toString(36);
        }

        
/*
 Method: decodeNum
Description: Functionality for decodeNum.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
var decodeNum = function(str) {
            return str.parseInt(36); // .length == 1 ? "0" + hex : hex
        }
    */
    
/*
 Method: colorToRgba
Description: Functionality for colorToRgba.
@param {any} hex - Description for hex
@param {any} opacity - Description for opacity
@returns {any} - Result of the operation
 */
this.colorToRgba = function (hex, opacity) {
        if (hex == 'white') { hex = '#fff'; }
        if (hex == 'black') { hex = '#000'; }
        var c;
        if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
            c = hex.substring(1).split('');
            if (c.length == 3) {
                c = [c[0], c[0], c[1], c[1], c[2], c[2]];
            }
            c = '0x' + c.join('');
            return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + opacity + ')';
        } else {
            self.log('Bad Hex:' + hex);
        }
    };
    
/*
 Method: hueToRGB
Description: Functionality for hueToRGB.
@param {any} m1 - Description for m1
@param {any} m2 - Description for m2
@param {any} h - Description for h
@returns {any} - Result of the operation
 */
var hueToRGB = function (m1, m2, h) {
        h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
        if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
        if (h * 2 < 1) return m2;
        if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
        return m1;
    };

    
/*
 Method: HSLToRGB
Description: Functionality for HSLToRGB.
@param {any} hsl - Description for hsl
@returns {any} - Result of the operation
 */
var HSLToRGB = function (hsl) {
        var m1, m2, r, g, b;
        var h = hsl[0], s = hsl[1], l = hsl[2];
        m2 = (l <= 0.5) ? l * (s + 1) : l + s - l * s;
        m1 = l * 2 - m2;
        return [hueToRGB(m1, m2, h + 0.33333),
        hueToRGB(m1, m2, h),
        hueToRGB(m1, m2, h - 0.33333)];
    };

    
/*
 Method: packRGB
Description: Functionality for packRGB.
@param {any} rgb - Description for rgb
@returns {any} - Result of the operation
 */
var packRGB = function (rgb) {
        var r = Math.round(rgb[0] * 255);
        var g = Math.round(rgb[1] * 255);
        var b = Math.round(rgb[2] * 255);
        // '#' + 
        return (r < 16 ? '0' : '') + r.toString(16) +
            (g < 16 ? '0' : '') + g.toString(16) +
            (b < 16 ? '0' : '') + b.toString(16);
    };

    
/*
 Method: degToRGB
Description: Functionality for degToRGB.
@param {any} deg - Description for deg
@returns {any} - Result of the operation
 */
this.degToRGB = function (deg) {
        return packRGB(HSLToRGB([deg / 360, 1, 0.5]));
    };

    
/*
 Method: stripHtml
Description: Functionality for stripHtml.
@param {any} html - Description for html
@returns {any} - Result of the operation
 */
this.stripHtml = function (html) {
        var tmp = document.createElement("DIV");
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || "";
    };

    //--------------------------
    // TASK MANAGER
    //--------------------------
    
/*
 Method: log
Description: Functionality for log.
@param {any} value - Description for value
@param {any} color - Description for color
@returns {any} - Result of the operation
 */
this.log = function (value, color) {
        if (self.json.setup && self.json.setup.log) { // log enabled
            if (typeof value === 'string')
                console.log('%c' + value, 'color:' + color);

            else
                console.log(value);
        }
    };

    
/*
 Method: varlog
Description: Functionality for varlog.
@param {any} str - Description for str
@returns {any} - Result of the operation
 */
var varlog = function (str) {
        self.log(str);
        self.log(window[str]); // 'var' -> var
    };

    
/*
 Method: logJson
Description: Functionality for logJson.
@param {any} jsonPar - Description for jsonPar
@returns {any} - Result of the operation
 */
this.logJson = function (jsonPar) { self.log(JSON.stringify(jsonPar)); };

    
/*
 Method: getTimestamp
Description: Functionality for getTimestamp.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.getTimestamp = function () {
        // we need also an API to get the time
        // like .getTime()?
        return Math.floor(Date.now());
    };

    
/*
 Method: cloneObject
Description: Functionality for cloneObject.
@param {any} obj - Description for obj
@returns {any} - Result of the operation
 */
this.cloneObject = function (obj) {
        var newObj = JSON.stringify(obj);
        return self.parse(newObj);
    };

    /*------------------------
        TEXT
    ------------------------*/
    /* 
/*
 Method: caseUp
Description: Functionality for caseUp.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var caseUp = function (string) {
        if (!string) string = '';
        return string.toUpperCase();
    }

    
/*
 Method: caseDown
Description: Functionality for caseDown.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var caseDown = function (string) {
        if (!string) string = '';
        return string.toLowerCase();
    }

    
/*
 Method: capitalize
Description: Functionality for capitalize.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
var capitalize = function(string) {
        if (!string) string = '';
        return self.capitalize(string);
    }

    
/*
 Method: text
Description: Functionality for text.
@param {any} key - Description for key
@param {any} caseFunction - Description for caseFunction
@returns {any} - Result of the operation
 */
var text = function (key, caseFunction) {
        var t = self.json.text[key] || '';
        if (caseFunction) return caseFunction(t); else return t;
    } */
    
/*
 Method: setElement
Description: Functionality for setElement.
@param {any} elementString - Description for elementString
@param {any} elementValue - Description for elementValue
@returns {any} - Result of the operation
 */
this.setElement = function (elementString, elementValue) {
        var context = window;
        var namespaces = elementString.split(".");
        var func = namespaces.pop();

        if (namespaces[0] == 'frame') {
            namespaces.shift();
            var frameName = namespaces.shift();
            if (frameName && document.getElementById(frameName) !== null)
                context = document.getElementById(frameName).contentWindow;
        } else if (namespaces[0] == 'admin') { // fixo
            namespaces.shift();
            context = fixoAdmin;
        }

        for (var i = 0; i < namespaces.length; i++) {
            if (context[namespaces[i]]) { // var
                context = context[namespaces[i]];
            } else {
                if (elementValue) // scrittura
                    context[namespaces[i]] = {}; // crea contesto inesistente
            }
        }

        context[func] = elementValue;
    };

    
/*
 Method: context
Description: Functionality for context.
@param {any} path - Description for path
@returns {any} - Result of the operation
 */
this.context = function (path) {
        const re = /^(setup|var|data|texts|parts)[\.\s]/; // reserved namespaces
        if (re.test(path)) {
            /* self.log('MATCHED setup|var|data|parts');
            console.log(params); */
            return self.json;
        } else {
            return window;
        }
    };

    
/*
 Method: element
Description: Functionality for element.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.element = function (params) {
        // {root, path, value} 
        // in respect to docElement, it includes replaceProperties for path and value
        // but only if value is a string. TO DO: check it 
        let context;
        let path = self.replaceProperties(params.path, params.args);
        let value = (params.value && typeof params.value == 'string') ? self.replaceProperties(params.value, params.args) : params.value;

        // NOTE: log error if the params.root is equal to the first element of the path? (window.window works)
        if (!params.root) {
            context = self.context(path);
        } else {
            context = params.root;
        }

        return self.docElement(path, value, context, params.delete);

        //self.log(value);
        /* if (value && typeof value == 'string') {
            if (value.startsWith('js:')) value = self.js(value.substr(3));
            else if (value.startsWith('+=')) value = self.element({path: path}) + self.js(value.substr(2));
            else if (value.startsWith('-=')) value = self.element({path: path}) - self.js(value.substr(2));
            else if (value.startsWith('/=')) value = self.element({path: path}) / self.js(value.substr(2));
            else if (value.startsWith('*=')) value = self.element({path: path}) / self.js(value.substr(2))
        } */
        // self.log(value);
        /* if (value.startsWith('number:')) value = Number(self.js(value.substr(6)));
        alert('dopo number:'+value);
        if (value.startsWith('boolean:')) value = Boolean(self.js(value.substr(7)));
        if (value.startsWith('string:')) value = String(self.js(value.substr(6)));
        if (value.startsWith('+:')) value = self.element({path: path}) + self.js(value.substr(2));
        if (value.startsWith('-:')) value = self.element({path: path}) - self.js(value.substr(2));
        if (value.startsWith('/:')) value = self.element({path: path}) / self.js(value.substr(2));
        if (value.startsWith('*:')) value = self.element({path: path}) / self.js(value.substr(2)); */
        /* // verificare...
        if (value.startsWith('push:')) {var obj = self.docElement(path, undefined, root); return obj.push(value.substr(3));}
        if (value.startsWith('unshift:')) {var obj = self.docElement(path, undefined, root); return obj.unshift(value.substr(3));}
        if (value.startsWith('pop:')) {var obj = self.docElement(path, undefined, root); return obj.pop(value.substr(3));}
        if (value.startsWith('shift:')) {var obj = self.docElement(path, undefined, root); return obj.shift(value.substr(3));} */
    };

    
/*
 Method: docElement
Description: Functionality for docElement.
@param {any} elementString - Description for elementString
@param {any} elementValue - Description for elementValue
@param {any} context - Description for context
@param {any} elementDelete - Description for elementDelete
@returns {any} - Result of the operation
 */
this.docElement = function (elementString, elementValue, context, elementDelete) {

        if (!context) context = window; // TO DO: remove
        var namespaces = elementString.split(/[.\s]+/); // path nodes separed by dot or spaces

        /* console.log('<-------');
        console.log('context');
            console.log(context);
            console.log('path');
            console.log(elementString);
            console.log('value');
            console.log(elementValue);
            console.log('------>');
*/
        var lastElement = namespaces.pop();

        /* if (namespaces[0] == 'self') {
            namespaces.shift();
            context = self;
        } else if (namespaces[0] == 'frame') {
            namespaces.shift();
            var frameName = namespaces.shift();
            if (frameName && document.getElementById(frameName) !== null)
                context = document.getElementById(frameName).contentWindow;
        } */
        /* console.log('namespaces');
        console.log(namespaces); */
        for (var i = 0; i < namespaces.length; i++) {
            if (context[namespaces[i]]) { // var
                context = context[namespaces[i]];
            } else {
                if (elementValue !== undefined && elementValue !== null) { // wants to assign a value
                    context[namespaces[i]] = {}; // it create the context context[namespaces[i]] if it doesn't exist 
                    context = context[namespaces[i]];
                    /* self.log('crea context inesistente');
                    self.log('namespaces[i]: '+namespaces[i]); */
                    /* self.log('elementValue');
                    self.log(elementValue);
                    self.log('context[namespaces[i]]');
                    self.log(context[namespaces[i]]); */
                } else {
                    /* self.log(elementString + ' undefined');
                    self.log('lastElement:' + lastElement);
                    self.log(elementValue); */
                }
                //break; // perché? rendeva impossibile creare un listener su un nodo secondario (solo su app e non su app/dev)
            }
        }

        if (elementValue !== undefined) { // write

            //self.log(lastElement+'='+elementValue);
            if (typeof elementValue == 'string') elementValue = self.js(elementValue); // 1.0.3
            context[lastElement] = elementValue;

            // elementValue can be a js function
            /* self.log('context');
            self.log(context);
            self.log('context[lastElement]');
            self.log(context[lastElement]);
            self.log('====='); */
        }

        if (elementDelete) delete context[lastElement];

        /* self.log('context');
        self.log(context); */
        /* console.log('namespaces');
        console.log(namespaces);
        self.log('lastElement');
        self.log(lastElement);
        self.log('context[lastElement]');
        self.log(context[lastElement]); */
        if (context[lastElement] && context[lastElement].isConnected)
            return self.elementToSelector(context[lastElement]);

        else
            return context[lastElement];
    };

    /*    
/*
 Method: openCloseDropdown
Description: Functionality for openCloseDropdown.
@param {any} key - Description for key
@returns {any} - Result of the operation
 */
this.openCloseDropdown = function(key) {
            self.log('openCloseDropdown');
            self.log("key");
            self.log(key);
            //$('.dropdown-menu').css('display','none');
            var selector = self.selector({
                "id": "dropdown" +key,
                "class": 'points',
                "value": key,
            });
            self.log("selector");
            self.log(selector);
            if ($(selector + ' > .dropdown-menu').css('display') == 'none') {
                $(selector + ' > .dropdown-menu').css('display', 'block');
            } else {
                $(selector + ' > .dropdown-menu').css('display', 'none');
            }
        }  */
    
/*
 Method: timeToMs
Description: Functionality for timeToMs.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
this.timeToMs = function (string) {
        // bug
        var timeArray = string.match(/(\d+)(\w+)/); // es: 1s / 100ms
        if (timeArray) {
            var value = Number(timeArray[0]);
            var unit = timeArray[1];
            var unitInMs = {
                ms: 1,
                s: 1000,
                m: 1000 * 60,
                h: 1000 * 60 * 60,
                d: 1000 * 60 * 60 * 24,
                milliseconds: 1,
                seconds: 1000,
                minutes: 1000 * 60,
                hours: 1000 * 60 * 60,
                days: 1000 * 60 * 60 * 24
            };
            if (value)
                if (unitInMs[unit])
                    return value * unitInMs[unit];

                else
                    self.log('wrong unit in the time string [value][unit]: ' + string);

            else
                self.log('wrong value in the time string [value][unit]: ' + string);
        }
        else
            return Number(string);
    };

    this.intervals = {};

    
/*
 Method: timer
Description: Functionality for timer.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.timer = function (params, args) {
        // can go in "browser.json / functions (JS Browser BOM)

        let name = self.replaceProperties(params.name);
        if (!name)
            name = self.getTimestamp() + Math.random(1000);

        if (params.play !== undefined && params.play == 0) {
            if (self.intervals[name]) {
                self.clearInterval({
                    name: name
                }, args);
            }
            if (self.timeouts[name]) {
                self.clearTimeout({
                    name: name
                }, args);
            }
        }
        else if (params.every !== undefined) {
            let every = self.replaceProperties(params.every);
            if (every) {
                self.setInterval({
                    name: name,
                    duration: every,
                    do: params.do
                }, args);
            }
        }
        else if (params.after !== undefined) {
            let after = self.replaceProperties(params.after);
            if (after) {
                self.setTimeout({
                    name: name,
                    duration: after,
                    do: params.do
                }, args);
            } else {
                self.do(params.do, null, args);
            }
        }
    };

    
/*
 Method: setInterval
Description: Functionality for setInterval.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.setInterval = function (params, args) {

        if (params.name && params.duration) {
            if (self.intervals[params.name])
                clearInterval(self.intervals[params.name]);

            self.do(params.do);
            self.intervals[params.name] = setInterval(function () {

                self.do(params.do);
                return params.name;
            }, Number(params.duration));  
        }
        else
            self.log('"setInterval" function requires "name" and "duration" parameters');
    };

    
/*
 Method: clearInterval
Description: Functionality for clearInterval.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.clearInterval = function (params, args) {

        if (self.intervals[params.name]) {
            clearInterval(self.intervals[params.name]);
            delete self.intervals[params.name];
        }

    };

    this.timeouts = {};

    
/*
 Method: setTimeout
Description: Functionality for setTimeout.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.setTimeout = function (params, args) {

        if (params.name && params.duration) {
            if (self.timeouts[params.name])
                clearInterval(self.timeouts[params.name]);

            self.timeouts[params.name] = setTimeout(function () {

                self.do(params.do);
                return params.name;
            }, Number(params.duration));  
        }
        else
            self.log('"setTimeout" function requires "name" and "duration" parameters');
    };

    
/*
 Method: clearTimeout
Description: Functionality for clearTimeout.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.clearTimeout = function (params, args) {

        if (self.timeouts[params.name]) {
            clearTimeout(self.timeouts[params.name]);
            delete self.timeouts[params.name];
        }

    };

    
/*
 Method: on
Description: Functionality for on.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.on = function (params, selectorParams) {

        var container = (selectorParams) ? self.selector(selectorParams) : params.selector;  

        var element;

        if (container) {
            element = self.query(container);  
            if (element)
                self.dataStorage.set(element, "onData", { on: params, selector: container });
            else {
                self.log('can\'t find the element "' + container + '" ("on" function)');
                self.log('params');
                self.log(params);
                self.log('selectorParams');
                self.log(selectorParams);
            }
        }  

        for (var eventId in params) {

            if (eventId == 'init') {

                self.do(params.init, selectorParams);
            } else if (eventId == 'hashchange') {
                self.onHashChange.push(params.hashchange);
                window.addEventListener('hashchange', function () {
                    self.json.setup.page.hash = window.location.hash.substr(1);  

                    self.do(self.onHashChange);
                    self.uiUpdate();
                });
            } else if (eventId == 'scroll') {
                self.onScroll.push(params.scroll);
                window.addEventListener('scroll', function () {

                    self.do(self.onScroll);
                });
            } else if (eventId == 'resize') {
                self.onResize.push(params.resize);
                window.addEventListener('resize', function () {

                    self.do(self.onResize);
                });
            } else if (eventId == 'in') {  
                if (container) {
                    const observer = new IntersectionObserver((entries, observer) => {
                        entries.forEach(function (entry) {

                            if (entry.intersectionRatio == 1) {  
                                var el = entry.target;
                                if (el.getAttribute('in') !== 'true') {
                                    el.setAttribute('in', 'true');  
                                    self.onEvent(entry.target, { type: 'in' });
                                }
                            }
                        });
                    }, {
                        rootMargin: '0px',
                        threshold: [0, 1]  
                    });

                    observer.observe(self.query(container));
                }
            } else if (eventId == 'inViewport') {
                if (container) {
                    const observer = new IntersectionObserver((entries, observer) => {
                        entries.forEach(function (entry) {

                            if (entry.intersectionRatio > 0) {
                                self.onEvent(entry.target, { type: 'inViewport' });
                            }
                        });
                    }, {
                        rootMargin: '0px',
                        threshold: [0, 1]  
                    });

                    observer.observe(self.query(container));
                }
            } else if (eventId == 'outViewport') {
                const observer = new IntersectionObserver((entries, observer) => {
                    entries.forEach(function (entry) {

                        if (entry.intersectionRatio == 0) {
                            self.onEvent(entry.target, { type: 'outViewport' });

                        }
                    });
                }, {
                    rootMargin: '0px',
                    threshold: [0, 1]  
                });
                const element = self.query(container);
                observer.observe(element);

            } else if (eventId !== 'selector') {

                if (element) {

                    element.addEventListener(eventId, function (event) {

                        element.setAttribute('listener', 'true');

                        self.onEvent(this, event);
                    });

                    if (eventId == 'mousedown' || eventId == 'mouseup' || eventId == 'click')
                        element.style.cursor = 'pointer';
                }

            }
        }

    };

    
/*
 Method: money
Description: Functionality for money.
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
this.money = function (value) {

        var rounded = Math.round(Number(value) * 100) / 100;
        var string = String(rounded);
        if (rounded == Math.floor(rounded))
            string += ',00';
        string = String(string).replace('.', ',');
        return string;
    };

    
/*
 Method: offcanvas
Description: Functionality for offcanvas.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.offcanvas = function (params) {
        self.log('offcanvas');
        self.log('params');
        self.log(params);

        var container = params.selector || params.container;
        var selector = self.selector(container);
        var element = self.query(selector);
        self.log('offcanvas');
        self.log(params);
        self.log('selector');
        self.log(selector);
        self.log('element');
        self.log(element);

        if (element) {
            if (params.do == 'hide') {
                self.css({
                    removeClass: 'show'
                }, params);

            } else {
                new bootstrap.Offcanvas(element).show();
            }
        } else {
            self.log('"offcanvas" method can\'t find the selected element');
        }
    };

    
/*
 Method: elementToSelector
Description: Functionality for elementToSelector.
@param {any} element - Description for element
@returns {any} - Result of the operation
 */
this.elementToSelector = function (element) {

        if (element.id) {
            return '#' + element.id;
        }

        if (element.dataset?.value) {
            const sameDataValue = document.querySelectorAll(`[data-value="${element.dataset.value}"]`);
            if (sameDataValue.length === 1) {
                return `[data-value="${element.dataset.value}"]`;
            }
        }

        if (element.classList && element.classList.length > 0) {
            for (const className of element.classList) {
                if (className) {
                    const sameClass = document.querySelectorAll('.' + className);
                    if (sameClass.length === 1) {
                        return '.' + className;
                    }
                }
            }
        }

        const tagName = element.tagName.toLowerCase();
        const sameTag = document.querySelectorAll(tagName);
        if (sameTag.length === 1) {
            return tagName;
        }

        let path = [];
        while (element.parentNode) {
            let selector = element.tagName.toLowerCase();
            const index = Array.from(element.parentNode.children).indexOf(element) + 1;
            selector += `:nth-child(${index})`;

            path.unshift(selector);
            element = element.parentNode;
        }
        return path.join(' > ');
    };

    
/*
 Method: onEvent
Description: Functionality for onEvent.
@param {any} element - Description for element
@param {any} event - Description for event
@returns {any} - Result of the operation
 */
this.onEvent = function (element, event) {

        var item = self.dataStorage.get(element, 'onData');
        var code = self.dataStorage.get(element, 'code') || '';

        var value = element.getAttribute('data-value');

        var selector = self.elementToSelector(element);  

        var eventType = (event.type && item.on) ? item.on[event.type] : undefined;

        var action, specialKey;

        if (item.on) {

            if (event.button == '2' && event.type == 'mousedown') {  

                if (item.on['contextmenu'])
                    action = item.on['contextmenu'];

            } else if (event.type && item.on[event.type]) {

                var eventAction = item.on[event.type];

                if (eventAction) {
                    if (eventAction.shiftKey && event.shiftKey) {
                        specialKey = 'shiftKey';
                        action = eventAction.shiftKey;
                    } else if (eventAction.altKey && event.altKey) {
                        specialKey = 'altKey';
                        action = eventAction.altKey;
                    } else if (eventAction.cmdKey && event.cmdKey) {
                        specialKey = 'cmdKey';
                        action = eventAction.cmdKey;
                    } else if (!eventAction.shiftKey && !eventAction.altKey && !eventAction.cmdKey) {
                        action = item.on[event.type];
                    }
                }
            }
        }

        if (action) {

            if (event.target) {

                var actionString;
                var targetId = event.target.id;
                var targetClass = event.target.className;
                var targetValue = event.target.dataset.value;
                var targetSelector = event.target.dataset.selector;  

                targetSelector = self.replaceAll(targetSelector, '"', '\'');  
                action = self.stringify(action);
                if (typeof targetId !== 'string') targetId = '';
                if (typeof targetClass !== 'string') targetClass = '';

                actionString = self.replaceAll(action, '{on:target.id}', targetId);
                actionString = self.replaceAll(actionString, '{on:altKey}', event.altKey);
                actionString = self.replaceAll(actionString, '{on:target.className}', targetClass);
                actionString = self.replaceAll(actionString, '{on:target.data-value}', targetValue);  
                actionString = self.replaceAll(actionString, '{on:target.dataset.value}', targetValue);
                actionString = self.replaceAll(actionString, '{on:target.dataset.selector}', targetSelector);  
                actionString = self.replaceAll(actionString, '{on:which}', event.which);
                actionString = self.replaceAll(actionString, '{on:clientY}', event.clientY);
                actionString = self.replaceAll(actionString, '{on:clientX}', event.clientX);

                actionString = self.replaceAll(actionString, '{on:value}', targetValue);
                actionString = self.replaceAll(actionString, '{on:dragging}', (event.which > 0));

                if (this.isJson(actionString))
                    action = self.parse(actionString);

                else
                    action = actionString;
            }

            var args = item.args;  

            let specialKeyLog = (specialKey) ? ' + ' + specialKey : '';

            self.do(action, { selector: selector }, args);
        }  

    };

    this.pluginsFunctionAvailable = [];  

    self.pluginsLoader = function (packs, callback, params) {

        var filesToLoad = [];

        for (pack of packs) {

            if (self.pluginsLoaded[pack.name] == undefined) {

                self.log('require plugin ' + pack.name, 'grey');
                var plugin = self.findPlugin(pack.name);
                let version;
                var pluginsFiles;
                if (plugin) {
                    pluginsFiles = plugin.files || plugin;
                    version = plugin.version;
                } else {
                    pluginsFiles = self.json.resources.pluginsFiles[pack.name];  
                    version = pack.version;
                }

                self.pluginsLoaded[pack.name] = version;  

                if (!Array.isArray(pluginsFiles))  
                    pluginsFiles = [pluginsFiles];

                for (item of pluginsFiles) {
                    var url = self.replaceAll(item.url, '{version}', version);
                    if (!self.findUrl(filesToLoad, url))  
                        filesToLoad.push({ name: item.name, url: url, type: item.type });
                }
            }

        }

        if (filesToLoad.length > 0) {

            let promises = [];

            filesToLoad.forEach(function (item) {
                promises.push(loadPlugin({ name: item.name, url: item.url, type: item.type, rel: item.rel, as: item.as, content: item.content }));

            });

            Promise.all(promises).then(function (files) {

                callback(...params);  

            }).catch(function (script) {
                self.log('failed to load error');
                self.log(item);
                self.log(script);
            });
        }
    };

    
/*
 Method: pluginsRequired
Description: Functionality for pluginsRequired.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.pluginsRequired = function (params) {

        if (self.json.resources) {
            const pluginsFunctions = self.json.resources.pluginsFunctions;
            var pluginsRequired = [];
            if (typeof params == 'string') {

                if (pluginsFunctions[params]) {
                    if (!self.pluginsLoaded[pluginsFunctions[params].name])
                        pluginsRequired.push(pluginsFunctions[params]);
                }
            } else if (typeof params == 'object') {
                var objArray;
                if (Array.isArray(params)) objArray = params; else objArray = [params];

                for (var obj of objArray) {

                    for (var key in obj) {
                        var mainFunction = (key.indexOf('.') >= 0) ? key.substr(0, key.indexOf('.')) : key;

                        if (pluginsFunctions[mainFunction]) {
                            if (!self.pluginsLoaded[pluginsFunctions[mainFunction].name])
                                pluginsRequired.push(pluginsFunctions[mainFunction]);
                        }  

                    }
                }
            }
            return pluginsRequired;
        } else {
            return [];
        }
    };

    
/*
 Method: functionName
Description: Functionality for functionName.
@param {any} obj - Description for obj
@returns {any} - Result of the operation
 */
this.functionName = function (obj) {
        if (obj == 'object') {
            const name = Object.keys(obj)[0];
            const pluginFunctions = ['database', 'ace', 'moment', 'dayjs'];
            if (pluginFunctions.indexOf(name) >= 0)
                return name;
        }
    };

    
/*
 Method: for
Description: Functionality for for.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.for = function (params, selectorParams, args) {

        let container = self.selector(params.selector || params.container) || self.selector(selectorParams);

        let itemsName = params.id || params.name;

        if (params.of) {

            var itemsArray = self.replaceProperties(params.of, args);

            if (itemsArray) {

                if (params.html) {  
                    if (!params.do) params.do = [];
                    else if (!Array.isArray(params.do)) params.do = [params.do];
                    params.do.push({ html: params.html });
                }

                if (typeof itemsArray == 'object') {

                    let indexNum = 0;

                    for (var index in itemsArray) {

                        var item = itemsArray[index];

                        if (typeof item == 'object') {
                            item.key = index;  
                            item.index = index;  
                        }

                        if (typeof item.output == 'object') {  

                            for (let key in item.output) {

                                var output = item.output[key];
                                if (typeof output == 'string') {
                                    output = self.js(output, args);
                                } else if (typeof output == 'object') {

                                    output = self.replaceProperties(output, args);
                                }

                                item = self.replaceItems(item, output, key);  

                            }
                        }

                        if (params.do.length > 0) {

                            let objWithIndex = self.cloneObject(params.do);

                            if (params.var) {  
                                self.element({ path: params.var, value: item, root: self.json.var });
                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, 'index:' + params.var, index);

                            }

                            if (itemsName) {

                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, 'index:' + itemsName, index);
                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, itemsName, item);

                            }

                            if (params.delay) {
                                params.delay = self.replaceProperties(params.delay);

                                let forId = params.var || itemsName;
                                self.timer({
                                    name: 't' + forId + index,
                                    after: indexNum * params.delay,
                                    do: objWithIndex
                                });
                            } else {
                                self.do(objWithIndex, { selector: container });
                            }
                        }

                        indexNum += 1;

                    }
                }
            }

        } else if (params.from !== undefined && params.to !== undefined) {

            var step = self.js(self.replaceProperties(params.step)) || 1;
            var from = self.js(self.replaceProperties(params.from)) || 0;
            var to = self.js(self.replaceProperties(params.to));

            if (to !== undefined) {

                for (var index = Number(from); index <= Number(to); index += Number(step)) {

                    if (params.var)
                        self.element({ path: params.var, value: index, root: self.json.var });

                    var item = self.replaceItems(params, String(index), itemsName);  

                    if (item.output && typeof item.output == 'object') {  

                        for (let key in item.output) {

                            var output = item.output[key];
                            if (typeof output == 'string') {
                                output = self.js(output, args);
                            } else if (typeof output == 'object') {

                                output = self.replaceProperties(output, args);
                            }

                            item = self.replaceItems(item, output, key);

                        }
                    }

                    if (item.do) {
                        params.do = self.replacePropertyWithPrefix(item.do, 'index:' + itemsName, index);

                        self.do(item.do, selectorParams, args);  

                    }
                    if (item.html) {

                        params.html = self.replacePropertyWithPrefix(item.html, 'index:' + itemsName, index);
                        self.html(item.html, selectorParams);
                    }
                }
            }
        } else {
            self.log('"for" element requires "value" or "from" and "to" parameters');  
            self.log(params);
        }

    };

    
/*
 Method: htmlTag
Description: Functionality for htmlTag.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} tagParam - Description for tagParam
@param {any} mainParam - Description for mainParam
@returns {any} - Result of the operation
 */
this.htmlTag = function (params, selectorParams, tagParam, mainParam) {

        if (Array.isArray(params)) {
            for (var index in params)
                self.htmlTag(params[index], selectorParams, tagParam, mainParam);
        } else {

            let htmlTagSplitter = tagParam.indexOf(' ');
            if (htmlTagSplitter > 0) {
                let tagClass = tagParam.substr(htmlTagSplitter + 1);
                tagParam = tagParam.slice(0, htmlTagSplitter);
                if (tagClass) {
                    if (typeof params !== 'object') params = { 'text': String(params) };
                    if (!params.attr) params.attr = {};

                    let tagArr = tagClass.split(' ');
                    var attrClass = '';
                    for (let tagProp of tagArr) {
                        let tagAttr = new RegExp("([_a-zA-Z]+[_a-zA-Z0-9-]*)=(.+)");  
                        if (tagAttr.test(tagProp)) {

                            let tagPropArr = tagProp.split('=');
                            params.attr[tagPropArr[0]] = tagPropArr[1];
                        } else {
                            attrClass += ' ' + tagProp;
                        }
                    }

                    if (params.attr.class)
                        params.attr.class = params.attr.class + attrClass;
                    else {
                        params.attr.class = attrClass;
                    }

                }
            }

            let htmlTagObj = self.createTagParams(params, tagParam, mainParam);

            self.do(htmlTagObj, selectorParams);

            if (params.database) self.addFirebaseTag(self.extend({}, params, selectorParams));

        }
    };

    
/*
 Method: createTagParams
Description: Functionality for createTagParams.
@param {any} params - Description for params
@param {any} tagParam - Description for tagParam
@param {any} mainParam - Description for mainParam
@returns {any} - Result of the operation
 */
this.createTagParams = function (params, tagParam, mainParam) {
        if (!mainParam) {
            switch (tagParam) {
                case 'header':
                case 'main':
                case 'footer':
                case 'div':
                case 'p':
                    mainParam = 'html';
                    break;
                case 'img':
                    mainParam = 'src';
                    break;
                default:
                    mainParam = 'text';  
                    break;
            }
        }
        var paramsObj = {};
        if (typeof params == 'string') paramsObj[mainParam] = params; else paramsObj = params;
        paramsObj.tag = tagParam;
        return paramsObj;
    };

    this.onResize = [];
    this.onHashChange = [];
    this.onScroll = [];

    
/*
 Method: notEmptyObject
Description: Functionality for notEmptyObject.
@param {any} obj - Description for obj
@returns {any} - Result of the operation
 */
this.notEmptyObject = function (obj) {
        return (obj  
            && Object.keys(obj).length > 0);

    };

    
/*
 Method: run
Description: Functionality for run.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.run = function (params, selectorParams, args) {

        if (Array.isArray(params)) {
            for (var index in params)
                self.run(params[index], selectorParams, args);
        } else {

            var codeToRun;
            if (typeof params == 'object')
                codeToRun = self.cloneObject(params);
            else if (typeof params == 'string')
                codeToRun = self.replaceProperties(params);
            else if (typeof params == 'function')
                return params(args);

            else
                return params;

            if (typeof codeToRun == 'object') {
                var selector = codeToRun.selector || codeToRun.container;
                if (!selector) selector = selectorParams;
                if (typeof selector == 'string') selector = { selector: selector };

                var actions = {};
                for (key in codeToRun) {

                    if (nodes.extend.indexOf(key) >= 0) {
                        var obj = {};
                        obj[key] = codeToRun[key];
                        self.extendJson(self.json, obj);
                    } else if (['setup', 'container', 'selector', 'resources', 'on', 'do'].indexOf(key) < 0)
                        actions[key] = codeToRun[key];
                }

                if (codeToRun.on) { self.on(codeToRun.on, selector); }
                if (codeToRun.do) { self.do(codeToRun.do, selector, args); }

                if (self.notEmptyObject(actions)) {

                    return self.do(actions, selector, args);

                }
            }
        }

    };

    
/*
 Method: inputValue
Description: Functionality for inputValue.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.inputValue = function (params) {
        var selector = self.selector(params.selector || params.container);
        var element = document.querySelector(selector);  
        return element.value;
    };

    this.part = 
/*
 Method: block
Description: Functionality for block.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.block = function (params, selectorParams, args) {

        let nested;
        let part;
        let partArguments;

        if (typeof params.do == 'string' && self.json.parts) {  

            self.log('PART: ' + params.do, 'brown');

            params.do = self.replaceProperties(params.do);

            if (typeof params.do == 'object')
                part = params.do;

            else
                part = self.element({ path: self.replaceAll(params.do, ':', '.'), root: self.json.parts });  

        } else if (typeof params.do == 'object')
            part = params.do;

        if (part) {

            if (params.arguments)
                partArguments = self.replaceProperties(params.arguments);

            if (typeof params.arguments == 'object') {
                partArguments = (params.arguments.setup) ? params.arguments.setup : params.arguments;  
                partArguments.selector = partArguments.selector || params.selector;
            }

            nested = self.replacePropertyWithPrefix(part, 'setup', partArguments);  
            nested = self.replacePropertyWithPrefix(nested, 'arguments', partArguments);  

            self.extendJsonFromElement(nested);
            self.do(nested, params.selector, partArguments);
        }
    };

    
/*
 Method: blocks
Description: Functionality for blocks.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.blocks = function (params, selectorParams, args) {
        if (Array.isArray(params)) {
            for (var index in params)
                self.blocks(params[index], selectorParams, args);
        } else {

            if (typeof params == 'string') {

                var blockName = self.replaceProperties(params, args);  

                var libraryObj = self.element({ path: 'self.json.blocks.' + blockName });  

                if (libraryObj)

                    self.run(libraryObj, selectorParams, args);

                else
                    self.log('library component not found: ' + params);
            } else {
                self.run(params, selectorParams, args);

            }
        }
    };

    
/*
 Method: uiUpdate
Description: Functionality for uiUpdate.
@param {any} option - Description for option
@returns {any} - Result of the operation
 */
this.uiUpdate = function (option) {
        window.dispatchEvent(new Event('resize'));  

    };

    
/*
 Method: resizeEvent
Description: Functionality for resizeEvent.
@param {any} event - Description for event
@returns {any} - Result of the operation
 */
this.resizeEvent = function (event) {

        for (var selector in self.resizeActions) {
            var element = self.query(selector);
            for (var property in self.resizeActions[selector]) {
                if (property == 'action') {
                    self.do(self.resizeActions[element].action, { selector: selector });

                } else {
                    var value = self.resizeActions[selector][property];
                    var result = self.replaceProperties(value) || '';

                    element.style[property] = result;
                }
            }
        }
    };

    
/*
 Method: pageFullScreen
Description: Functionality for pageFullScreen.
@param {any} onOff - Description for onOff
@returns {any} - Result of the operation
 */
this.pageFullScreen = function (onOff) {
        self.log('pageFullScreen');
        if (onOff)
            self.addClass(document.querySelector('body'), 'fullscreen');

        else
            self.removeClass(document.querySelector('body'), 'fullscreen');
    };

    
/*
 Method: link
Description: Functionality for link.
@param {any} link - Description for link
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.link = function (link, args) {

        link = self.replaceProperties(link, args);
        window.location.href = link;
    };

    
/*
 Method: reload
Description: Functionality for reload.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.reload = function () {
        location.reload();
    };

    
/*
 Method: gotoHomePage
Description: Functionality for gotoHomePage.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.gotoHomePage = function () {
        self.gotoPage(self.homePage());
    };

    
/*
 Method: homePage
Description: Functionality for homePage.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.homePage = function () {

        if (self.json.pages) {
            if (Object.keys(self.json.pages).length > 0) {

                if (self.json.pages.home)
                    return 'home';

                else
                    return Object.keys(self.json.pages)[0];
            } else {
                self.log('No pages in "pages" node');
            }
        } else {
            self.log('Can\'t find the "pages" node');
        }
    };

    
/*
 Method: updateCodeEditors
Description: Functionality for updateCodeEditors.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.updateCodeEditors = function () {

        var elements = document.querySelectorAll('.ace_editor');
        if (elements) {
            elements.forEach(function (element, index) {
                var editor = ace.edit(element);
                editor.renderer.updateFull(true);

            });
        }
    };

    
/*
 Method: gotoPage
Description: Functionality for gotoPage.
@param {any} pageId - Description for pageId
@param {any} fromHash - Description for fromHash
@returns {any} - Result of the operation
 */
this.gotoPage = function (pageId, fromHash) {
        self.log('page');  
        self.log('pageId');
        self.log(pageId);
        self.log('fromHash');
        self.log(fromHash);

        if (pageId) {

            if (self.json.pages) {
                if (self.json.pages[pageId]) {
                    var page = self.json.pages[pageId];

                    if (pageId !== self.json.setup.page.id || fromHash !== self.json.setup.page.hash) {

                        if (fromHash) {

                            self.json.setup.page.hash = fromHash;
                        } else {

                            delete self.json.setup.page.hash;
                        }

                        var fullScreen = self.json.pages[pageId].fullscreen || false;

                        var pageRoles = (page.roles) ? String(page.roles).split(',') : undefined;

                        if (self.params.d || !page.roles || pageRoles.indexOf(self.userRole()) >= 0) {

                            if (pageId !== self.json.setup.page.id) {

                                self.json.setup.page.id = pageId;
                                self.json.setup.page.title = page.title;
                                self.json.setup.page.roles = page.roles;

                                self.hide({
                                    "class": "page"
                                });
                                self.in({
                                    "class": "page",
                                    "data-value": pageId,
                                    "transition": "fadeIn"
                                });

                                self.uiUpdate();  

                            }

                            if (page.update) {  
                                self.do(page.update);  

                            }

                            if (page.init) self.do(page.init);

                            self.log(page);
                            if (page.on)
                                self.on(page.on, { selector: '.page[data-value=' + pageId + ']' });

                        } else {
                            if (self.userRole() == 'guest') {

                            } else {
                                self.alert('Non hai i privilegi per accedere a questa pagina');
                            }

                        }
                    } else {

                    }
                } else {
                    self.log('Can\'t find the page "' + pageId + '"');
                }
            } else {
                self.log('Can\'t find the main node "pages"');
            }
        } else {
            return self.json.setup.page;
        }
    };

    var alertInterval;

    
/*
 Method: getTime
Description: Functionality for getTime.
@param {any} ms - Description for ms
@returns {any} - Result of the operation
 */
this.getTime = function (ms) {
        var sec = parseInt(ms / 1000);
        var min = parseInt(sec / 60);
        sec = sec - min * 60;
        if (min < 10) min = '0' + min;
        if (sec < 10) sec = '0' + sec;
        return min + ':' + sec;
    };

    
/*
 Method: sortablejs
Description: Functionality for sortablejs.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.sortablejs = function (params, selectorParams, args) {
        let container = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
        let query = self.query(container);
        self.log('container');
        self.log(container);
        self.log('query');
        self.log(query);
        var sortable = Sortable.create(query);
    };

    
/*
 Method: alert
Description: Functionality for alert.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.alert = function (params, args) {

        var paramsReplaced = self.cloneObject(params);

        if (paramsReplaced) {
            if (paramsReplaced.do && paramsReplaced.do !== 'fire') {
                return Swal[paramsReplaced.do](params);

            } else {
                if (typeof paramsReplaced == 'string') paramsReplaced = self.text({ string: paramsReplaced, args: args });  

                if (typeof paramsReplaced == 'number') paramsReplaced = String(paramsReplaced);  
                if (paramsReplaced.title !== undefined) paramsReplaced.title = self.text({ string: paramsReplaced.title, args: args });

                if (paramsReplaced.html && typeof paramsReplaced.html == 'string') paramsReplaced.html = self.replaceProperties(paramsReplaced.html, args);
                if (paramsReplaced.text !== undefined) paramsReplaced.text = self.text({ string: paramsReplaced.text, args: args });

                if (paramsReplaced.inputValue) paramsReplaced.inputValue = self.text({ string: paramsReplaced.inputValue, args: args });

                if (paramsReplaced.cancelButtonText) paramsReplaced.cancelButtonText = self.text({ string: paramsReplaced.cancelButtonText, args: args });
                if (paramsReplaced.confirmButtonText) paramsReplaced.confirmButtonText = self.text({ string: paramsReplaced.confirmButtonText, args: args });
                if (paramsReplaced.denyButtonText) paramsReplaced.denyButtonText = self.text({ string: paramsReplaced.denyButtonText, args: args });

                if (paramsReplaced.inputPlaceholder) paramsReplaced.inputPlaceholder = self.text({ string: paramsReplaced.inputPlaceholder, args: args });
                if (paramsReplaced.validationMessage) paramsReplaced.validationMessage = self.text({ string: paramsReplaced.validationMessage, args: args });

                if (paramsReplaced.footer) {
                    paramsReplaced.footer = self.text({ string: paramsReplaced.footer, args: args });
                }

                if (paramsReplaced.upload) {
                    paramsReplaced.html = '<form class="box" method="post" action="" enctype="multipart/form-data"><div class="drop-zone"><div class="drop-zone-caption">' + self.text('dragAndDropFiles') + '</div><span class="btn btn-primary btn-file" style="position: relative"><span>' + self.text('chooseFiles') + '</span><input type="file" accept=".ply, .obj, .stl" class="drop-zone-file" name="files[]"></span></div></form> ';

                }

                alertObj.confirm = paramsReplaced.confirm;  
                alertObj.cancel = paramsReplaced.cancel;  
                alertObj.deny = paramsReplaced.deny;  
                alertObj.on = paramsReplaced.on;
                alertObj.id = paramsReplaced.id;
                delete paramsReplaced.on;

                var alertHtml = paramsReplaced.html || paramsReplaced.blocks;
                if (alertHtml)
                    paramsReplaced.html = " ";

                var swalParams = self.replaceProperties(paramsReplaced, args);

                if (paramsReplaced.animate) {
                    var animationIn = paramsReplaced.animate.in || paramsReplaced.animate;
                    var animationOut = paramsReplaced.animate.out || paramsReplaced.animate;
                    if (paramsReplaced.animate.in)
                        animationIn = paramsReplaced.animate.in.transition || paramsReplaced.animate.in;
                    if (paramsReplaced.animate.out)
                        animationOut = paramsReplaced.animate.out.transition || paramsReplaced.animate.out;
                    swalParams.showClass = {
                        popup: 'animate__animated animate__faster animate__animated animate__' + animationIn
                    };
                    swalParams.hideClass = {
                        popup: 'animate__animated animate__faster animate__animated animate__' + animationOut
                    };
                }

                var customIcon;
                if (swalParams.icon) {

                    if (typeof swalParams.icon !== 'string') {
                        customIcon = self.cloneObject(swalParams.icon);
                        delete swalParams.icon;
                    }

                }

                delete swalParams.blocks;
                delete swalParams.onUpload;
                delete swalParams.upload;

                if (swalParams.confirm) delete swalParams.confirm;
                if (swalParams.cancel) delete swalParams.cancel;
                if (swalParams.timerProgressBar) {
                    swalParams.onBeforeOpen = function () {

                        alertInterval = setInterval(() => {
                            const content = Swal.getContent();
                            if (content) {
                                const b = content.querySelector('b');
                                if (b) {
                                    b.textContent = self.getTime(Swal.getTimerLeft());
                                }
                            }
                        }, 100);
                    };
                    swalParams.onClose = function () {
                        clearInterval(alertInterval);
                    };
                }

                Swal.fire(swalParams).then((result) => {

                    var alertResult = result.value;

                    if (alertObj.on) {
                        if (alertObj.on.value && result.value) {

                            if (alertObj.id) alertValues[alertObj.id] = result.value;
                            alertValues.value = result.value;
                            setTimeout(function () {
                                alertObj.on.value = self.replacePropertyWithPrefix(alertObj.on.value, 'alert', alertValues);

                                self.do(alertObj.on.value, undefined, result.value);  

                            }, 500);
                        }
                        if (alertObj.on.isConfirmed && result.isConfirmed) {
                            if (alertObj.id) alertValues[alertObj.id] = result.isConfirmed;
                            alertObj.on.isConfirmed = self.replacePropertyWithPrefix(alertObj.on.isConfirmed, 'alert', alertValues);
                            alertObj.on.isConfirmed = self.replacePropertyWithPrefix(alertObj.on.isConfirmed, 'alert', result);
                            self.do(alertObj.on.isConfirmed, undefined, result.value);  

                        }
                        if (alertObj.on.isDenied && result.isDenied) {
                            alertObj.on.isDenied = self.replacePropertyWithPrefix(alertObj.on.isDenied, 'alert', result);
                            self.do(alertObj.on.isDenied, undefined, result.value);  

                        }
                        if (alertObj.on.isDismissed && result.isDismissed) {
                            alertObj.on.isDismissed = self.replacePropertyWithPrefix(alertObj.on.isDismissed, 'alert', result);
                            self.do(alertObj.on.isDismissed, undefined, result.dismiss);  

                        }

                        console.log('alertValues');
                        console.log(alertValues);

                    } else {

                    }

                });

                if (alertHtml)
                    self.do(alertHtml, { selector: '#swal2-html-container' });

                if (customIcon) {

                    self.do(customIcon, { selector: '.swal2-icon' });

                    self.css({
                        selector: '.swal2-icon',
                        style: {
                            'border': '0px solid transparent'
                        }
                    });
                    self.show({
                        selector: '.swal2-icon'
                    });

                }

                if (swalParams.on && swalParams.on.init)  
                    self.do(swalParams.on.init);

            }
        }
    };

    
/*
 Method: findKey
Description: Functionality for findKey.
@param {any} obj - Description for obj
@param {any} key - Description for key
@returns {any} - Result of the operation
 */
this.findKey = function (obj, key) {
        return obj.filter(function (item) {
            return Boolean(item[key]);
        });
    };

    
/*
 Method: formatBytes
Description: Functionality for formatBytes.
@param {any} bytes - Description for bytes
@param {any} decimals - Description for decimals
@returns {any} - Result of the operation
 */
var formatBytes = function (bytes, decimals) {
        if (bytes == 0) return '0 Byte';
        var k = 1000;
        var dm = decimals + 1 || 3;
        var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
        var i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    };

    
/*
 Method: userRole
Description: Functionality for userRole.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.userRole = function () {
        switch (Boolean(self.user('uid'))) {
            case true:
                return 'user';  
                break;
            case false:
                return 'guest';
                break;
        }
    };

    
/*
 Method: userIn
Description: Functionality for userIn.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.userIn = function () {

        return Boolean(self.user('uid'));
    };

    
/*
 Method: userOut
Description: Functionality for userOut.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.userOut = function () {
        return (!self.user('uid'));
    };

    
/*
 Method: user
Description: Functionality for user.
@param {any} param - Description for param
@returns {any} - Result of the operation
 */
this.user = function (param) {

        if (auth && auth.currentUser)
            if (param == 'firstName')
                return self.firstName(auth.currentUser['displayName']);
            else if (auth.currentUser[param])
                return auth.currentUser[param];
            else if (!param)
                return auth.currentUser;

            else
                return undefined;

        else
            return undefined;
    };

    
/*
 Method: firstName
Description: Functionality for firstName.
@param {any} nameParam - Description for nameParam
@returns {any} - Result of the operation
 */
this.firstName = function (nameParam) {
        var firstNameStr = (nameParam) ? nameParam.split(' ')[0] : '';
        return firstNameStr;
    };

    
/*
 Method: userUid
Description: Functionality for userUid.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.userUid = function () {
        if (auth) return auth.currentUser.uid;
        else return undefined;
    };

    
/*
 Method: userVerified
Description: Functionality for userVerified.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.userVerified = function () {
        if (auth) return auth.currentUser.emailVerified;
        else return undefined;
    };

    
/*
 Method: thunkableMessage
Description: Functionality for thunkableMessage.
@param {any} message - Description for message
@param {any} callback - Description for callback
@returns {any} - Result of the operation
 */
this.thunkableMessage = function (message, callback) {
        if (callback) callback(message);
    };

    this.onMessage = {
        alert: "test"
    };

    
/*
 Method: processMessage
Description: Functionality for processMessage.
@param {any} message - Description for message
@returns {any} - Result of the operation
 */
this.processMessage = function (message) {
        self.alert('processMessage');
        self.do(self.onMessage, message);

    };

    
/*
 Method: thunkable
Description: Functionality for thunkable.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.thunkable = function (params, args) {
        if (params.postMessage) {
            var message = self.replaceProperties(params.postMessage, args);
            if (message) {
                try {
                    ThunkableWebviewerExtension.postMessage(message);
                } catch (error) {
                }
            }
            else
                self.log('in function "thunkable" "postMessage" parameter is wrong');
        }
        if (params.receiveMessage) {
            self.onMessage = params.receiveMessage;
            try {
                ThunkableWebviewerExtension.receiveMessage(self.processMessage);
            } catch (error) {
            }
        }

    };

    
/*
 Method: extendFunctions
Description: Functionality for extendFunctions.
@param {any} functions - Description for functions
@returns {any} - Result of the operation
 */
this.extendFunctions = function (functions) {

        for (var name in functions) {
            if (functions[name].js) {
                try {

                    self.functions[name] = new Function(functions[name].js);
                } catch (error) {
                    console.log('ERROR creating function %c' + name, 'color:orange;');
                    self.log(functions[name].js);
                    self.log(error.message);

                }
            } else {
                self.log(name + ' function requires js property');
            }
        }
    };

    
/*
 Method: function
Description: Functionality for function.
@param {any} obj - Description for obj
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.function = function (obj, args) {

        if (obj.name && obj.js) {

            self.functions[obj.name] = new Function(obj.js);
        } else if (obj.name && !obj.js) {
            if (self.functions[obj.name]) {

                if (!obj.arguments || !Array.isArray(obj.arguments)) obj.arguments = [obj.arguments];
                try {
                    return self.functions[obj.name](...obj.arguments);

                } catch (error) {
                    console.log('ERROR in function %c' + obj.name, 'color:orange;');
                    console.log('obj');
                    console.log(obj);
                    console.log('function');
                    console.log(self.json.functions[obj.name]);
                    console.log(error.message);
                }
            } else {
                console.log('ERROR function undefined %c' + obj.name, 'color:orange;');
            }

        } else if (obj.js) {

            if (obj.arguments && Array.isArray(obj.arguments)) {
                return new Function(obj.js).call(null, obj.arguments);

            } else {
                return new Function(obj.js).call(null, obj.arguments);
            }
        }

    };

    this.eval = 
/*
 Method: js
Description: Functionality for js.
@param {any} code - Description for code
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.js = function (code, args) {

        code = self.replaceProperties(code, args);

        try {

            return eval(code);
        } catch (error) {

            return code;
        }

    };

    
/*
 Method: if
Description: Functionality for if.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.if = function (params, selectorParams, args) {

        if (typeof params == 'object') {

            var conditionString = 'if';
            var condition;

            if (params.is !== undefined) {
                if (typeof params.is == 'string') {
                    let isReplaced = self.replaceProperties(params.is, args, true);
                    self.log(isReplaced, 'grey');
                    try {
                        condition = Boolean(eval(isReplaced));  
                        self.log(isReplaced + ' is ' + condition, 'grey');
                    } catch (error) {
                        self.log(isReplaced, 'red');
                        self.log('"if" condition wrong', 'red');
                        self.log(error, 'red');
                    }
                    conditionString += ' is ' + params.is;
                } else if (Array.isArray(params.is)) {
                    let i = 0;
                    params.is[i];
                    switch (params.is[i + 1]) {
                        case '=': condition = Boolean(eval(params.is[i] + '===' + params.is[i + 2])); break;
                    }
                }
            } else if (params.not !== undefined) {
                condition = (!Boolean(eval(self.replaceProperties(params.not, args, true))));  
                if (typeof params.not == 'string') conditionString += ' not ' + params.not;
            } else if (params.regexp) {
                const regex = new RegExp(params.regexp);
                condition = Boolean(regex.test(params.string));
            } else if (params.isArray) {
                condition = self.isArray(params.isArray);
                conditionString += 'isArray ' + params.isArray;
            } else if (params.exist) {
                let elementExist = self.element({ path: params.exist });
                condition = (elementExist);
                self.log('"if" exist ' + params.exist, 'grey');
            } else if (params.value && params.in) {

                condition = self.valueInJson(self.replaceProperties(params.in, args, true), self.replaceProperties(params.value));

                self.log('"if" value ' + self.replaceProperties(params.value) + ' in array is ' + condition, 'grey');
            } else if (params.key && params.in) {
                condition = self.keyInJson(self.replaceProperties(params.in, args, true), self.replaceProperties(params.key));
                self.log('"if" key ' + self.replaceProperties(params.key) + ' in object is ' + condition, 'grey');
            } else {
                let selector = self.selector(params.selector || params.container, undefined) || self.selector(selectorParams, undefined);

                if (selector) {
                    if (params.exist) {

                        condition = Boolean(self.exist(selector));
                        conditionString += ' ' + selector + ' exist ' + params.exist;
                    }
                    if (params.hasClass) {  
                        var element = self.query(selector);
                        condition = Boolean(self.hasClass(element, params.hasClass));
                        conditionString += ' ' + selector + ' hasClass ' + params.hasClass;
                    }
                    if (params.isVisible) {
                        var element = self.query(selector);
                        condition = Boolean(self.isVisible(element, params.isVisible));
                        conditionString += ' ' + selector + ' isVisible';
                    }
                    if (params.isHidden) {
                        var element = self.query(selector);
                        condition = Boolean(self.isHidden(element, params.isHidden));
                        conditionString += ' ' + selector + ' isHidden';
                    }
                    if (params.inViewport) {
                        var element = self.query(selector);
                        condition = Boolean(self.inViewport(element, params.inViewport));
                        conditionString += ' ' + selector + ' inViewport';
                    }
                    if (params.outViewport) {
                        var element = self.query(selector);
                        condition = Boolean(self.outViewport(element, params.outViewport));
                        conditionString += ' ' + selector + ' outViewport';
                    }
                    if (params.isDisplay) {
                        var element = self.query(selector);

                        condition = Boolean(self.isDisplay(element, params.isDisplay));
                        conditionString += ' ' + selector + ' isDisplay ' + params.isDisplay;
                    }
                } else {
                    self.log('the "if" function requires "is" or "not" parameter');
                }
            }

            if (condition)
                if (params.then) {

                    if (typeof params.then == 'object') {
                        return self.do(params.then);

                    } if (typeof params.then == 'string') {
                        return self.replaceProperties(params.then);
                    } else {
                        return params.then;
                    }

                }
                else
                    self.log('the "if" function requires "then" parameter');
            else if (params.else) {
                if (typeof params.else == 'object') {
                    return self.do(params.else);

                } if (typeof params.else == 'string') {
                    return self.replaceProperties(params.else);
                } else {
                    return params.else;
                }
            }
        } else {

            return Boolean(self.js(params, args));  

        }
    };

    
/*
 Method: switch
Description: Functionality for switch.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.switch = function (params, args) {

        var value;
        var answer;
        if (params.expression)
            value = String(self.js(params.expression, args));

        else
            self.log('"switch" function requires "expression" parameter');

        if (params.cases) {

            if (value !== undefined && params.cases[value]) {
                if (typeof params.cases[value] == 'object')
                    answer = self.do(params.cases[value], undefined, args);

                else
                    answer = params.cases[value];

            } else if (params.cases['default']) {
                self.log('default');
                self.log(params.cases['default']);
                if (typeof params.cases['default'] == 'object')
                    answer = self.do(params.cases['default'], undefined, args);

                else
                    answer = params.cases['default'];
            }
        } else {
            self.log('"switch" function requires "cases" parameter');
            self.log('answer');
            self.log(answer);
        }
        return answer;
    };

    
/*
 Method: delay
Description: Functionality for delay.
@param {any} params - Description for params
@param {any} selector - Description for selector
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.delay = function (params, selector, args) {
        self.log('delay');
        self.log('selector');
        self.log(selector);
        var duration = params.duration || 1000;
        duration = Number(self.replaceProperties(duration, args));
        self.log(duration);
        if (params.do) {
            setTimeout(function () {
                self.do(params.do, selector, args);

            }, duration);
        }
    };

    
/*
 Method: firebasePasswordReset
Description: Functionality for firebasePasswordReset.
@param {any} paramEmail - Description for paramEmail
@param {any} paramSuccess - Description for paramSuccess
@param {any} paramError - Description for paramError
@returns {any} - Result of the operation
 */
this.firebasePasswordReset = function (paramEmail, paramSuccess, paramError) {

        firebase.auth().useDeviceLanguage();
        auth.sendPasswordResetEmail(paramEmail).then(function () {

            if (paramSuccess) self.do(paramSuccess);

        }).catch(function (error) {

            if (paramError) self.do(paramError, undefined, error.message);

        });
    };

    
/*
 Method: firebaseUpdateProfile
Description: Functionality for firebaseUpdateProfile.
@param {any} params - Description for params
@param {any} paramSuccess - Description for paramSuccess
@param {any} paramError - Description for paramError
@returns {any} - Result of the operation
 */
this.firebaseUpdateProfile = function (params, paramSuccess, paramError) {
        self.log('firebaseUpdateProfile');
        self.log(params);
        auth.currentUser.updateProfile(params).then(function () {

            self.log('User Profile Updated Successfully');
            if (paramSuccess) self.do(paramSuccess);

        }).catch(function (error) {

            if (paramError) self.do(paramError, undefined, error.message);

        });
    };

    
/*
 Method: firebaseRegister
Description: Functionality for firebaseRegister.
@param {any} params - Description for params
@param {any} success - Description for success
@param {any} error - Description for error
@returns {any} - Result of the operation
 */
this.firebaseRegister = function (params, success, error) {
        if (params.email && params.password) {

            auth.createUserWithEmailAndPassword(params.email, params.password).then(cred => {
                self.log(cred.user);

                var user = firebase.auth().currentUser;

                if (params.success) self.do(params.success);
            }).catch(error => {

                if (params.error) self.do(params.error, undefined, error.message);
            });
        }
    };

    
/*
 Method: firebaseLogin
Description: Functionality for firebaseLogin.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseLogin = function (params) {
        self.log('firebaseLogin');
        self.log(params);

        if (auth) {
            auth.signInWithEmailAndPassword(String(params.email), String(params.password)).then(cred => {

                var user = firebase.auth().currentUser;

                if (user.emailVerified) {
                    if (params.success) self.do(self.replaceResult(params, undefined, cred));

                } else {
                    self.sendEmailVerification(params);
                }
            }).catch(error => {
                self.log('error.code');
                self.log(error.code);
                if (params.error) self.do(self.replaceResult(params, undefined, error.message));

            });
        } else {

        }
    };

    
/*
 Method: firebaseLogout
Description: Functionality for firebaseLogout.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.firebaseLogout = function (params) {
        self.log('firebaseLogout');
        firebase.auth().signOut().then(function () {
            self.log('firebaseLogout SUCCESS');
            self.log(params.success);
            if (params.success) {

                localStorage.clear();

                self.do(params.success);
            }
        }).catch(function (error) {
            self.log('firebaseLogout ERROR');
            self.log(error);

            if (params.error) self.do(params.error);
        });
    };

    
/*
 Method: sendEmailVerification
Description: Functionality for sendEmailVerification.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.sendEmailVerification = function (params) {
        self.log('sendEmailVerification');
        var user = firebase.auth().currentUser;
        firebase.auth().languageCode = 'it';
        var successAction = params.success || self.reload;
        user.sendEmailVerification().then(function () {
            firebase.auth().signOut();
            self.alert({

                icon: "success",
                title: "verifyingemail",  
                html: "msgverifyingemail",

                showConfirmButton: false,
                confirm: successAction
            });

        });
    };

    
/*
 Method: path
Description: Functionality for path.
@param {any} pathParams - Description for pathParams
@param {any} args - Description for args
@param {any} separatorParam - Description for separatorParam
@returns {any} - Result of the operation
 */
this.path = function (pathParams, args, separatorParam) {
        var pathString = '';
        var separator = separatorParam || '/';

        if (typeof pathParams == 'string') {
            pathString = self.replaceProperties(pathParams, args);
        } else {
            for (var index in pathParams) {
                pathObj = pathParams[index];
                if (pathString !== '') pathString += separator;
                if (pathObj.string)
                    pathString += String(pathObj.string);
                else if (pathObj.number)
                    pathString += Number(pathObj.number);
                else if (pathObj.function)
                    pathString += self.doFunctionByName(pathObj.function, window, '');
            }
        }
        return pathString;
    };

    
/*
 Method: executeFunctionByName
Description: Functionality for executeFunctionByName.
@param {any} functionName - Description for functionName
@param {any} context - Description for context
@returns {any} - Result of the operation
 */
this.executeFunctionByName = function (functionName, context  ) {

        if (functionName) {
            var args = [].slice.call(arguments).splice(2);
            var namespaces = functionName.split(".");
            var func = namespaces.pop();
            for (var i = 0; i < namespaces.length; i++) {
                context = context[namespaces[i]];
            }
            if (context[func] !== undefined) {
                return context[func].apply(context, args);
            } else {

            }
        } else {
            self.log('executeFunctionByName');
            self.log('functionName undefined');
        }

    };

    this.functions = {};

    this.methods = {

        run: function (params, container, args) { self.run(params, container, args); },
        delay: function (params, container, args) { self.delay(params, container, args); },
        ajax: function (params, container, args) { self.ajax(params, container, args); },
        do: function (params, container, args) { self.do(params, container, args); },
        module: function (params, container, args) { self.module(params, args); },
        js: function (params, container, args) { self.js(params, args); },
        function: function (params, container, args) { self.function(params, args); },
        dispatchEvent: function (params, container, args) { self.dispatchEvent(params, args); },

        lang: function (params, container, args) { self.lang(params, args); },
        find: function (params, container, args) { self.find(params, args); },
        page: function (params, container, args) { self.page(params, args); },

        for: function (params, container, args) { self.for(params, container, args); },
        if: function (params, container, args) { self.if(params, container, args); },
        switch: function (params, container, args) { self.switch(params, args); },
        set: function (params, container, args) { self.set(params, args); },

        data: {
            set: function (params, args) {
                for (var param in params) {

                    var value = params[param];

                    if (value) value = self.replaceProperties(value, args);
                    if (value && args) value = self.replacePropertyWithPrefix(value, 'result', args);  

                    self.element({ path: param, value: value });
                }
            },
            get: function (params) {
                return self.element(params);
            },
            delete: function (params, args) {
                var path = self.replaceProperties(params, args);
                alert(path);
                self.objToDelete = self.element({ path: path });
                delete self.objToDelete;
            },
            add: function (params, args) {
                for (var param in params) {
                    var path = self.replaceProperties(param, args);
                    var value = self.replaceProperties(params[param], args);
                    if (value) {
                        var obj = self.element({ path: path }) || '';
                        obj += String(value);
                        self.element({ path: path, value: obj });
                    }
                }
            },
            sum: function (params, args) {
                console.log('%cdeprecated key "sum". Use set{x: "{x}+1"}', 'color:orange');
                console.log(params);
                console.log(args);
                for (var param in params) {
                    var path = self.replaceProperties(param, args);
                    var value = self.replaceProperties(params[param], args);
                    if (value) {
                        var obj = self.element({ path: path }) || 0;
                        obj += Number(value);
                        self.element({ path: path, value: obj });
                    }
                }
            },
            sub: function (params, args) {
                console.log('%cdeprecated key "sub". Use set{x: "{x}+1"}', 'color:orange');
                console.log(params);
                console.log(args);
                for (var param in params) {
                    var path = self.replaceProperties(param, args);
                    var value = self.replaceProperties(params[param], args);
                    if (value) {
                        var obj = self.element({ path: path }) || 0;
                        obj -= Number(value);
                        self.element({ path: path, value: obj });
                    }
                }
            },
            push: function (params, args) {
                for (var param in params) {
                    var path = self.replaceProperties(param, args);
                    var value = self.replaceProperties(params[param], args);
                    if (value) {
                        var obj = self.element({ path: path }) || [];
                        obj.push(value);
                        self.element({ path: path, value: obj });
                    }
                }
            }
        },

        text: function (params, container, args) { self.text(params, container, args); },  
        html: function (params, container, args) { self.html(params, container, args); },
        attr: function (params, container, args) { self.attr(params, container, args); },
        empty: function (params, container, args) { self.empty(params, args); },
        delete: function (params, args) {
            var path = self.replaceProperties(params, args);

            var obj = self.element({ path: path });
            self.log(JSON.stringify(obj));
            self.element({ path: path, delete: true });

            self.log(obj);
        },
        add: function (params, container, args) {
            for (var param in params) {
                var path = self.replaceProperties(param, args);
                var value = self.replaceProperties(params[param], args);

                if (value) {
                    var obj = self.element({ path: path }) || '';
                    obj += String(value);
                    self.element({ path: path, value: obj });
                }
            }
        },
        sum: function (params, container, args) {
            for (var param in params) {
                var path = self.replaceProperties(param, args);
                var value = self.replaceProperties(params[param], args);
                if (value) {
                    var obj = self.element({ path: path }) || 0;
                    obj += Number(value);
                    self.element({ path: path, value: obj });
                }
            }
        },
        sub: function (params, container, args) {
            for (var param in params) {
                var path = self.replaceProperties(param, args);
                var value = self.replaceProperties(params[param], args);
                if (value) {
                    var obj = self.element({ path: path }) || 0;
                    obj -= Number(value);
                    self.element({ path: path, value: obj });
                }
            }
        },
        push: function (params, container, args) {
            for (var param in params) {
                var path = self.replaceProperties(param, args);
                var value = self.replaceProperties(params[param], args);
                if (value) {
                    var obj = self.element({ path: path }) || [];
                    obj.push(value);
                    self.element({ path: path, value: obj });
                    self.log('push obj');
                    self.log(obj);
                }
            }
        },

        calendar: function (params, container, args) { self.calendar(params, args); },  
        array: function (params, container, args) { self.array(params, args); },
        replace: function (params, container, args) { self.replace(params, args); },
        shuffle: function (array) {
            let currentIndex = array.length, randomIndex;

            while (currentIndex != 0) {

                randomIndex = Math.floor(Math.random() * currentIndex);
                currentIndex--;

                [array[currentIndex], array[randomIndex]] = [
                    array[randomIndex], array[currentIndex]
                ];
            }
            return array;
        },

        alert: function (params, container, args) { self.alert(params, args); },  
        offcanvas: function (params, container, args) { self.offcanvas(params, args); },  

        out: function (params, container, args) { self.out(params, container, args); },  
        qrcode: function (params, container, args) { self.qrcode(params, container, args); },  
        hide: function (params, container, args) { self.hide(params, container, args); },  
        show: function (params, container, args) { self.show(params, container, args); },  
        toggle: function (params, container, args) { self.toggle(params, container, args); },  
        sortablejs: function (params, container, args) { self.sortablejs(params, args); },  
        lottie: function (params, container, args) { self.lottie(params, container, args); },  
        chart: function (params, container, args) {
            var selector = self.selector(container);
            var element = self.query(selector);
            element.innerHTML = '<canvas width="100%" height="100%"></canvas>';
            const ctx = element.querySelector('canvas');
            const myChart = new Chart(ctx, self.replaceProperties(params));
        },

        animate: function (params, container, args) { self.animate(params, container, args); },

        uiUpdate: function (params, container, args) { self.uiUpdate(params, args); },
        swiper: {
            data: {},
            init: function (params, selector, args) {

                setTimeout(function () {

                    console.log('swiper inited');
                    console.log(selector || params.selector);
                    console.log(params.options);
                    var swiperConfig = params.options;
                    self.methods.swiper.data[params.name] = new Swiper(self.selector(selector || params.selector), swiperConfig);
                    console.log(self.methods.swiper.data[params.name]);
                }, 1000);
            }
        },

        link: function (params, container, args) { self.link(params, args); },
        scroll: function (params, container, args) { self.scroll(params, args); },
        reload: function (params, container, args) { self.reload(params, args); },
        timer: function (params, container, args) { self.timer(params, args); },
        setInterval: function (params, container, args) { self.setInterval(params, args); },
        clearInterval: function (params, container, args) { self.clearInterval(params, args); },
        setTimeout: function (params, container, args) { self.setTimeout(params, args); },
        clearTimeout: function (params, container, args) { self.clearTimeout(params, args); },
        local: {
            set: function (params) {
                for (var param in params) {
                    var value = params[param];
                    if (value) value = self.replaceProperties(value);
                    if (typeof value == 'object') value = JSON.stringify(value);
                    localStorage.setItem(param, value);
                }
            },
            get: function (key) {
                var localValue = localStorage.getItem(key);
                if (localValue && self.isJsonString(localValue))
                    localValue = JSON.parse(localValue);
                return localValue;
            },
            remove: function (key) {
                localStorage.removeItem(key);
            },
            clear: function () {
                localStorage.clear();
            }
        },
        storage: {
            _storage: new WeakMap(),
            set: function (element, key, obj) {
                if (!self.methods.storage._storage.has(element)) {
                    self.methods.storage._storage.set(element, new Map());
                }
                self.methods.storage._storage.get(element).set(key, obj);
            },
            get: function (element, key) {
                return self.methods.storage._storage.get(element).get(key);
            },
            has: function (element, key) {
                return self.methods.storage._storage.has(element) && self.methods.storage._storage.get(element).has(key);
            },
            remove: function (element, key) {
                var ret = self.methods.storage._storage.get(element).delete(key);
                if (!self.methods.storage._storage.get(element).size === 0) {
                    self.methods.storage._storage.delete(element);
                }
                return ret;
            }
        },

        choose: function (params, container, args) { self.choose(params, container, args); },
        remove: function (params, container, args) { self.remove(params, container, args); },
        part: function (params, container, args) { self.part(params, container, args); },
        blocks: function (params, container, args) { self.part(params, container, args); },  
        block: function (params, container, args) { self.part(params, container, args); },
        ace: function (params, container, args) { self.ace(params, container, args); },
        code: function (params, container, args) { self.code(params, container, args); },

        database: function (params, container, args) { self.database(params, args); },  
        firebase: function (params, container, args) { self.firebase(params, args); },

        firebaseEvent: {
            action: function (newObj) {

                var path = newObj.path;
                delete newObj.path;

                document.querySelectorAll('[data-firebase="' + path + '"]').forEach(function (element, index) {

                    var pathString = 'self.json.var.db.' + self.replaceAll(element.getAttribute('data-firebase'), '/', '.');  
                    var dbObj = self.docElement(pathString);

                    var firebaseValue = newObj[element.getAttribute('data-value')];  

                    var template = self.dataStorage.get(element, 'data-template');

                    if (element.tagName == 'UL') {  

                        var liTemplate = template.li || template.template;  

                        element.innerHTML = '';

                        var items = newObj;
                        for (itemKey in items) {
                            var item = items[itemKey];
                            item.key = itemKey;

                            var dbParams = self.replaceItems(liTemplate, item, 'item') || '';

                            var container = element.getAttribute('data-selector');

                            self.li(dbParams, { selector: container });
                        }

                    } else if (element.tagName == 'SPAN' || element.tagName == 'P') {

                        element.textContent = firebaseValue;

                    } else if (element.tagName == 'SVG') {

                    } else {  

                        if (template.blocks || template.html) {
                            var blocksTemplate = template.html || template.blocks;

                            element.innerHTML = '';

                            var items = newObj;
                            self.log('newObj');
                            self.log(newObj);
                            for (itemKey in items) {
                                var item = items[itemKey];
                                item.key = itemKey;
                                var dbParams = self.replaceProperties(blocksTemplate, undefined, item) || '';

                                self.html(dbParams, { selector: container });
                            }
                        }

                        if (template.init) self.do(template.init);  

                    }

                });
            }
        },

        auth: {
            init: function (params, container, args) {
                self.log("auth init");
                auth = firebase.auth();
                auth.onAuthStateChanged(user => {
                    self.json.var.user = user;
                    if (!user) {

                        self.log('authStateChanged: GUEST\n');

                    } else {

                        self.log('authStateChanged: USER\n');
                        self.log('name: ' + self.user('displayName') + '\nemail: ' + user.email + '\nuid:' + user.uid);

                    }

                    self.do(params.onAuthStateChanged);

                });
            },

            sendEmailVerification: function (params, container, args) {
                self.sendEmailVerification(params);
            },

            login: function (params, container, args) {
                self.log('params.auth');
                self.log(params.auth);  
                self.log(args);  
                var fba = self.replaceProperties(params.auth, args);
                self.log('fba');
                self.log(fba);
                var fbAuth = self.cloneObject(fba);
                self.log('fbAuth');
                self.log(fbAuth);
                if (params.success) fbAuth.success = params.success;
                if (params.error) fbAuth.error = params.error;
                self.firebaseLogin(fbAuth, params.success, params.error);
            },

            register: function (params, container, args) {

                var fbAuth = self.replaceProperties(params.auth, args);
                fbAuth.success = params.success;
                fbAuth.error = params.error;

                self.firebaseRegister(fbAuth, params.success, params.error);
            },

            updateProfile: function (params, container, args) {

                params.profile = self.replaceProperties(params.profile, args);
                self.firebaseUpdateProfile(params.profile, params.success, params.error);
            },

            passwordReset: function (params, container, args) {

                params.email = self.replaceProperties(params.email, args);
                self.firebasePasswordReset(params.email, params.success, params.error);
            },

            logout: function (params, container, args) {
                self.firebaseLogout(params);
            }
        },

        thunkable: function (params, container, args) { self.thunkable(params, args); },

        modelViewer: {
            events: {},
            on: function (params) {
                let container = self.selector(params.selector || params.container);
                let element = self.query(container);
                self.log('modelViewer on');
                self.log('container');
                self.log(container);
                self.log('element');
                self.log(element);
                if (element) {
                    self.methods.modelViewer.events[container] = {};  

                    var event = 'load';
                    if (params[event]) {

                        self.methods.modelViewer.events[container][event] = params[event];
                        self.log('modelViewer element');
                        self.log(element);
                        element.addEventListener(event, (e) => {
                            self.log('modelViewer event');
                            self.log(e);
                            let container = e.path[0].getAttribute('data-selector');
                            self.log(container);

                            self.do(self.methods.modelViewer.events[container][e.type]);
                        });
                    }
                }
            },
            set: function (params) {
                let element = self.query(self.selector(params.selector || params.container));
                if (element) {
                    if (params.color) {
                        let color = params.color.split(',').map(numberString => parseFloat(numberString));
                        self.log('Changing color to: ', color);
                        const [material] = element.model.materials;
                        material.pbrMetallicRoughness.setBaseColorFactor(color);
                    }
                    if (params.exposure) element.exposure = params.exposure;
                    if (params.shadow) element.shadowIntensity = params.shadow;
                    if (params.orientation) element.orientation = params.orientation;

                }
            }
        }
    };

    
/*
 Method: googleSignInPopup
Description: Functionality for googleSignInPopup.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.googleSignInPopup = function () {
        self.log('googleSignInPopup');
        var provider = new firebase.auth.GoogleAuthProvider();

        firebase.auth().useDeviceLanguage();  

        firebase.auth()
            .signInWithPopup(provider)
            .then((result) => {

                var credential = result.credential;

                var token = credential.accessToken;

                var user = result.user;
                window.location.reload();

            }).catch((error) => {

                var errorCode = error.code;
                var errorMessage = error.message;

                var email = error.email;

                var credential = error.credential;
            });
    };

    
/*
 Method: console
Description: Functionality for console.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.console = function (params, args) {

        if (typeof params.log == 'string' && params.color) {
            var color = params.color || 'white';
            console.log('%c' + self.replaceProperties(params.log), 'color:' + color);
        } else {
            console.log(self.replaceProperties(params.log || params));
        }
    };

    
/*
 Method: module
Description: Functionality for module.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.module = function (params, args) {
        if (typeof params == 'string') {
            var nameReplaced = self.replaceProperties(params, args);
            return self.element({ root: self.modules, path: nameReplaced });
        } else {
            if (params.name) {
                if (params.value) {
                    var name = self.replaceProperties(params.name, args);

                    var value = self.replaceProperties(value, args);
                    self.log('module');
                    self.log(name);
                    self.log('value');
                    self.log(value);
                    self.log(value.app.html.div.div[0].text.lang.en);
                    self.modules[name] = value;
                } else if (params.url) {
                    self.addModule(params);  
                }
            }
        }
    };

    
/*
 Method: db
Description: Functionality for db.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.db = function (params, args) {
        self.log('db');
        if (typeof params == 'string') {
            var paramsReplaced = self.replaceProperties(params, args);
            self.log('paramsReplaced');
            self.log(paramsReplaced);
            self.log(self.var('db.' + paramsReplaced, args));

            return self.var('db.' + paramsReplaced, args);  

        }  

    };

    
/*
 Method: firebase
Description: Functionality for firebase.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.firebase = function (params, args) {
        if (params.initializeApp) {
            var firebaseConfig = self.replaceProperties(params.initializeApp);
            firebase.initializeApp(firebaseConfig);
        } else {
            self.database(params, args);
        }
    };

    
/*
 Method: database
Description: Functionality for database.
@param {any} params - Description for params
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.database = function (params, args) {

        if (params.do == 'init') {
            database = firebase.database();
            self.log('database');
            self.log(database);
        } else {
            if (database) {

                if (typeof params == 'string') {

                    return self.db(params, args);  

                } else {

                    if (params) {
                        if (params.path) {
                            params.path = self.path(params.path, args);
                        }

                        if (params.value) {

                            params.value = self.replaceProperties(params.value, args);

                            self.log("params.value post replace");
                            self.log(params.value);
                        }

                        if (params.do == 'init') {
                        } else if (params.do == 'increase') {

                            self.firebaseIncrease(params);

                        } else if (params.do == 'get') {

                            self.firebaseGet(params.path, function (result) {
                                if (result.success) {
                                    if (result.data) {
                                        self.log('firebase get SUCCESS');
                                        self.log(params.success);
                                        if (params.on && params.on.success) self.do(params.on.success, undefined, result);

                                    } else {
                                        self.log('firebase get no result.data ERROR');

                                        if (params.on && params.on.error) self.do(params.on.error);
                                    }
                                } else {
                                    self.log('firebase get ERROR');

                                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                }
                            });
                        } else if (params.do == 'new' && params.value) {
                            var newKey = self.getKey(params.path);
                            self.firebaseSet(params.path + '/' + newKey, params.value, function (result) {
                                if (result.success) {
                                    self.log('firebase set SUCCESS');
                                    if (params.on && params.on.success) self.do(params.on.success, undefined, result);

                                } else {
                                    self.log('firebase set ERROR');
                                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                }
                            });
                        } else if (params.do == 'remove' && params.path) {
                            self.firebaseRemove(params.path, function (result) {
                                if (result.success) {
                                    self.log('firebase set SUCCESS');

                                    if (params.on && params.on.success) self.do(params.on.success, undefined, result);
                                } else {
                                    self.log('firebase set ERROR');
                                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                }
                            });
                        } else if (params.do == 'addListener') {
                            self.firebaseAddListener(params);
                        } else if (params.do == 'removeListener') {
                            self.firebaseRemoveListener(params);
                        } else if (params.do == 'update') {
                            if (params.value) {  
                                self.firebaseUpdate(params.path, params.value, function (result) {
                                    if (result.success) {
                                        self.log('firebase update SUCCESS');
                                        if (params.on && params.on.success) self.do(params.on.success, undefined, result);

                                    } else {
                                        self.log('firebase update ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                    }
                                });
                            } else {
                                self.log('data update requires value param');
                            }
                        } else if (params.do == 'push') {
                            if (params.value) {  
                                self.firebasePush(params.path, params.value, function (result) {
                                    if (result.success) {
                                        self.log('firebase push SUCCESS');
                                        if (params.on && params.on.success) self.do(params.on.success, undefined, result);

                                    } else {
                                        self.log('firebase push ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                    }
                                });
                            } else {
                                self.log('data push requires value param');
                            }
                        } else {
                            if (params.value) {  
                                self.firebaseSet(params.path, params.value, function (result) {
                                    if (result.success) {
                                        self.log('firebase set SUCCESS');
                                        if (params.on && params.on.success) self.do(params.on.success, undefined, result);

                                    } else {
                                        self.log('firebase set ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);

                                    }
                                });
                            }
                        }

                    }
                }
            } else {
                self.log('"database" function needs to be initialised');
            }
        }
    };

    
/*
 Method: ajax
Description: Functionality for ajax.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.ajax = function (params, selectorParams, args) {

        if (params.url) {

            var url = self.replaceProperties(params.url, args);
            var data;
            if (params.data)
                data = JSON.stringify(self.replaceProperties(params.data, args));

            var type = params.type || 'POST';
            var request = new XMLHttpRequest();
            request.open(type, url, true);

            request.onload = function () {
                self.log('onload');
                self.log(this);
                if (this.status >= 200 && this.status < 400) {

                    var result = {};

                    var response = this.response;
                    response = response.replace(/(\\\")/g, '&quot;');
                    try {
                        response = decodeURI(response);
                    } catch (error) {
                        /* console.log('response');
                        console.log(response); */
                        console.log('decodeURI error');
                        console.log(error);
                    }
                    result.response = self.parse(response);
                    result.status = this.status;
                    result.header = this.getAllResponseHeaders().split('\r\n').reduce((resultHeader, current) => {
                        let [name, value] = current.split(': ');
                        resultHeader[name] = value;
                        return resultHeader;
                    }, {});
                    //} catch (e) {
                    //}
                    self.log('result');
                    self.log(result);
                    if (params.success)
                        self.do(params.success, undefined, result);
                    //self.do(params.success, result);
                    //self.do(self.replaceProperty(params.success, 'result', result), result);
                } else {
                    // We reached our target server, but it returned an error
                    if (params.error)
                        self.do(params.error, undefined, 'Server error');
                    //self.do(params.error, 'Server error');
                }
            };
            request.onerror = function (error) {
                // There was a connection error of some sort
                self.log('error');
                self.log(error);
                self.log('this');
                self.log(this);
                if (params.error)
                    self.do(params.error, undefined, 'Server unavailable');
                //self.do(params.error, 'Server unavailable');
            };
            //request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
            if (data)
                request.send(data);

            else
                request.send();
        } else {
            self.log('ajax method requires url parameter');
        }

        /*  $.ajax({
            url: paramsReplaced.url,
            type: params.type || 'post',
            //dataType: 'json', // questo dovrebbe evitare JSON.parse
            //dataType: 'text', // verificare se necessario
            data: paramsReplaced.data || {},
            success: function (result) {
                self.log('ajax');
                self.log('result');
                self.log(result);
                self.do(params.success, result);
                //self.do(data); // in .params dovrebbe andare il resto
            },
            error: function (error) {
                var errorText = error.responseText || error;
                self.do(params.error, errorText);
                //self.do(data); // in .params dovrebbe andare il resto
            }
        }); */
    };

    /* View in fullscreen */
    
/*
 Method: openFullscreen
Description: Functionality for openFullscreen.
@param {any} id - Description for id
@returns {any} - Result of the operation
 */
this.openFullscreen = function (id) {
        var element = document.getElementById(id);

        if (screenfull.isEnabled) {
            screenfull.request(element);
        }

    };

    /*    
/*
 Method: addListener
Description: Functionality for addListener.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.addListener = function(params) {
            var selector = self.selector(params);
            if (selector)
                $(selector).on(params.event, self.do(params.action));
                //event.stopPropagation();
        } */
    /* Close fullscreen */
    
/*
 Method: closeFullscreen
Description: Functionality for closeFullscreen.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.closeFullscreen = function () {

        //document.exitFullscreen();
        screenfull.off('change'); // callback
    };

    /*         
/*
 Method: list
Description: Functionality for list.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.list = function (params, selectorParams) {

            var container = (selectorParams) ? self.selector(selectorParams) : params.container;
            var selector = self.selector(self.extend({}, params, selectorParams));

            if (selector) {

                if (params.list) params.items = params.list; // retro-compatibilità

                if (params.listAction) $(selector).data('listAction', params.listAction);
                if (params.listClass) $(selector).data('listClass', params.listClass); // ul

                if (params.items) {

                    $(selector).empty();
                    var index = 0;

                    //params.items = self.replaceTags({text:params.items, args:selectorParams});
                    //params.items = self.replaceResult(params.items, selectorParams);
                    params.items = self.replaceProperties(params.items, selectorParams);

                    for (var itemKey in params.items) {
                        // da rimuovere id="item'+itemKey+'" href=""
                        //self.log('PARAMETRI');
                        //self.log(itemKey);
                        //self.log(params.items);

                        $(selector).append('<li data-index="'+itemKey+'" class="list-group-item"></li>');
                        var item = params.items[itemKey];
                        //var itemSelector = selector + ' li:eq(' + index + ')';
                        var itemSelector = selector + ' li:eq(' + itemKey + ')';

                        // Item action
                        var itemAction;
                        if (item.action)
                            itemAction = item.action;
                        else if (params.action)
                            itemAction = params.action;
                        else if ($(selector).data('listAction'))
                            itemAction = $(selector).data('listAction');
                        item.action = itemAction;

                        if (item.action) {
                            $(itemSelector).addClass('list-group-item-action');
                            $(itemSelector).css('cursor', 'pointer');
                        }
                        if (item.color) $(itemSelector).css('color', item.color);
                        if (item.background) $(itemSelector).css('background', item.background);

                        //$(itemSelector).append('<div></div>');

                        var itemFields = params.items[itemKey].fields || params.items[itemKey];  // retro-compatibilità
                        //self.log('itemFields');
                        //self.log(itemFields);

                        for (var itemField in itemFields) {
                            var field = itemFields[itemField];
                            //self.log(field);
                            if (itemField == 'icons') {

                                self.icons(field, {container: itemSelector});

                            } else if ((itemField != 'params') && (itemField != 'action')) { // retro-compatibilità
                                    $(itemSelector).append('<span data-field="'+itemField+'">'+self.text(itemFields[itemField])+'</span>');
                            }
                        }

                        // action
                        if (item.action) {
                            $(itemSelector).data('onData', item);
                            $(itemSelector).on(self.touch, function (event) {
                                event.stopPropagation();
                                self.onEvent(this, event);
                            });
                        }
                        // events
                        if (item.on) self.on(item.on, {container: selector});

                        index++;
                    }
                } else if (params.append) {
                    // aggiunge una riga alla fine (calcolare path)
                } else if (params.prepend) {
                    // aggiunge una riga all'inizio (calcolare path)
                    // sballa tutti i dati presenti in var.functions
                    // sarebbe meglio mem in data value tutto il json
                    // azione
                }

                // List classes

                //if (params.id) $(selector).attr('id', params.id);
                var listClass = $(selector).data('listClass');

                if (listClass) {
                    var ulClass = listClass.ul || '';
                    var liClass = listClass.li || '';

                    $(selector).addClass(ulClass);
                    $(selector + ' > li').addClass(liClass);

                    var index = 0;
                    for (var field in listClass.fields) {
                        var fieldClass = listClass.fields[field] || '';
                        $(selector + ' > li > span[data-field='+field+']').addClass(fieldClass);
                        index++;
                    }
                }
            }
        } */
    
/*
 Method: scroll
Description: Functionality for scroll.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.scroll = function (params) {
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
        // {id, class, value}
        var selector = self.selector(params.selector || params.container);
        var block = document.querySelector(selector);
        //var offset = elem.dataset.offset ? parseInt(elem.dataset.offset) : 0;
        if (block) {
            var bodyOffset = document.body.getBoundingClientRect().top;
            window.scrollTo({
                top: block.getBoundingClientRect().top - bodyOffset, // + offset,
                behavior: 'smooth'
            });
        } else {
            self.log('scroll to unknown element: ' + selector);
        }
    };

    /*         
/*
 Method: scroll
Description: Functionality for scroll.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.scroll = function (params) {
    //                var selector = self.selector(params);
            //var obj = $(selector).get(0);

            var selector = "g."+params.class+".myLabelStyle."+params.value;

            //var parent = $(selector).parent().get(0);

            //document.querySelector(selector).scrollIntoView();
            //self.resizeEvent(); // bug workaround
            //$(selector).parent().scrollTop($(selector).position().top);
                $(selector).parent().animate({
                //scrollTop: $(selector).position().top
                //scrollTop: $(selector).position().top,
                transform: 'translate(0,-'+$(selector).position().top+'), scale(1)'
            });
        }
    */
    //------------------------
    /* 
/*
 Method: do
Description: Functionality for do.
@param {any} data - Description for data
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.do = function (data, args) {
        if (data) {
            if (typeof data == 'string' && self.json.actions[data]) {
                self.do(self.json.actions[data], args);
            } else {
                if (!data.index) data.index = 0;
                if (!data.tasks) {
                    if (Array.isArray(data)) {

                        if (data.index < data.length) {
                            self.do(data[data.index]);
                            data.index = data.index +1;
                            self.do(data);
                        } else {
                            delete data.index;
                        }
                    } else {
                        self.do(data);
                    }
                //} else if (data.tasks.length > 0) {
                } else {
                    if (data.index < data.tasks.length) {
                        var currentTask = data.tasks[data.index];
                        //var currentTask = data.tasks.shift();
                        if (currentTask.params == undefined) {currentTask.params = {}}
                        if (data.params == undefined) {data.params = {}}
                        if (currentTask.if == undefined) {currentTask.if = true}
                        var taskParams = currentTask.params; // parametri specifici del task
                        taskParams.params = data.params;
                        taskParams.index = data.index+1;
                        //for (var param in data.params) {taskParams[param] = data.params[param]; } // merge
                        taskParams.tasks = data.tasks;
                        var taskIf = (typeof currentTask.if == "function") ? currentTask.if() : currentTask.if;

                        if (taskIf) {
                            if (currentTask.delay !== undefined) {
                                setTimeout(function () {
                                    self.do(currentTask.task, taskParams);

                                }, currentTask.delay);
                            } else if (currentTask.interval !== undefined) {
                                self.createTaskInterval(currentTask);
                            } else {
                                self.do(currentTask.task,taskParams);

                            }
                        } else {
                            self.do(taskParams);
                        }
                    }
                }
            }
        }
    } */
    /*     
/*
 Method: playTaskInterval
Description: Functionality for playTaskInterval.
@param {any} taskFunction - Description for taskFunction
@returns {any} - Result of the operation
 */
this.playTaskInterval = function (taskFunction) {
            self.log('playTaskInterval');
            var taskName = taskFunction.name;
            if (tasksIntervals[taskName]) {
                clearInterval(tasksIntervals[taskName].id);
                tasksIntervals[taskName].id = setInterval(function () {
                    tasksIntervals[taskName].task(tasksIntervals[taskName].params);
                }, tasksIntervals[taskName].interval);
            } else {
                self.log('taskName: '+taskName);
            }
        }

        
/*
 Method: stopTaskInterval
Description: Functionality for stopTaskInterval.
@param {any} taskFunction - Description for taskFunction
@returns {any} - Result of the operation
 */
this.stopTaskInterval = function (taskFunction) {
            self.log('stopTaskInterval');
            var taskName = taskFunction.name;
            if (tasksIntervals[taskFunction.name]) {
                clearInterval(tasksIntervals[taskFunction.name].id);
            } else {
                self.log('taskName: '+taskName);
            }
        }

        
/*
 Method: createTaskInterval
Description: Functionality for createTaskInterval.
@param {any} myTask - Description for myTask
@returns {any} - Result of the operation
 */
this.createTaskInterval = function (myTask) {
            tasksIntervals[myTask.task.name] = {};
            tasksIntervals[myTask.task.name].task = myTask.task;
            tasksIntervals[myTask.task.name].params = myTask.params;
            tasksIntervals[myTask.task.name].interval = myTask.interval;
            self.playTaskInterval(myTask.task.name);
        }

        
/*
 Method: addTimedTask
Description: Functionality for addTimedTask.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.addTimedTask = function (data) {
            self.log('addTimedTask');
            if (data.frequence && data.task) {
                if (!data.active) {data.active = true;}
                if (!timedTasks[data.frequence]) {timedTasks[data.frequence] = {}}
                timedTasks[data.frequence][data.task] = {active:data.active, params:{}};
                if (data.params) {
                    timedTasks[data.frequence][id].params = data.params;
                }
            }
        }

        
/*
 Method: removeTimedTask
Description: Functionality for removeTimedTask.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.removeTimedTask  = function (data) {
            self.log('removeTimedTask');
            if (data.frequence && data.task) {
                delete timedTasks[data.frequence][data.task];
            }
        }

        
/*
 Method: doTimedTask
Description: Functionality for doTimedTask.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.doTimedTask = function (data) {
            self.log('doTimedTask');
            self.log(data);
            for (var task in timedTasks[data.frequence]) {
                var taskObj = timedTasks[data.frequence][task];
                if (taskObj.active) {
                    self.do(task, taskObj.params);
                }
            }
        } */
    //------------------------
    // CSS METHODS
    //------------------------
    /*        // SELETTORI
        var previewPanel = '.appPanel[data-value="pnlPreview"]';
        var toolsPanel = '.appPanel[data-value="pnlTools"]';

        var stagePanel = '.framePanel[data-value="pnlStage"]';
        var devicePanel = '.framePanel[data-value="pnlDevice"]';

        
/*
 Method: closePanel
Description: Functionality for closePanel.
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
this.closePanel = function(value) {
            self.log(value);

            switch (value) {
                case 'closePanelTools':
                    $(toolsPanel).fadeOut();
                    $(previewPanel).fadeIn();
                case 'closePanelFrame':
                    $(devicePanel).fadeOut();
                    $(stagePanel).fadeIn();
            }
            }
    */
    //------------------------
    // CSS METHODS
    //------------------------
    /*     
/*
 Method: cssProp
Description: Functionality for cssProp.
@param {any} propPar - Description for propPar
@returns {any} - Result of the operation
 */
this.cssProp = function (propPar) {
            var cssObj = {transform: ''};
            if ((propPar.x) && (propPar.y)) {cssObj.transform += ' translate('+propPar.x+'px,'+propPar.y+'px)'; }
            if (propPar.rotateY) {cssObj.transform += ' rotateY('+propPar.rotateY+'deg)'; }
            if (propPar.rotateX) {cssObj.transform += ' rotateX('+propPar.rotateX+'deg)'; }
            if (propPar.scale) {cssObj.transform += ' scale('+propPar.scale+','+propPar.scale+')'; }
            if (propPar.deg) {cssObj.transform += ' rotate('+propPar.deg+'deg)'; }
            if (propPar.opacity) {cssObj.opacity = propPar.opacity;}
            return cssObj;
        } */
    /*     
/*
 Method: getCSS
Description: Functionality for getCSS.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.getCSS = function (data) {
            var $inspector = $("<div>").css('display', 'none').addClass(data.class);
            $("body").append($inspector); // add to DOM, in order to read the CSS property
            try {
                var props = [];
                for (i=0; i<data.properties.length; i++) {props.push($inspector.css(data.properties[i]))}
                return props;
            } finally {
                $inspector.remove(); // and remove from DOM
            }
        }

            
/*
 Method: getTransform
Description: Functionality for getTransform.
@param {any} object - Description for object
@returns {any} - Result of the operation
 */
this.getTransform = function (object) {
            var transformArray = $(object).css('transform').match(/(-?[0-9\.]+)/g);
            var objTransform = {x:0,y:0};
            objTransform.x = transformArray[4];
            objTransform.y = transformArray[5];
            return objTransform;
        } */
    
/*
 Method: getClickPosition
Description: Functionality for getClickPosition.
@param {any} e - Description for e
@returns {any} - Result of the operation
 */
var getClickPosition = function (e) {
        var parentPosition = getPosition(e.currentTarget);
        var xPosition = e.clientX - parentPosition.x;
        var yPosition = e.clientY - parentPosition.y;
    };

    
/*
 Method: getPosition
Description: Functionality for getPosition.
@param {any} element - Description for element
@returns {any} - Result of the operation
 */
function getPosition(element) {
        var xPosition = 0;
        var yPosition = 0;

        while (element) {
            xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
            yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
            element = element.offsetParent;
        }
        return { x: xPosition, y: yPosition };
    }

    /*     
/*
 Method: cssTransform
Description: Functionality for cssTransform.
@param {any} namePar - Description for namePar
@param {any} fromPar - Description for fromPar
@param {any} toPar - Description for toPar
@returns {any} - Result of the operation
 */
var cssTransform = function (namePar, fromPar, toPar) {
            var sdDefineFrom = {transform: ''};
            var sdDefineTo = {transform: ''};
            if ((fromPar.x) && (fromPar.y)) {sdDefineFrom.transform += ' translate('+fromPar.x+'px,'+fromPar.y+'px)'; }
            if ((toPar.x) && (toPar.y)) {sdDefineTo.transform += ' translate('+toPar.x+'px,'+toPar.y+'px)'; }
            if (fromPar.scale) {sdDefineFrom.transform += ' scale('+fromPar.scale+','+fromPar.scale+')'; }
            if (toPar.scale) {sdDefineTo.transform += ' scale('+toPar.scale+','+toPar.scale+')'; }
            if (fromPar.deg !== undefined) {sdDefineFrom.transform += ' rotate('+fromPar.deg+'deg)'; }
            if (toPar.deg !== undefined) {sdDefineTo.transform += ' rotate('+toPar.deg+'deg)';}
            if (fromPar.opacity) {sdDefineFrom.transform += ' opacity'; sdDefineFrom.opacity = String(fromPar.opacity); }
            if (toPar.opacity) {sdDefineTo.transform += ' opacity'; sdDefineTo.opacity = String(toPar.opacity); }
            $.keyframe.define({name: namePar, from: sdDefineFrom, to: sdDefineTo});
        }

        
/*
 Method: defineTransform
Description: Functionality for defineTransform.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.defineTransform = function (data) {
            cssTransform(data.name, data.from, data.to);
            cssTransform(data.name+'Reverse', data.to, data.from);
            animations[data.name] = {name: data.name, duration:data.duration, timingFunction: data.ease, fillMode:'both'};
        } */
    /*     
/*
 Method: defineAnimations
Description: Functionality for defineAnimations.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.defineAnimations = function (data) {
            self.log('defineAnimations');

            for (var name in self.json.styles.animations) {
                self.json.styles.animations[name].name = name;
                self.defineTransform(self.json.styles.animations[name]);
            }

            self.do(data);
        }
            */
    /* 
/*
 Method: styleToAnimation
Description: Functionality for styleToAnimation.
@param {any} obj - Description for obj
@param {any} style - Description for style
@returns {any} - Result of the operation
 */
this.styleToAnimation = function (obj, style) {
        // inutilizzata
        $(obj).addClass('resetAnimation'); // Disable transitions
        $(obj).css(style);
        $(obj).eq(0).offsetHeight; // Trigger a reflow, flushing the CSS changes
        $(obj).removeClass('resetAnimation'); // Re-enable transitions
        // https://stackoverflow.com/questions/11131875/what-is-the-cleanest-way-to-disable-css-transition-effects-temporarily
    } */
    //------------------------
    // DIPENDENZE TWEENMAX
    //------------------------
    /*  
/*
 Method: getImage
Description: Functionality for getImage.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.getImage = function (data) {
            // corrisponde al metodo fixo.loadImage
            // ma senza la dipendenza con fixo.iconUrl
            // che aggiunge il dominio ai nomi di file senza url
            // se si vuole utilizzare al posto di fixo.loadImage
            // data.url = fixo.iconUrl(data.url); va fatta in fixo.js
            // fixo.loadImage è usato sicuramente in hom.js / spo.js
            // e in fixo.items.js

            if (data.style)
                $(data.object).css(data.style);
            //if (data.animation == undefined) {TweenMax.to(data.object,0,{opacity:0,display:'block'});}
            if (data.animation !== undefined)
                $(data.object).css({opacity:0, display:'none'});
            if (TweenMax) TweenMax.set(data.object,{backgroundImage:'url('+data.url+')'});
            $(data.object).waitForImages({
                each: function(loaded, count, success) {if(!success) {log('error loadImage id:'+this.id);}},
                eachError: function() {
                    // http://stackoverflow.com/questions/6137934/how-should-i-handle-a-missing-image-in-my-plugin
                    self.log('error:'+urlPar);
                },
                finished: function () {
                    if (data.animation !== undefined) {
                        self.animation({object:data.object, animation:data.animation});
                    } else {
                        if (TweenMax) TweenMax.to(data.object,1,{autoAlpha:1,display:'block',float:'left'});
                        else $(data.object).css({display: 'block', opacity: '1'});
                    }
                    if (data.move != undefined) {
                        self.move({object:data.object, move:data.move, target:data.target});
                    }
                    if (data.action) {self.setAction({object:data.object, action:data.action});} // obsolete
                    if (data.on) {self.on(data.on, {container: data.object});}
                    if (data.onComplete) {self.do(data.onComplete)};
                },
                waitForAll: true
            });
            //$(objPar).waitForImages.progress(function(loaded, count, success) {alert(loaded + ' of ' + count + ' images has ' + (success ? 'loaded' : 'failed to load') +  '.');});
        } */
    /*     
/*
 Method: setAction
Description: Functionality for setAction.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.setAction = function (data) {
            $(data.object).off().on(self.touch, function () {
                var.touchTime = self.getTimestamp();
                self.do(data.action, data.value);
            });
        } */
    
/*
 Method: animate
Description: Functionality for animate.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.animate = function (params, selectorParams, args) {
        // We create a Promise and return it
        new Promise((resolve, reject) => {
            /* self.log('animate');
            self.log('params');
            self.log(params);

            self.log('selectorParams');
            self.log(selectorParams); */

            /* selectorParams.container = (params.container) ? params.container : selectorParams.container;
            selectorParams.id = (params.id) ? params.id : selectorParams.id ;
            selectorParams.class = (params.class) ? params.class : selectorParams.class;
            selectorParams.value = (params.value) ? params.value : selectorParams.value; */
            const prefix = 'animate__';
            //var selector = self.selector(self.extend({}, params, selectorParams), undefined, true); // selectAll
            let selector = self.selector(params.selector || params.container, undefined, true) || self.selector(selectorParams, undefined, true);
            //var selector = self.selector(params.selector || params.container, selectorParams, true); // selectAll
            /* self.log('animate');
            self.log('selector');
            self.log(selector); */
            var transition = params.transition || params;
            transition = self.replaceProperties(transition, args);

            const duration = params.duration || '0.5s';

            const animationName = `${prefix}${transition}`;

            if (!selector) {
                self.log('animate function without selector');
                self.log(params);
                self.log(selectorParams);
            } else {

                var elements = self.queryAll(selector);
                /* console.log('elements');
                console.log(elements); */
                //var elements = document.querySelectorAll(selector);
                if (elements)
                    elements.forEach(function (element, index) {
                        if (element) {
                            //element.classList.add(`${prefix}animated`, animationName);
                            //console.log(element.classList);
                            element.classList.add('animate__animated', animationName);
                            element.style.setProperty('--animate-duration', duration);
                        } else {
                            console.log('element');
                            console.log(element);
                        }
                    });

                if (params.on && params.on.start)
                    self.do(params.on.start, { selector: selector });
                //self.do(params.on.start, undefined, {selector: selector});
                if (params.style) self.css(params, selectorParams); // TO DO: deprecated / to be removed

                //if (params.infinite)
                //    node.style.setProperty('--animate-infinite', 'infinite');
                // When the animation ends, we clean the classes and resolve the Promise
                
/*
 Method: handleAnimationEnd
Description: Functionality for handleAnimationEnd.
@param {any} event - Description for event
@returns {any} - Result of the operation
 */
function handleAnimationEnd(event) {
                    if (params.on && params.on.end) {
                        self.do(params.on.end, { selector: selector });
                        //self.do(params.on.end, undefined, {selector: selector});
                        /* self.log('handleAnimationEnd');
                        self.log('params.on.end');
                        self.log(params.on.end); */
                    }

                    event.stopPropagation();

                    if (elements)
                        elements.forEach(function (element, index) {
                            if (element)
                                element.classList.remove('animate__animated', animationName);
                        });
                    resolve('Animation ended');
                }

                if (elements)
                    elements.forEach(function (element, index) {
                        if (element)
                            element.addEventListener('animationend', handleAnimationEnd, { once: true });
                    });
            }
        });
    };

    /*
        
/*
 Method: animation
Description: Functionality for animation.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.animation = function (data) { // deprecated

            if (data.animation != undefined) {
                if (data.animation == 'show') {
                    $(data.object).css({'display':'block', opacity:1});
                } else if (data.animation == 'hide') {
                    $(data.object).css({'display':'none'});
                } else {
                    //if (data.autoDisplay == undefined) {data.autoDisplay = true};
                    //var animationObject = self.cloneObject(animations[data.animation]);
                    var animationObject = jQuery.extend({}, animations[data.animation]);
                    //if (data.autoDisplay)
                    //    if (data.animation == 'fadeIn') $(data.object).css({'display':'block'});
                    if (data.reverse) {animationObject.name += 'Reverse'}
                    if (data.duration !== undefined) {animationObject.duration = data.duration}

                    animationObject.complete = function () {
                        if (data.autoDisplay) {
                            if ((data.animation == 'fadeOut') || (data.animation == 'hide')) {
                                $('data.object').css({'display':'none'});
                            }
                        }
                        if (data.onComplete !== undefined) {
                            self.do(data.onComplete);
                        }
                    }
                    self.log('animationObject.name');
                    self.log(animationObject.name);
                    self.log('animationObject');
                    self.log(animationObject);

                    // playKeyframe va aggiornato con
                    // animate({from, to, ease, type, stiffness, dumping, mass, velocity, duration})
                    // https://popmotion.io/#quick-start-animation-animate-keyframes-options
                    //var obj = popmotion.styler($(data.object));
                    //popmotion.animate(animationObject).start(obj.set); // $(data.object).css

                    $(data.object).playKeyframe(animationObject);
                }
            } else {
                $(data.object).resetKeyframe();
                $(data.object).css({'opacity':1,'display':'block'});
                self.log('ANIMATION UNDEFINED');
                self.log(data);
            }
        }

        
/*
 Method: move
Description: Functionality for move.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.move = function (data) { // deprecated
            var targetPos = {x:0,y:0};
            if (data.target != 'center') { targetPos = {x:384,y:384}; }
            else if (data.target !== undefined) {targetPos = self.getTransform(data.target);}
            else {self.log(data);}
            if (data.move == 'toTarget') {
                    TweenMax.to(data.object, self.TweenMaxDuration(data.duration), { x: targetPos.x, y: targetPos.y});
            } else if (data.move == 'fromTarget') {
                TweenMax.from(this, self.TweenMaxDuration(data.duration), { x: targetPos.x, y: targetPos.y});
            }
            if (data.animation !== undefined) {
                self.animation({object:data.object, animation:data.animation});
            }
        }

        
/*
 Method: TweenMaxDuration
Description: Functionality for TweenMaxDuration.
@param {any} duration - Description for duration
@returns {any} - Result of the operation
 */
this.TweenMaxDuration = function (duration) {
            if (duration != undefined) {return Number(duration)/1000} else {return 0.667;}
        }
        */
    // CSS CLASSES
    
/*
 Method: addClassRule
Description: Functionality for addClassRule.
@param {any} ruleName - Description for ruleName
@param {any} ruleParams - Description for ruleParams
@returns {any} - Result of the operation
 */
this.addClassRule = function (ruleName, ruleParams) {
        //self.log('addClassRule');
        var ruleProperties = '{';
        for (var property in ruleParams) {
            if (typeof ruleParams[property] == 'object') {
                self.addClassRule(ruleName + ' ' + property, ruleParams[property]);
            } else {
                ruleProperties += property + ':' + ruleParams[property] + ';';
            }
        }
        ruleProperties += '}';
        self.styleObj.sheet.insertRule(ruleName + ' ' + ruleProperties);
    };

    
/*
 Method: addApplyRule
Description: Functionality for addApplyRule.
@param {any} ruleName - Description for ruleName
@param {any} ruleParams - Description for ruleParams
@returns {any} - Result of the operation
 */
this.addApplyRule = function (ruleName, ruleParams) {
        // Use @apply to inline any existing utility classes into your own custom CSS
        // https://tailwindcss.com/docs/functions-and-directives#apply
        self.styleObj.sheet.insertRule(ruleName + ' {@apply ' + ruleParams + '}');
    };

    
/*
 Method: findPlugin
Description: Functionality for findPlugin.
@param {any} name - Description for name
@returns {any} - Result of the operation
 */
this.findPlugin = function (name) {
        if (self.json.plugins) {
            for (var plugin of self.json.plugins) {
                if (plugin.name == name) return plugin;
            }
        } else {
            return false;
        }
    };

    
/*
 Method: defineCss
Description: Functionality for defineCss.
@param {any} cssObj - Description for cssObj
@returns {any} - Result of the operation
 */
this.defineCss = function (cssObj) {
        self.log('defineCss');

        self.styleObj = document.createElement("style");

        document.head.appendChild(self.styleObj);
        self.styleObj.appendChild(document.createTextNode(""));

        /*  self.styleObj.sheet.insertRule('@tailwind base;');
            self.styleObj.sheet.insertRule('@tailwind components;');
            self.styleObj.sheet.insertRule('@tailwind utilities;'); */
        // shortcuts (must be improved / can't be extended)
        /* if (self.json.shortcuts && self.json.shortcuts.class)
            shortcuts = self.json.shortcuts.class; */
        if (cssObj) {
            if (Array.isArray(cssObj)) {
                for (var cssElement of cssObj) {
                    //console.log(cssElement);
                    for (var elementName in cssElement) {
                        //console.log(elementName);
                        let valueWithData = self.replaceProperties(cssElement[elementName]);
                        self.addClassRule(elementName, valueWithData);
                    }
                }
            } else {
                for (var elementName in cssObj) {
                    let valueWithData = self.replaceProperties(cssObj[elementName]);
                    self.addClassRule(elementName, valueWithData);
                }
            }
        }

        if (self.pluginsLoaded.tailwindcss) {
            var plugin = self.findPlugin('tailwindcss');
            //var config = (plugin) ? plugin.config : {preflight: false};
            var config = (self.json.setup.config.tailwindcss) ? self.json.setup.config.tailwindcss : { preflight: false };
            //if (self.json.style.theme) setup.theme = self.json.style.theme;
            //loadPlugin(undefined, 'tailwind-config', undefined, undefined, JSON.stringify(config)) // version 2.2.0
            if (tailwind)
                tailwind.config = config;

            else
                console.log('tailwind object is undefined');
        }

        /*         // OBSOLETE
                if (self.pluginsLoaded.twind && window.twind) {
                    var plugin = self.findPlugin('twind');
                    var config = (plugin) ? plugin.config : {preflight: false, mode:'silent'};
                    //if (self.json.setup.target) setup.target = self.query(self.json.setup.target);
                    //if (self.json.style.theme) setup.theme = self.json.style.theme;

                    window.twind.setup(config);

                } */
        /* if (self.pluginsLoaded.windicss && window.windicssRuntimeOptions) {

            var config = self.json.style.windi || {
                // enabled preflight
                preflight: false,
                // scan the entire dom tree to infer the classnames on page loaded
                extractInitial: false,
                // generate mock classes for browser to do the auto-completeion
                mockClasses: false,
                // the windi config you are used to put in `windi.setup.js`
                setup: {},
                theme: {extend: {}}
            };
            if (self.json.style.theme) setup.theme.extend = self.json.style.theme;
            window.windicssRuntimeOptions = config;
        } */
        /* if (self.pluginsLoaded.unocss) {

            var config = self.json.style.unocss || {  // -> self.json.plugins[].setup
                rules: [
                    // custom rules...
                ],
                presets: [
                    // custom presets...
                ],
                // ...
                };
                if (self.json.style.theme) setup.theme = self.json.style.theme;
                if (self.json.shortcuts) setup.shortcuts = self.json.shortcuts;

                window.__unocss = config;
                //alert(JSON.stringify(config));
        } */
        //self.do(data);
    };

    /*    
/*
 Method: createLoader
Description: Functionality for createLoader.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.createLoader = function (data) {
            // !--<div class="preloader-wrapper"><div class="preloader"><img alt="Loading..."/></div></div>-->

            if (self.json.loader) $('.preloader img').attr('src', self.json.loader);
            self.do(data);
        } */
    /*     
/*
 Method: preloadIcons
Description: Functionality for preloadIcons.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.preloadIcons = function (data) {
            self.log('preloadIcons');
            // to do: check for duplicated ids

            if (self.json.icons) {
                if (!data.icons && self.json.icons) data.icons = self.cloneObject(self.json.icons);
                if (data.icons) {
                    var iconSet = Object.keys(data.icons)[0];
                    var icons = data.icons[iconSet];
                    delete data.icons[iconSet];
                    if (icons.preload) {

                        var ajax = new XMLHttpRequest();
                        ajax.open("GET", icons.src, true);
                        ajax.onload = function(e) {

                            if (this.status >= 200 && this.status < 400) {

                                self.log('Icon set loaded ' + iconSet);

                                var result = this.response; // responseText
                                //self.log(result);
                                var div = document.createElement("div");
                                //var iconsHtml = new XMLSerializer().serializeToString(result);
                                var iconsHtml = result;
                                iconsHtml = self.replaceAll(iconsHtml, 'id="', 'id="' + iconSet + '_');
                                div.innerHTML = iconsHtml;
                                div.setAttribute("style", "display: none");
                                //div.setAttribute("aria-hidden", "true");
                                //document.getElementbyId('preloadIcons').appendChild(element);
                                document.body.insertBefore(div, null);
                                if (Object.keys(data.icons).length > 0)
                                    self.preloadIcons(data);
                                else
                                    self.do(data);
                            }
                        }
                        ajax.send();

                    } else {
                        if (Object.keys(data.icons).length > 0)
                                self.preloadIcons(data);
                            else
                                self.do(data);
                    }
                } else
                    self.do(data);
            } else {
                self.do(data);
            }
        } */
    /* 
/*
 Method: initApp
Description: Functionality for initApp.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.initApp = function (data) {
        self.log('initApp');

        self.defineCss();

        window.addEventListener('resize', self.resizeEvent);

        if (self.json) { // can be self.json.init
            //if (self.json.on.init)
            if (self.json.on)
                self.on(self.json.on);

            //else
            //    self.log('no on init functions');
        }
        //self.do(data);
    }
    */
    /* 
/*
 Method: createUI
Description: Functionality for createUI.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.createUI = function (data) {
        jsonic.do({
            tasks:[
                {task: self.firebaseInit},
                {
                    task: self.firebaseVerifyUser,
                    params: {
                        code: self.params.oobCode
                    }
                },
                {task: self.getDevices, if: jsonic.userIn},
                {task: self.defineCss},
                {task: self.defineAnimations},   // animazioni css predefinite
                {task: self.createHeader},
                {task: self.createMenu, params: {
                    container: '#menu',
                    id: data.params.mode || 'guest'
                }},
                {task: jsonic.createPages}
            ]
        });
    } */
    /* 
/*
 Method: openPageFromUrl
Description: Functionality for openPageFromUrl.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.openPageFromUrl = function(data){
        jsonic.log('openPageFromUrl');
        jsonic.log('self.params');
        jsonic.log(self.params);

        var pageId = '';

        //if (localStorage.getItem('scanning')) {
        //    self.hashopenTask({id:localStorage.getItem('scanningFile')});
        //} else {

        //self.disableFileIcons();

        //var noDevices = (jsonic.userIn() && (!var.db.devices || Object.keys(var.db.devices).length == 0));
        //if (noDevices && !self.params.d && !self.params.m) {
        //    jsonic.gotoPage('devices');
        //} else {

        if (window.location.hash && window.location.hash !== '') {
            //pageId = window.location.hash.substr(1);
            self.hash();
        } else {
            if (self.params.p) // retro-compatibilità
                pageId = self.params.p;
            else
                pageId = self.homePage();
                self.gotoPage(pageId);
        }

        //}
            self.do(data);
    }
*/
    /*
    // Load a script from given `url`
    
/*
 Method: loadPlugin
Description: Functionality for loadPlugin.
@param {any} url - Description for url
@returns {any} - Result of the operation
 */
var loadPlugin = function(url) {
        return new Promise(function(resolve, reject) {
            const script = document.createElement('script');
            script.src = url;

            script.addEventListener('load', function() {
                // The script is loaded completely
                resolve(true);
            });

            document.head.appendChild(script);
        });
    };

    // Perform all promises in the order
    
/*
 Method: waterfall
Description: Functionality for waterfall.
@param {any} promises - Description for promises
@returns {any} - Result of the operation
 */
var waterfall = function(promises) {
        return promises.reduce(
            function(p, c) {
                // Waiting for `p` completed
                return p.then(function() {
                    // and then `c`
                    return c().then(function(result) {
                        return true;
                    });
                });
            },
            // The initial value passed to the reduce method
            Promise.resolve([])
        );
    };

    // Load an array of scripts in order
    
/*
 Method: loadScriptsInOrder
Description: Functionality for loadScriptsInOrder.
@param {any} arrayOfJs - Description for arrayOfJs
@returns {any} - Result of the operation
 */
var loadScriptsInOrder = function(arrayOfJs) {
        self.log('loadScriptsInOrder');
        self.log(arrayOfJs);
        const promises = arrayOfJs.map(function(url) {
            self.log(url);
            return loadScript(url);
        });
        return waterfall(promises);
    };
    */
    
/*
 Method: hash
Description: Functionality for hash.
@param {any} path - Description for path
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.hash = function (path, args) {
        self.log('hash');
        var path = self.replaceProperties(path, args);
        self.log(path);
        window.location.hash = path;
    };

    
/*
 Method: page
Description: Functionality for page.
@param {any} path - Description for path
@param {any} args - Description for args
@returns {any} - Result of the operation
 */
this.page = function (path, args) {
        self.log('page');
        var path = self.replaceProperties(path, args);
        self.log(path);
        if (path.startsWith('#'))
            window.location.hash = path;

        else
            window.location.href = path;
        //window.location.hash = path;
    };

    
/*
 Method: hashChangeEvent
Description: Functionality for hashChangeEvent.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.hashChangeEvent = function () {
        self.log('hashChangeEvent');
        self.log(window.location.hash);

        /* if (window.location.hash && window.location.hash !== '') {
            var hashParams = window.location.hash.substr(1).split('/');
            self.gotoPage(hashParams[0], hashParams[1]);
        } else {
            //pageId = self.homePage();
            if (self.json.setup && self.json.setup.wordpress && self.json.setup.wordpress.slug)
                self.gotoPage(self.json.setup.wordpress.slug);
            else
                self.gotoPage(self.homePage());
        }

        if (json && json.app && json.app.on && json.app.on.page) { // page event
            self.do(json.app.on.page);
        }

        if (json && json.actions && json.actions.init) { // obsolete
            self.do(json.actions.init);
        } */
    };

    /*     
/*
 Method: addPageListeners
Description: Functionality for addPageListeners.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.addPageListeners = function (data) {
            self.log('addPageListeners');

            // Resize event
            window.addEventListener('resize', self.resizeEvent);

            // Location hash (url) event
            window.addEventListener('hashchange', self.hashChangeEvent);
            //$(window).on('hashchange', self.hashChangeEvent);

            self.hashChangeEvent();

            self.do(data);

        } */
    /* // testing
    
/*
 Method: findValues
Description: Functionality for findValues.
@param {any} obj - Description for obj
@param {any} key - Description for key
@param {any} found - Description for found
@returns {any} - Result of the operation
 */
let findValues = function(obj, key, found) {
        for (let localKey in obj) {
            if (obj.hasOwnProperty(localKey)) {
            let val = obj[localKey];
            //self.log(localKey)
            if (localKey === key) {
                found.add(val)
            } else {
                if (typeof val === 'object') {
                findValues(val, key, found)
                }
            }
            }
        }
    }
    
/*
 Method: uniqueValue
Description: Functionality for uniqueValue.
@param {any} obj - Description for obj
@param {any} key - Description for key
@param {any} value - Description for value
@returns {any} - Result of the operation
 */
function uniqueValue(obj, key, value) {
        let found = new Set()
        findValues(object, key, found)
        return found.size === 1 && found.has(value);
    } */
    
/*
 Method: lookup
Description: Functionality for lookup.
@param {any} obj - Description for obj
@param {any} k - Description for k
@returns {any} - Result of the operation
 */
this.lookup = function (obj, k) {
        for (var key in obj) {
            var value = obj[key];

            if (k == key) {
                return [k, value];
            }

            if (typeof (value) === "object" && !Array.isArray(value)) {
                var y = lookup(value, k);
                if (y && y[0] == k) return y;
            }
            if (Array.isArray(value)) {
                // for..in doesn't work the way you want on arrays in some browsers
                //
                for (var i = 0; i < value.length; ++i) {
                    var x = lookup(value[i], k);
                    if (x && x[0] == k) return x;
                }
            }
        }

        return null;
    };

    
/*
 Method: findValues
Description: Functionality for findValues.
@param {any} obj - Description for obj
@param {any} key - Description for key
@returns {any} - Result of the operation
 */
this.findValues = function (obj, key) {
        //return Boolean(JSON.stringify(obj).indexOf('"' + string + '":') >= 0)
        let matrix = [...JSON.stringify(obj).matchAll(new RegExp('\"?' + key + '\"?\:\s*\"([^,\"]+)', 'gi'))];
        var results = [];
        for (var element of matrix)
            results.push(element[1]);
        return results;
        //return Boolean(JSON.stringify(obj).match(new RegExp('\"?' + key + '\"?\:\s+\"' + value + '\"'))) // JSON5 compatible
    };

    // testing
    
/*
 Method: findKey
Description: Functionality for findKey.
@param {any} obj - Description for obj
@param {any} key - Description for key
@returns {any} - Result of the operation
 */
function findKey(obj, key) {
        for ([k, v] of Object.entries(obj)) {
            if (k == key) return v;
            if (typeof v === 'object' && v !== null) {
                let found = findKey(v, key);
                if (found) return found;
            }
        }
    }

    // lookup in json
    
/*
 Method: keyInJson
Description: Functionality for keyInJson.
@param {any} obj - Description for obj
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
this.keyInJson = function (obj, string) {
        //return Boolean(JSON.stringify(obj).indexOf('"' + string + '":') >= 0)
        return Boolean(JSON.stringify(obj).match(new RegExp('\"?' + string + '\"?\:'))); // JSON5 compatible
    };

    // lookup in json
    
/*
 Method: valueInJson
Description: Functionality for valueInJson.
@param {any} obj - Description for obj
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
this.valueInJson = function (obj, string) {
        //return Boolean(JSON.stringify(obj).indexOf('"' + string + '":') >= 0)
        return Boolean(JSON.stringify(obj).match(new RegExp('\"?' + string + '\"?'))); // JSON5 compatible
    };

    /* 
/*
 Method: keyInFiles
Description: Functionality for keyInFiles.
@param {any} string - Description for string
@returns {any} - Result of the operation
 */
this.keyInFiles = function (string) {
        var found = self.keyInJson(self.options, string); // in main app options: jsonic = jsonicApp(options)
        for (fileName in self.modules) {
            if (fileName !== 'resources' && self.keyInJson(self.modules[fileName], string))
                found = true;
        }
        return found;
    } */
    /*     
/*
 Method: openPageFromUrl
Description: Functionality for openPageFromUrl.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.openPageFromUrl = function (data) {
            self.log('openPageFromUrl');
            self.hashChangeEvent();
            self.do(data);
        }; */
    /*     
/*
 Method: addEventListeners
Description: Functionality for addEventListeners.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.addEventListeners = function (data) {
            self.log('addEventListeners');

            // Resize event
            window.addEventListener('resize', self.resizeEvent);

            // Location hash (url) event
            window.addEventListener('hashchange', self.hashChangeEvent);
            $(window).on('hashchange', self.hashChangeEvent);

            self.hashChangeEvent(); // add here on.hashchange

            // Fullscreen event
            if (screenfull && screenfull.isEnabled) {
                screenfull.on('change', () => {
                    self.log('Am I fullscreen?', screenfull.isFullscreen ? 'Yes' : 'No');
                    if (screenfull.isFullscreen) {
                        var.fullscreen = true;
                        if (var.onFullScreenOpen) self.do(var.onFullScreenOpen);
                    } else {
                        var.fullscreen = false;
                        if (var.onFullScreenClose) self.do(var.onFullScreenClose);
                    }
                });
            }

            if (json && json.app && json.app.on) {

                if (json.app.on.thunkable && json.app.on.thunkable.receiveMessage)
                    window.receiveMessage(function (message) {
                        //document.querySelector('#message').value = message;
                        jsonic.functions(json.app.on.thunkable.receiveMessage, message, undefined);
                    });

                var container = json.app.container || 'body';
                self.on(json.app.on, {container: container});
                self.do(data);
            } else {
                self.do(data);
            }
        } */
    /* 
/*
 Method: loadIconset
Description: Functionality for loadIconset.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.loadIconset = function (data) {
        self.log('loadIconset');

        if (self.json.iconset) {
            if (!self.pluginsLoaded['iconify']) {
                let plugin = self.json.resources.pluginsFunctions['iconify'];
                self.pluginsLoader([plugin], self.loadIconset, [data]);
            } else {
                var iconset;

                if (!self.json.iconset[0])
                    iconset = [self.json.iconset];
                else
                    iconset = self.json.iconset;

                for (var index in iconset) {

                    if (Iconify.addCollection(iconset[index]))
                        self.log('added iconset '+ iconset[index].prefix);
                    else
                        self.log('can\t add iconset '+ iconset[index].prefix);
                    //alert(x);
                }
                self.do(data);

            }

        } else {
            self.do(data);
        }

    } */
    // Si potrebbe creare una nuova istanza di jsonicApp
    
/*
 Method: start
Description: Functionality for start.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.start = function () {

        self.log('start');

        //if (params.onFullScreenOpen) var.onFullScreenOpen = params.onFullScreenOpen;
        //if (params.onFullScreenClose) var.onFullScreenClose = params.onFullScreenClose;
        // PWA
        /* if ('serviceWorker' in navigator) {
            // Register a service worker hosted at the root of the
            // site using the default scope.
            navigator.serviceWorker.register('/app/assets/pwa/sw.js').then(function(registration) {
                self.log('Service worker registration succeeded:', registration);
            }, function(error) {
                self.log('Service worker registration failed:', error);
            });
        } else {
            self.log('Service workers are not supported.');
        } */
        let deferredPrompt;

        window.addEventListener('beforeinstallprompt', (e) => {
            // Prevent Chrome 67 and earlier from automatically showing the prompt
            e.preventDefault();
            // Stash the event so it can be triggered later.
            deferredPrompt = e;
        });

        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
            self.json.setup.darkMode = e.matches ? "dark" : false;
        });

        // DEFINE ICONSET (iconify)
        if (self.json.iconset) {
            if (!self.pluginsLoaded['iconify']) {
                let plugin = self.json.resources.pluginsFunctions['iconify'];
                self.pluginsLoader([plugin], self.start);
                return false;
            } else {
                var iconset;

                if (!self.json.iconset[0])
                    iconset = [self.json.iconset];

                else
                    iconset = self.json.iconset;

                for (var index in iconset) {

                    if (Iconify.addCollection(iconset[index]))
                        self.log('added iconset ' + iconset[index].prefix);

                    else
                        self.log('can\t add iconset ' + iconset[index].prefix);
                    //alert(x);
                }

            }

        }

        // DEFINE CSS
        //self.defineCss(); // TO DO: only additional css (do it on load module success)
        // ADD RESIZE LISTENER (for dynamic styles) TO DO: check it
        window.addEventListener('resize', self.resizeEvent);

        // PAGE
        if (!self.json.setup.page) self.json.setup.page = {};
        self.json.setup.page.hash = window.location.hash.substr(1);

        // DO / ON
        /* if (self.json) {
            if (self.json.do)
                self.do(self.json.do, undefined, self.json.container);

            if (self.json.on)
                self.on(self.json.on, self.json.container);
        } */
        var selector = self.json.selector || self.json.container;

        /* if (self.json.do)
            self.do(self.json.do, undefined, selector); */
        // should go action/run
        if (self.json.on)
            self.on(self.json.on, selector);

    };

    
/*
 Method: extendJson
Description: Functionality for extendJson.
@param {any} jsonBase - Description for jsonBase
@param {any} jsonExtension - Description for jsonExtension
@returns {any} - Result of the operation
 */
this.extendJson = function (jsonBase, jsonExtension) {
        // TO DO: should be renamed in extendApp (or executeModule) because extend self.json and add CSS style
        if (jsonExtension.css) self.defineCss(jsonExtension.css);
        if (jsonExtension.functions) self.extendFunctions(jsonExtension.functions);

        for (var key in jsonExtension) {
            // AGGIUNGERE ALL'ARRAY DELLE AZIONI ON.INIT...
            if (key == 'on') {
                for (var subKey in jsonExtension[key]) {
                    if (typeof jsonExtension[key][subKey] !== 'string') {
                        //jsonBase[key][subKey] = self.extend({}, jsonBase[key][subKey], jsonExtension[key][subKey]);
                        if (!jsonBase[key][subKey]) jsonBase[key][subKey] = [];
                        jsonBase[key][subKey].push(jsonExtension[key][subKey]);
                    } else {
                        jsonBase[key][subKey] = jsonExtension[key][subKey];
                    }
                }
            } else if (key == 'parts' || key == 'shortcuts') { // combine subelements
                for (var subKey in jsonExtension[key]) {
                    if (typeof jsonExtension[key][subKey] !== 'string')
                        jsonBase[key][subKey] = self.extend({}, jsonBase[key][subKey], jsonExtension[key][subKey]);

                    else
                        jsonBase[key][subKey] = jsonExtension[key][subKey];
                }
                /* } else if (key == 'css') {
                    for (var subKey in jsonExtension[key]) {
                        if (typeof jsonExtension[key][subKey] !== 'string')
                            jsonBase[key][subKey] = self.extend({}, jsonBase[key][subKey], jsonExtension[key][subKey]);
                        else
                            jsonBase[key][subKey] = jsonExtension[key][subKey];
                    } */
                /* } else if (key == 'app') { // to do: obsolete
                    for (var subKey in jsonExtension[key]) {
                        if (typeof jsonExtension[key][subKey] !== 'string')
                            jsonBase[key][subKey] = self.extend({}, jsonBase[key][subKey], jsonExtension[key][subKey]);
                        else
                            jsonBase[key][subKey] = jsonExtension[key][subKey];
                    } */
            } else if (key == 'iconset' || key == 'plugins') { // combine arrays
                if (!jsonBase[key]) jsonBase[key] = [];
                jsonBase[key] = jsonBase[key].concat(jsonExtension[key]);
            } else {
                if (key == 'css') {
                } else {
                    jsonBase[key] = self.extend({}, jsonBase[key], jsonExtension[key]);
                }
            }
        }
        return jsonBase;
    };

    
/*
 Method: loadModuleFromDb
Description: Functionality for loadModuleFromDb.
@param {any} data - Description for data
@returns {any} - Result of the operation
 */
this.loadModuleFromDb = function (data) {
        self.log('loadModuleFromDb');
        self.firebaseGet('jsonic', function (result) {
            if (result.success) {
                self.log('firebase get SUCCESS');
                for (codeKey in result.data)
                    if (result.data[codeKey].code)
                        self.extendJson(json, result.data[codeKey].code);
                //self.log('json');
                //self.log(json);
                //if (params.success) self.do(params.success,result.data);
            } else {
                self.log('firebase get ERROR');
                //if (params.error) self.do(params.error, result.error);
                // alert errore di connessione. alert riprova
            }
            self.do(data);
            //self.do(data);
        });
    };

    this.modules = {};
    this.modulesLoading = {};

    
/*
 Method: fileType
Description: Functionality for fileType.
@param {any} fileName - Description for fileName
@returns {any} - Result of the operation
 */
this.fileType = function (fileName) {
        return /(?:\.([^.]+))?$/.exec(fileName)[1];
    };

    
/*
 Method: jsoncToJson
Description: Functionality for jsoncToJson.
@param {any} jsoncText - Description for jsoncText
@returns {any} - Result of the operation
 */
this.jsoncToJson = function (jsoncText) {
        return jsoncText.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1').replace(/\r/, "\n").replace(/\n[\n]+/, "\n");
        // https://stackoverflow.com/questions/244777/can-comments-be-used-in-json/24545329
    };
    //var json = jsoncToJson('https://fantacards.com/app/data.jsonc');
    //console.log(json);
    
/*
 Method: loadModule
Description: Functionality for loadModule.
@param {any} mod - Description for mod
@returns {any} - Result of the operation
 */
this.loadModule = function (mod) {
        /* self.log('loadModule');
        self.log(mod); */
        // params: {files, onComplete}
        // self.cloneObject(params.json) to avoid to remove from initial params
        //if (typeof setup.modules == 'string') setup.modules = [setup.modules];
        if (!self.modulesLoading[mod.name] && !self.modules[mod.name]) {
            self.modulesLoading[mod.name] = true;

            return new Promise(function (resolve, reject) {
                //mod = self.replaceProperties(mod);
                //self.log(mod);

                const getAPIData = new XMLHttpRequest();
                getAPIData.open("GET", mod.url);
                getAPIData.onload = function () {
                    self.modulesLoading[mod.name] = false;
                    //console.log(getAPIData);
                    let responseText = getAPIData.responseText;
                    if (self.fileType(getAPIData.responseURL) == 'jsonc')
                        responseText = self.jsoncToJson(responseText);
                    // try
                    const result = JSON.parse(responseText);
                    // error self.log('Wrong JSON in module '+ mod.name);
                    if (self.isJson(result)) {
                        self.modules[mod.name] = result;
                        self.extendJson(self.json, result);
                        self.log('Loaded module ' + mod.name, 'grey');
                    } else {
                        self.log('Wrong JSON in module ' + mod.name, 'red');
                    }
                    resolve();
                };
                getAPIData.onerror = function () {
                    self.modulesLoading[mod.name] = false;
                };
                getAPIData.send();
            });

        } else {
            self.log('Module ' + mod.name + ' already loaded', 'orange');
        }

    };

    /*   
/*
 Method: loadPlugin
Description: Functionality for loadPlugin.
@param {any} url - Description for url
@returns {any} - Result of the operation
 */
var loadPlugin = function (url) {
            //self.log('loading ' + url);

            return new Promise(function(resolve, reject) {
                var type = 'script';
                if (url.endsWith('.css')) type = 'link';

                let obj = document.createElement(type);

                // obj.src = url;
                obj.async = false;
                obj.onload = function() {
                    resolve(url);
                };
                obj.onerror = function() {
                    self.log('error loading: '+url);
                    reject(url);
                };

                if (type === 'script') {
                    if (url) obj.src = url;
                    //if (code) obj.text = code;
                    obj.location = document.body;
                    document.body.appendChild(obj);
                } else {
                    obj.href = url;
                    obj.rel = 'stylesheet';
                    obj.type  = 'text/css';
                    obj.location = document.head;
                    document.head.appendChild(obj);
                }

                //self.log('obj');
                //self.log(obj);
            });
        }  */
    
/*
 Method: loadPlugin
Description: Functionality for loadPlugin.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
var loadPlugin = function (params) {
        //self.log('loading ' + url);

        return new Promise(function (resolve, reject) {
            var tag = 'script', type = 'text/javascript';
            if (params.type == 'script') { tag = 'script'; type = 'text/javascript'; }
            else if (params.type == 'module') { tag = 'script'; type = 'module'; }
            else if (params.type == 'tailwind-config') { tag = 'script'; type = 'tailwind-config'; }
            else if (params.type == 'link') { tag = 'link'; type = 'text/css'; }
            else if (params.type == 'font') { tag = 'link'; type = undefined; }
            //if (!tag)
            //    tag = (url.endsWith('.js')) ? 'script' : 'link';
            let obj = document.createElement(tag);
            if (params.content)
                obj.appendChild(document.createTextNode(params.content));

            obj.async = false;
            obj.onload = function () {
                self.pluginsLoaded[params.name] = { version: "1.0.0" };
                self.log('Loaded plugin ' + params.name, 'grey');
                resolve(params.url);
            };
            obj.onerror = function () {
                self.log('Error loading plugin ' + params.url, 'red');
                reject(params.url);
            };

            if (tag === 'script') {
                if (params.url) obj.src = params.url;
                //if (code) obj.text = code;
                obj.location = document.body;
                obj.type = type || 'text/javascript';
                /* if (type == 'tailwind-config') {
                    self.log('obj');
                    self.log(obj);
                } */
                document.body.appendChild(obj);
            } else if (tag === 'link') {
                obj.rel = params.rel || 'stylesheet';
                obj.location = document.head;
                if (params.url) obj.href = params.url;
                if (type) obj.type = type;
                if (params.as) obj.as = params.as;
                document.head.appendChild(obj);
            }
        });
    };

    /*     
/*
 Method: loadPlugin
Description: Functionality for loadPlugin.
@param {any} url - Description for url
@param {any} tag - Description for tag
@param {any} rel - Description for rel
@param {any} as - Description for as
@returns {any} - Result of the operation
 */
var loadPlugin = function (url, tag, rel, as) {
            //self.log('loading ' + url);

            //alert(url);

            return new Promise(function(resolve, reject) {
                if (tag == 'js') tag = 'script';
                if (tag == 'css') tag = 'link';
                if (!tag)
                    tag = (url.endsWith('.js')) ? 'script' : 'link';

                let obj = document.createElement(tag);

                obj.async = false;
                obj.onload = function() {
                    self.log('loaded plugin: '+url);
                    resolve(url);
                };
                obj.onerror = function() {
                    self.log('error loading: '+url);
                    reject(url);
                };

                if (tag === 'script') {
                    if (url) obj.src = url;
                    //if (code) obj.text = code;
                    obj.location = document.body;
                    document.body.appendChild(obj);
                } else {
                    obj.href = url;
                    obj.rel = rel || 'stylesheet';
                    if (obj.rel == 'stylesheet')
                        obj.type = 'text/css';
                    if (as)
                        obj.as = as;
                    obj.location = document.head;
                    document.head.appendChild(obj);
                }
            });
        }
    */
    this.uiLoaded = {};
    this.pluginsLoaded = {};

    //The Firebase Realtime Database lets you store and query user data, and makes it available between users in realtime
    
/*
 Method: findUrl
Description: Functionality for findUrl.
@param {any} array - Description for array
@param {any} url - Description for url
@returns {any} - Result of the operation
 */
this.findUrl = function (array, url) {
        for (var item of array) {
            if (item.url == url)
                return true;
        }
    };

    /*     
/*
 Method: export
Description: Functionality for export.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.export = function (params) {
            // {name, data}
            if (params && params.name && params.data) {
                let element = document.createElement('a');
                element.style.display = 'none';
                element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(params.data)));
                element.setAttribute('download', params.name);
                document.body.appendChild(element);
                element.click();
                document.body.removeChild(element);
            } else {
                self.log('"export" requires name and data attributes');
            }
        } */
    /*     
/*
 Method: exportData
Description: Functionality for exportData.
@param {any} path - Description for path
@returns {any} - Result of the operation
 */
this.exportData = function (path) {
            // data
            const fileName = self.replaceAll(path, ' ', '.') + '.json';
            const jsonData = self.element({path: path, root: params.root});
            self.export({name: fileName, data: jsonData});
        }
            */
    
/*
 Method: loadPlugins
Description: Functionality for loadPlugins.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.loadPlugins = function () {
        //self.log('loadPlugins');
        //var setup = json.setup;
        // TEMPORAL API
        // https://cdn.jsdelivr.net/npm/@cogitatio/tc39-temporal@0.0.12-alpha.2/index.js
        // https://cdn.jsdelivr.net/npm/cogitatio-tc39-temporal@1.0.2/index.js
        /* <!-- <script type="module">
        import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.0.2/firebase-app.js'

        // If you enabled Analytics in your project, add the Firebase SDK for Google Analytics
        import { analytics } from 'https://www.gstatic.com/firebasejs/9.0.2/firebase-analytics.js'

        // Add Firebase products that you want to use
        import { auth } from 'https://www.gstatic.com/firebasejs/9.0.2/firebase-auth.js'
        import { firestore } from 'https://www.gstatic.com/firebasejs/9.0.2/firebase-firestore.js'
        </script> --> */
        var filesToLoad = [];

        // plugins
        if (self.json.plugins) {
            for (var index in self.json.plugins) {

                var plugin = self.json.plugins[index];

                //self.log('loading ' + plugin.name + '...');
                var version = plugin.version || '';

                if (!plugin.ondemand) { // V. >= 1.0.2

                    if (plugin.url) { // directly loaded
                        // plugin.name and version are needed?
                        if (!self.findUrl(filesToLoad, plugin.url)) { // not included yet
                            filesToLoad.push({ name: plugin.name, url: plugin.url, type: plugin.type });
                        }

                    } else { // from resources (TO DO: remove)

                        var pluginsFiles = plugin.files || self.json.resources.pluginsFiles[plugin.name];

                        // moved in loadPlugin =   ... onload
                        /* self.pluginsLoaded[plugin.name] = {
                            version: version
                        }; */
                        if (!Array.isArray(pluginsFiles))
                            pluginsFiles = [pluginsFiles];

                        for (item of pluginsFiles) {
                            var url = self.replaceAll(item.url, '{version}', version);
                            if (plugin.params) url += '?' + plugin.params;
                            let pluginName = plugin.name || item.name;
                            if (!url || !self.findUrl(filesToLoad, url)) // not included yet
                                filesToLoad.push({ name: pluginName, url: url, type: item.type, content: item.content });
                        }

                    }
                }
            }
        }

        // ui
        /* for (var index in config.ui) {

            var configUI = config.ui[index];

            //self.log('loading ' + configUI.name + '...');

            var version = configUI.version || '';
            var uiFiles = self.json.resources.uiFiles[configUI.name];

            if (!Array.isArray(uiFiles))
                uiFiles = [uiFiles];

            for (item of uiFiles) {
                var url = self.replaceAll(item.url, '{version}', version);
                if (!self.findUrl(filesToLoad, url)) // not included yet
                    filesToLoad.push({url: url, type: item.type, content: item.content});
            }
        } */
        // Auto load Google Fonts
        // TO DO: comment
        for (fontName of self.findValues(self.json, 'font-family')) {
            if (self.json.resources.googleFonts.indexOf(fontName) >= 0) {
                //alert(fontName);
                fontName = self.replaceAll(fontName, ' ', '+');
                //filesToLoad.push({type: 'font', url:'https://fonts.googleapis.com/css2?family='+fontName+'&display=swap', rel:'preload',  as:'font'});
                filesToLoad.push({ type: 'link', url: 'https://fonts.googleapis.com/css2?family=' + fontName });
                // +'&display=swap' // https://css-tricks.com/almanac/properties/f/font-display/
            }
        }

        // Auto load plugins (firebase only)
        /* if (config.plugins && config.plugins.autoload) {
            self.log('plugins autoload');
            //for (var index in self.json.resources.pluginsFunctions) {
            var pluginFunctions =  self.json.resources.pluginsFirebase; // pluginFunctions
            for (var index in self.json.resources.pluginsFirebase) {
                //self.log(index + ':' + self.keyInFiles(index));
                if (self.keyInFiles(index)) {
                    if (pluginFunctions[index]) {

                        var plugins = [];
                        if (Array.isArray(pluginFunctions[index]))
                            plugins = pluginFunctions[index];
                        else
                            plugins = [pluginFunctions[index]];

                        for (plugin of plugins) {

                            self.log('loading ' + plugin.name + '...');
                            self.pluginsLoaded[plugin.name] = version;

                            var version = plugin.version || '';
                            var pluginsFiles = self.json.resources.pluginsFiles[plugin.name];

                            if (!Array.isArray(pluginsFiles))
                                pluginsFiles = [pluginsFiles];

                            for (item of pluginsFiles) {
                                var url = self.replaceAll(item.url, '{version}', version);
                                if (!self.findUrl(filesToLoad, url)) // not included yet
                                    filesToLoad.push({url: url, type: item.type, content: item.content});
                            }
                        }
                    }
                }
            }
        } */
        // load multiple scripts in sequence
        // https://bit.ly/3nT5Ky1
        // one after another
        // https://medium.com/@asimmittal/sequential-script-loading-in-javascript-a0b77ca9467c
        // load multi js and css
        // https://sphacks.io/load-multiple-js-scripts-dynamically-without-jquery/
        // save all Promises as array
        let promises = [];

        //promises.push(loadPlugin("https: 

        filesToLoad.forEach(function (item) {
            if (item.url)
                promises.push(loadPlugin({ name: item.name, url: item.url, type: item.type, rel: item.rel, as: item.as, content: item.content }));

            else
                loadPlugin({ name: item.name, url: item.url, type: item.type, rel: item.rel, as: item.as, content: item.content });

        });

        Promise.all(promises).then(function () {

            self.start();

        }).catch(function (script) {
            self.log('Failed to load');
            self.log(script);
        });

    };

    
/*
 Method: modulesList
Description: Functionality for modulesList.
@param {any} modulesObj - Description for modulesObj
@returns {any} - Result of the operation
 */
this.modulesList = function (modulesObj) {

        var modulesList = [];
        if (modulesObj) {
            for (key in modulesObj) {
                var mod = self.replaceProperties(modulesObj[key]);
                modulesList = modulesList.concat(mod);
            }
        }
        return self.replaceProperties(modulesObj);
    };

    
/*
 Method: loadModules
Description: Functionality for loadModules.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.loadModules = function () {

        if (self.json.setup.modules) {
            var modules = self.replaceProperties(self.json.setup.modules);  

            var promises = [];
            modules.forEach(function (item) {
                if (!self.modulesLoading[item.name] && !self.modules[item.name]) {
                    promises.push(self.loadModule(item));
                }
            });

            Promise.all(promises).then(function () {

                self.loadPlugins();

            }).catch(function (file) {
                self.log('Failed to load');
                self.log(file);
            });

        } else {
            self.loadPlugins();
        }

    };

    this.moduleExecuted = {};

    
/*
 Method: addModule
Description: Functionality for addModule.
@param {any} params - Description for params
@returns {any} - Result of the operation
 */
this.addModule = function (params) {

        if (!self.modules[params.name]) {

            var promises = [];

            promises.push(self.loadModule(params));

            Promise.all(promises).then(function () {

                if (!self.moduleExecuted[params.name]) {

                    var module = self.modules[params.name];

                    if (module.html) {
                        self.moduleExecuted[params.name] = true;
                        self.html(module.html);

                    }
                }
                if (params.success)
                    self.do(params.success);

            }).catch(function (file) {
                self.log('Failed to load');
                self.log(file);
                if (params.error)
                    self.do(params.error);

            });
        } else {
            if (params.success)
                self.do(params.success);

        }
    };

    
/*
 Method: loadModuleGlobal
Description: Functionality for loadModuleGlobal.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
this.loadModuleGlobal = function () {
        self.log('loadModuleGlobal');

        var promises = [];

        var modules = [];  
        for (let key in self.json.setup.modules) {
            var mod = self.replaceProperties(self.json.setup.modules[key]);
            modules = modules.concat(self.cloneObject(mod));
        }

        for (let mod of modules)
            promises.push(self.loadModule(mod));

        Promise.all(promises).then(function () {

            self.loadModules();

        }).catch(function (file) {
            self.log('Failed to load');
            self.log(file);
        });
    };

    
/*
 Method: code
Description: Functionality for code.
@param {any} params - Description for params
@param {any} selectorParams - Description for selectorParams
@returns {any} - Result of the operation
 */
this.code = function (params, selectorParams) {

        let container = self.selector(params.selector || params.container) || self.selector(selectorParams);

        self.log("this.code");
        self.log("container");
        self.log(container);

        var editorContainer = container.slice(1);

        var editor = ace.edit(editorContainer);

        var value = params.value;  
        if (!params.do || (params.do == 'set')) {

            if (params.theme) editor.setTheme(params.theme);  
            var editorMode = params.mode || "ace/mode/json";
            editor.getSession().setMode(editorMode);

            var editorStyle = self.extend({
                background: "rgba(255,255,255,0)"
            }, params.style);

            var codeOptions = self.extend({
                fontSize: "11pt",
                selectionStyle: "line",
                highlightActiveLine: false,
                wrap: true,
                showLineNumbers: false,
                showGutter: false,
                fixedWidthGutter: false,
                readOnly: true,

            }, params.options);

            editor.setOptions(codeOptions);

            var codeString;
            if (typeof value == 'string')
                if (editorMode == "ace/mode/json")
                    codeString = JSON.stringify(JSON.parse(value), null, '\t');  

                else
                    codeString = value;
            else {
                if (editorMode == "ace/mode/json")
                    codeString = JSON.stringify(value, null, '\t');
                else {
                    var functionObj = self.docElement(value.function);
                    var functionParams = value.params;
                    codeString = functionObj(functionParams);
                }
            }

            var codeRows = codeString.split(/\r\n|\r|\n/).length;

            editor.setValue(codeString, -1);
        } else if (params.do == 'get') {
            return editor.getValue();
        } else {
            self.log('"code" function requires value param');
        }

    };

    
/*
 Method: init
Description: Functionality for init.
@param {any} appString - Description for appString
@param {any} modulesString - Description for modulesString
@returns {any} - Result of the operation
 */
this.init = function (appString, modulesString) {
        console.log('init');

        if (typeof appString == 'string') {

            let modules = [{ "name": "app", "url": appString }];
            let modulesNames = (modulesString) ? modulesString.split(",") : [];
            for (let modName of modulesNames) {
                modules.push({ "name": modName, "url": "jsonic/modules/" + modName + ".json" });
            }

            self.json.setup = { "modules": modules };

        } else {

            if (appString) self.extendJson(self.json, appString);

            if (appString.css) self.defineCss(appString.css);  

        }

        self.loadModuleGlobal();
    };

    
/*
 Method: toJsonNode
Description: Functionality for toJsonNode.
@param {any} xml - Description for xml
@returns {any} - Result of the operation
 */
function toJsonNode(xml) {
        let obj = {};
        if (xml.nodeType == 1) {
            if (xml.attributes.length > 0) {
                obj["attr"] = {};
                for (let j = 0; j < xml.attributes.length; j++) {
                    let attribute = xml.attributes.item(j);
                    obj["attr"][attribute.nodeName] = attribute.nodeValue;
                }
            }
        }
        if (xml.hasChildNodes()) {
            for (let i = 0; i < xml.childNodes.length; i++) {
                let item = xml.childNodes.item(i);
                let nodeName = item.nodeName;
                if (item.nodeType == 3 && nodeName == "#text") {
                    if (/^\s+$/.test(item.nodeValue)) {
                    } else obj.text = item.nodeValue;
                } else {
                    if (typeof obj[nodeName] == "undefined") {
                        obj[nodeName] = toJsonNode(item);
                    } else {
                        if (!Array.isArray(obj[nodeName]))
                            obj[nodeName] = [obj[nodeName]];
                        obj[nodeName].push(toJsonNode(item));
                    }
                }
            }
        }
        return obj;
    }

    
/*
 Method: htmlToJson
Description: Functionality for htmlToJson.
@param {any} text - Description for text
@returns {any} - Result of the operation
 */
this.htmlToJson = function (text) {
        let xmlDoc = new DOMParser().parseFromString(text, "text/xml");
        return JSON.stringify(toJsonNode(xmlDoc), null, "\t");
    };

    
/*
 Method: elementToJson
Description: Functionality for elementToJson.
@param {any} selector - Description for selector
@returns {any} - Result of the operation
 */
this.elementToJson = function (selector) {
        let el = document.querySelector(selector);
        return self.htmlToJson(el.getHTML());
    };

    
/*
 Method: clearSelection
Description: Functionality for clearSelection.
@param {any} - No parameters
@returns {any} - Result of the operation
 */
function clearSelection() {
        if (window.getSelection) { window.getSelection().removeAllRanges(); }
        else if (document.selection) { document.selection.empty(); }
    }

    
/*
 Method: importHtml
Description: Functionality for importHtml.
@param {any} htmlText - Description for htmlText
@returns {any} - Result of the operation
 */
this.importHtml = function (htmlText) {

        if (!htmlText)
            htmlText = prompt("html/xml");
        if (htmlText) {
            let jsonDoc = self.htmlToJson(htmlText);
            let textarea = document.createElement("textarea");
            textarea.value = jsonDoc;
            textarea.style = "position: absolute; z-index: 999999; background: white; width:100vw; height:100vh";

            document.body.appendChild(textarea);

            textarea.addEventListener('click', function () {
                textarea.remove();
                navigator.clipboard.writeText(textarea.value).then(() => console.log('copied!'));
            });

        }
    };

    if (typeof options == 'object') {
    } else if (options)
        self.init(arguments[0], arguments[1]);

}  

var jsonApp = new jsonAppObj();
var js = jsonApp;
var jsonic = jsonApp;

