/*-----------------------------
    JSONIC V. 1.0.13
    jsonic.io  
-------------------------------*/

// CHANGELOG

// 1.0.13 jsonApp removed; [replaceProperties: include event properties]
// 1.0.12 jsonApp; if is array option
// 1.0.11 block blocks -> parts
// 1.0.10 for: {name} work also with {value:name with path} (function replaceValue)
// 1.0.8 for: {name} replaced with {value:name} 
// 1.0.5 set: if array of, the assignments follow the order
// 1.0.4 deprecated: to assign without set (too ambiguous now that the key can be function, window.function, etc) 
// 1.0.3 "x": "2*3" if the value is a string, -> self.js(value)
// 1.0.2 plugins ondemand
// 1.0.1 jsonc support
// 1.0.0 FUNCTIONS
// 0.9.9 RESOURCES: Inside modules (resources will be deprecated) 
// 0.9.3 DEBUG: Rimosso break in docElement
// 0.9.4 NOTE: In this.nested, commented and decommented nested.animate = partObj.animate; nested.if partObj.if, ...
// 0.9.5 CLASS: "div btn": "content" = <div class="btn">content</div>
// 0.9.6 DEBUG: in this.createElement newSelector (newSelector should be a dedicated function separated from selector)
// 0.9.7 REMOVE: method to remove an element from the DOM / Restored html {attr} to set the attributes of an element

// https://minify-js.com Input: 624.1 kB; Output: 95.46 kB; Compression: 84.7%, saving: 528.64 kB;

// README

// PRIORITY

// in attr -> addStyle / removeStyle 
// instead of shortcuts the user can use {data shortcuts}
// {arguments:value|default} -> ({arguments:value}) ? {arguments:value} : default
// use (typeof id == "undefined") to understand if id is defined and is = 0
// css should call defineCss in any part of code
// attr (addClass, removeClass, changeStyle direct methods?) can be changed (in element?) to avoid confusion with the use inside html
// add remove element from array
// debug of "set" key
// can be useful to define pointers (shortcode/path of the objects)
// https://dev.to/ternentdotdev/json-compression-in-the-browser-with-gzip-and-the-compression-streams-api-4135
// a method to append an image to the DOM in base64 format <image xlink:href="data:image/png;base64,...
// after (ms) can be a standard parameter (like selector) very useful
// BUG: if {arguments:id} is undefined gives "undefined"
// try to replace Snap.load with fetch
// selctor, if string, should return querySelectorAll
// find element.getAttribute('data-selector') and replace with self.elementToSelector(value). Then change queryAll
// // it let to select more than one element cardColorDark
// in self.pluginsLoadedQueue add the actions to do when the plugin is loaded
// notUndefined può essere migliorato per "if" e per considerare vera la stringa se presente (ma va considerato '0' e 0)
// for name=i, if i is an object, can use {i:node1} but not {i:node1.node2}ì. Workaround is to assign i to a var
// so a possible solution is to use "var" parameter
// functions (js like) -> methods? no
// check this.html notes
// anime can be a part/block/key function? called with "ui:anime" asking for the relative plugin?
// if the name of the block/part includes ":" it is more explicit
// blocks or parts or keys or nodes?
// parts -> blocks considering jsblocks.com?
// Dynamic modules loading? What if we ask for a node that is in the main modules?

// remove console from methods and add log in browser functions

// add log of the error if the app or json module is not in json format
// init, run, start -> do
// if selector is an array, repeat the action (do)
// think about html/do. Is it possible to avoid html object? Any unknown object is an html tag
// in parts, {setup:on.success} or {setup:config.color} doesn't work
// setup -> arguments
// part ui:icon: {selector:, config:} can be a js function () {var part = {}; self.run(config); }
// {arguments[0] arguments[1]. {1.setup}   }
// improve shortcuts (in extendJsonFromElement and in attr)
// this.text -> this.content
// html attribute of html method -> content (should be an html but can be multilingual)
// property validation like the sintax (\w[\w+d+]) of the "name" in the "for" method )
// default "html" al posto di "text": ogni contenuto testuale viene inserito con innerHTML anziché innerTEXT 
// html [] tipo array e  "" stringa (innerHTML) ok, html {} deprecato
// "p": "text" -> p.innerTHTML('text')
// "p": [] -> se item è oggetto -> htmlTag, se stringa dovrebbe fare append della stringa html sempre nello p. Ora invece crea più p.innerText
// ADD: append. empty true fixed on html
/* 
1 page -> url (method and property)
2 dynamic class with ternary condition
3 swal button style
4 components copy bugs
5 installation guide
6 self.page -> not the variable but the real window path and window hash (can be jsonic.url)
7 page -> url
8 "roles" -> "only"
9 add localStorage actions -> ANY WINDOW method
10 add dataStorage
10 in parts {string} instead of {value} ?
11 on load image solution (should already work on: load -> animate -> fadein)
 */

// BUGS

// BUG from Tailwind 3.3.2
// Sembra richiesto tailwind nel plugin Jsonic e non caricato dinamicamente come su eliokit.com
// Different rendering width or without spaces after commans "alert": " {js:(1 == 2) ? \"test\" : \"false condition\"} "



// HOW IT WORKS
// loadModules -> loadPlugins -> Run

// KNOWN BUGS

// in museomira.it cerca "socialIcons", rimuovendo data-value l'icona finisce dentro contatti perché il newSelector è a:eq(5) 
// è necesario creare una funzione selector destinata ai nuovi elementi

// PARTS

// 1. Responsive header menu
// 2. Alert
// 3. Slide
// 4. Form 
// 5. Table

// GOAL

// Reduce custom js code in Jsonic and move the functions in modules
// Requires an extension of js method to make it more flexible
// js {function: String, params: Array}

// TO DO

// inline use: data-jsonic -> jsonic.run(data-jsonic, selector); (es. in body)
// init = run (or play)
// in parts {setup} should get subreferences link {setup:on.success}
// i plugins possono includere direttamente l'url
// la gestione dei plugin dovrebbe essere centralizzata (riferimento a jsonic e non all'oggetto)
// si potrebbe definire la distinsione tra oggetti specifici e oggetti globali (jsonic)
// verificare se oggetto function con {name: , params: []} è assegnabile a variabili
// addClass, removeClass etc with array
// change the behaviour of p: [] become a a list of content, not a list of p
// to do it, search and change "div": [   "p": [   "span": [   "li": [   "button": [   
// hypothesis (no, more complete with actions): class="ui:button{text:#333;bg:#fff}" -> bg-[white] text-[#333] hover:bg-[#fff] hover:text-[white]
// OPTIMIZATION: pluginsRequiredByTag only for self.json.resources.pluginsFunctions keys or better only for parts that requires plugins. alert -> ui:alert
// getJSON: load json (id: exampleApp, url: ...), success-> {getJSON:exampleApp}
// Ajax: load ajax (id: exampleApp, url: ...), success-> {ajax:exampleApp}
// DOM: queryAll(selector) -> value is string, el.before(value) and all the other methods of an element of the dom
// properties {dom:title}
// https://www.w3schools.com/jsref/dom_obj_all.asp
// DOCUMENT
// https://www.w3schools.com/jsref/dom_obj_document.asp
// {document: {title: 'test'}}
// properties {document:title}
// ON: https://www.w3schools.com/jsref/dom_obj_event.asp
// "selector" can be "query" / no selector recalls querySelector
// or APPEND/PREPEND
// https://registry.npmjs.com/-/v1/search?text=tailwindcss&size=1
//https://api.npms.io/v2/package/tailwindcss
// https://api-docs.npms.io

/* 
TO DO

1. ajax call with an id to identify the data in case of simultaneous requests
1. database path should be separated by spaces (or slashes)

replaceProperties default:

check if

if p1 is a function, if p2 -> value = p1(p2), if p3 -> value = p1(p2(p3))
if p1 is a value, and not p2, value = p1

self.methods[p1] or... 
p1 is a function


{alert:string}
{window.f}
{window f1:window f2:window x}  = window.f1(window.f2(window.x))

{querySelector:'.class':append} = window.f(jsonic.json.var.x)

ESEMPI

{new Date:'December 17, 1995 03:24:00'}
{new Date().getDay}
{console.log:'test'}
{window.innerHeight} invece di {window:innerHeight} (specifica) o {js:window.innerHeight} 

il vantaggio di questo schema è che si evita un livello di parentesi annidate
quando sono coinvolte variabili jsonic:
{querySelector:var obj x:append}
che puo diventare
{querySelector:var {item} x:append}
LIMITE
questo metodo preclude l'accesso ad eventuali oggetti javascript var o data
SOLUZIONE
in realtà, var e data devono essere elementi definiti nel json
altrimenti non sono trovati dall'interprete che può vagliare l'esistenza
di una funzione js

{path to a data} is a reference to a value in the data node of the Jsonic app
if the action context (do)
{path to an action} is a reference to a method or a sub app in the "parts" node of the Jsonic app
if the html rendering context (html)

{window.f:var x} = window.f(jsonic.json.var.x)
{window.f:num} if is number
{window.f:array} if isArray (more than one parameter)
{window.f:'string'}

{jsonic.f:var x}

do: [
    "functionName": [] // parameters
]

check if functionName is a function
html tags can't have a space or points...

Align with ELIO Language: 
1. "if": {"is": ["a", "=", "b","and"...] (retrocompatibile: se is è un array...)
2. "set": ["a", "+=", "1","*", "3"...] (retrocompatibile: se set è un array...)
3. "div classes": ...

CODE REDUCING

1. string in "html" object that starts with a tag and continue with classes

"html": [
    "span appsDeviceListName ms-2 me-2 text-[16px] text-left w-[80%] leading-tight|content"
]

NOTES

// datapicker and other input fiels now are native https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date

COMPONENTS

MONACO TAILWINDCSS AUTOCOMPLETE (check Tailwindcss VS Code extensions)
https://github.com/remcohaszing/monaco-tailwindcss


FIREBASE STRUCTURE DATA

If you create your own keys, they must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127. You cannot use ASCII control characters in the values themselves, either.

24.4KB gzipped (85.24KB minified)
25.2KB minified + gzipped in 100ms

65ms 3G 3 Mbps (375 KBs)
10ms 4G 20 Mbps (2,5 MBs)
<1ms 5G/Fiber 200 Mbps (25 MBs)

TEST

tailwindcss
104 KB  105 ms

INTEGRATIONS:
https://b2bsaasleads.com

NEW PLUGINS:

http://jakim.me/Toasty.js/
https://tingle.robinparisi.com

GUIDE:

COMPONENTS: reusable components / architectural philosophy
ENGINE, (no hidden code) all the parts of the app is exposed to the developer / can set the layout and the behaviour
JSON E' il modello di struttura ad albero usato da JSONIC per descrivere i dati, lo stile, e le azioni dell'applicazione
SHORTCUTS: combinations of utility names (classes) to reuse in "class" property (from WindiCSS)
 // preloaded js/css plugins

{
    "attr": {
        "class": "py-2 text-center"
    },
    "subTitle": {
        "lang": {
            "it": "L'albero JSON dell'app può contenere i seguenti rami principali:",
            "en": "The JSON app tree may contain the following main branches:",
            "es": "El árbol de la aplicación JSON puede contener los siguientes ramas principales:"
        }
    }
},		


PROPERTIES

With {property}, a string delimited by braces you can refer to any sub-branch the app tree and special properties.

-----------------------
HOME PARAGRAPHS
-----------------------

AUTO-LOADED PLUGINS

NESTED CSS

HTML has a clear nested and visual hierarchy but CSS doesn't. JSONIC let you nest your CSS definitions in a way that follows the same visual hierarchy of your HTML. It's similar to the SAAS extension but without the need to compile the code. In modo simile al linguaggio SAAS, è necessario compilare il codice


 // BP
 Questo dimostra che c'è bisogno di semplificazione più di quanto non si possa pensare

IDE

Special char (or shortener or autocomplete) with &lbrace; &rbrace; etcetera)

 
PRIORITY: 

tags:

button: "title"
or
button: {
    text: title
    color: color
    on: ...
}

buttonOutline: {
    text: title
    color: color
    on: ...
}

------------------------
qualunque metodo:
[
    {"path": args},
    {"path": args}
    {"path": args}
]
------------------------


DEBUG
class must be a string otherwise log error
style in attr must work (now error)


CONFIG (now setup)
extend the main branch {b:config}

ACTIONS (now parts)
extend the main branch {b:actions}

DO
execute one or an array of functions
"do": "actionID" (string) execute the actions.actionID function

CONTAINER (now selector)
change the context to the element {b:container}
Can be an object like {id, class, data-value}
or a string with a CSS selector like "body > div:nt-child(2)"
or a jquery-like selector like "body > div:eq(2)"

------------------------

bootstrap menu -> tailwind

------------------------


IDEAS

- share the modules in the object "jsonic" with the other jsonic objects in the page
- jsonic.registerJsMethod(name, function) register new js method to be inserted in jsonic.methods and called with "method": {params}
- choose can contain the methods "next":"1" / "previous":"1" or step "+1"/"-1"
- calendar can return the object {lang: {...}}
- self.js can be embedded in self.compile (if typeof params == 'string')
- Page doesn't exist / if admin / want to create it?
- if no jsonic file for the page, create it on save

- AUTH (debug) + GITHUB

- preload font flag
- IDE: Wordpress API new page
- window methods in (docs)
- for -> addTag (?)
- setup: form to set setup properties
- Method ARRAY

"array": {
"push": "{var:num}" (pop, shift, unshift, length)

DONE:

remove JSONIC references: jsonic.firebaseEventAction / jsonic.resizeEvent
- HOME: docs / examples / integrations
- wordpress: access to media API
- iconify
- iconify remove SVG old
- colors -> setup (mmm) (like logo)
- styles -> css -> senza categorie, solo elements '#div'  -> https://transform.tools/css-to-js DONE
}

BUGS:
- version è necessario in pluginsFunctions
- shortcut as a node: attr / class values doesn't overwrite the shortcut

WORDPRESS:

(_embed to your URL will add the wp:featuremedia)
https://jsonic.io/wp-json/wp/v2/media?&context=embed&per_page=100
https://jsonic.io/wp-json/wp/v2/media?&context=embed&after=2017-11-07T00:00:00
https://jsonic.io/wp-json/wp/v2/media?include=1681,491

// https://github.com/schlosser/pig.js

NOTE:

- autoupdate only if the previous update is complete (we need a param in config complete:true/false)
- Play (create your app!)
- add a tag in menu buttons ?
- inViewport -> 'enter'/'exit' ?
- inViewport solo la prima volta?
- ('on': 'in'/out NO) confirm 'init'? 
- IDE: backup timeline
- duplicato di this.selector dedicata a ottenere il selector di un nuovo elemento (su append e poco altro)
- col: [1,2,3] -> row with col1 col2 col3 ? or tailwind grid solution (ragionare su soluzione di tag. esempio: flex: [])
- in examples, test in posizione assoluta sul codice (utile su mobile)

KEY BENEFITS

- Integration (Jsonic code includes the logic, the layout and the style of the app on in one JSON object)
- Accelerator

*/

// JQUERY to Vanilla: https://youmightnotneedjquery.com | http://vanilla-js.com
// Jquery plus: https://atypiccraft.com/insights/reasons-why-we-still-use-jquery
// https://cyrilletuzi.github.io/javascript-guides/jquery-to-javascript.html
// Tools to integrate: https://bestofjs.org/tags
// MINIFY: https://closure-compiler.appspot.com/home
// https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cat

// INTEGRATIONS

// https://www.dropzone.dev/js/
// uploadcare
// zaiper

// a graph js tool

// anime.js
// transition.style // analogo ad animate.style

/* There were issues affecting this run of Lighthouse:

There may be stored data affecting loading performance in this location: IndexedDB. Audit this page in an incognito window to prevent those resources from affecting your scores.

// UI

https://windicss.org/play.html
https://basscss.com
https://twind.dev
https://emotion.sh/docs/introduction
https://cssinjs.org/?v=v10.8.2

https://picturepan2.github.io/spectre/
https://get.foundation
https://ant.design/
https://purecss.io
https://onsen.io/
https://shuffle.dev/
*/

//{"type": "js", "url":"https://cdn.jsdelivr.net/npm/tailwindcss@{version}/lib/index.min.js"},


//$.fn.jsonic = function(options) {return new jsonicObject(options);};
//jsonicApp = function(options) {return new jsonicObject(options);};


var jsonAppObj = function(options) {

    var self = this;
    console.log();

    //this.options = options;
    /* var json = {  // the jsonic app tree
        setup: {},
        blocks: {}, // to be removed
        parts: {},
        // plugins: {}, // TO DO: remove the error if included
        // css: {},
        do: {},
        on: {},
        shortcuts: {},
        texts: {},
        data: {},
        var: {}
    }; */
    //var var = json.var;
    //this.setup = json.setup;
    // for external access
    this.json = {
        setup: {},
        blocks: {}, // to be removed
        parts: {},
        // plugins: {}, // TO DO: remove the error if included
        // css: {},
        functions: {},
        do: {},
        on: {},
        shortcuts: {},
        texts: {},
        data: {},
        var: {}
    };
    this.app = this.json;

    //var json = self.json; // TO DO: remove
    var alertObj = {};
    var alertValues = {};
    this.params = {}; // maybe can be removed

    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'];
    // Source: https://www.w3schools.com/js/js_reserved.asp
    var nodes = {
        //parts: ['text', 'div', 'ul', 'p', 'a', 'button', 'input', 'span', 'img', 'video', 'audio', 'source', 'figure', 'figcaption', 'textarea', 'iframe', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'svg', 'path', 'g', 'd', 'polygon', 'circle', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'form', 'form-group', 'label', 'select', 'option', 'optgroup', 'nav', 'tab', 'header', 'main', 'footer', 'ide', 'model-viewer', 'i', 'icon', 'pre', 'code','animateMotion','animateTransform','circle','clipPath','defs','desc','discard','ellipse','feBlend','feColorMatrix','feComponentTransfer','feComposite','feConvolveMatrix','feDiffuseLighting','feDisplacementMap','feDistantLight','feDropShadow','feFlood','feFuncA','feFuncB','feFuncG','feFuncR','feGaussianBlur','feImage','feMerge','feMergeNode','feMorphology','feOffset','fePointLight','feSpecularLighting','feSpotLight','feTile','feTurbulence','filter','foreignObject','g','hatch','hatchpath','image','line','linearGradient','marker','mask','metadata','mpath','path','pattern','polygon','polyline','radialGradient','rect','script','set','stop','switch','symbol','text','textPath','title','tspan','use','view'],
        //functions: ['do', 'function', 'for', 'run', 'module', 'page', 'html', 'text', 'empty', 'alert', 'blocks', 'attr', 'color', 'set', 'array', 'replace', 'var', 'js', 'javascript', 'if', 'switch', 'choose', 'delay', 'ajax', 'hide', 'show', 'toggle', 'in', 'out', 'link', 'scroll', 'lang', 'find', 'data', 'reload', 'calendar', 'moment', 'dayjs', 'log', 'setInterval', 'clearInterval', 'offcanvas', 'firebase', 'database', 'qrcode', 'editor', 'ace', 'code', 'thunkable', 'animate', 'sortablejs', 'uiUpdate'], // 'String', 'Number', 'Math', 
        //functionsWithContainer: ['for', 'editor', 'ace', 'code', 'qrcode', 'lottie', 'animate', 'html', 'hide', 'show', 'toggle', 'in', 'out', 'if', 'ajax', 'attr', 'run', 'delay'],
        // not execute
        extend: ['setup', 'blocks', 'parts', 'css', 'actions', 'texts', 'data', 'functions'], // also plugins? (TO DO: remove actions)
        params: ['style', 'on', 'matchMedia', 'action', 'code', 'roles', 'plugins', 'css'], // 'if', 'attr', 'text'. 'html'
        exclude: ['setup', 'container', 'selector', 'info', 'note', 'comment', 'lang', 'tag', 'plugins', 'template']
    };

    /*
        * Converts a JSON object to a JSON Schema
        * @param {any} json
        * @param {object} options
        * @returns {object} a json schema
    */
    // [{"name": "vito", "gallery": {"name":2, "date":"10-10-2012"}, "num":3, "bool":true, "color": "#ff0000"}]
    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]?)$/,
        // J. Meijer / Stackoverflow / 15491894
        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})$/,
        // Joey / Stackoverflow / 1636350
        color: /^#(?:[0-9a-fA-F]{3}){1,2}$|#(?:[0-9a-fA-F]{3,4}){1,2}$/,
    };

    this.jsonToSchema = function (json, options = {}) {
        // https://github.com/mohsen1/json-to-json-schema
        if (typeof json === 'function') {
            throw new TypeError('Can not convert a function');
        }

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

        // primitives
        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 all schemas are the same use that schema for items
            if (schemas.every(s => self.isEqual(s, schemas[0]))) {
                schema.items = schemas[0];

                // if there are multiple schemas use oneOf
            } 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;
    };

    this.isEqual = function (a, b) {
        return (JSON.stringify(a) == JSON.stringify(b));
    };


    //var canvasElement = document.getElementById("canvas");
    // * this is not important for PDFMake, it's here just to render the result *
    // It's a Mozilla lib called PDFjs that handles pdf rendering directly on the browser
    /* this.pdfjs = function (params, selectorParams) {
    
        var url = params.url;
        var options = params.options || { scale: 1.4 };
        let container = self.selector(params.selector || params.container) || self.selector(selectorParams);
        var canvasContainer = self.query(container);

        function renderPage(page) {
            var viewport = page.getViewport(options.scale);
            var wrapper = document.createElement("div");
            wrapper.className = "canvas-wrapper";
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            var renderContext = {
                canvasContext: ctx,
                viewport: viewport
            };
            
            canvas.height = viewport.height;
            canvas.width = viewport.width;
            wrapper.appendChild(canvas)
            canvasContainer.appendChild(wrapper);
            
            page.render(renderContext);
        }
        
        function renderPages(pdfDoc) {
            for(var num = 1; num <= pdfDoc.numPages; num++)
                pdfDoc.getPage(num).then(renderPage);
        }
    
        PDFJS.disableWorker = true;
        PDFJS.getDocument(url).then(renderPages);
    
    } */
    //renderPDF('https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf', document.querySelector('#canvas'));
    /* this.localStorage = function (params) {
        // -> use self.method
        if (params.getItem) return localStorage.getItem(params.get);
        else if (params.setItem) return localStorage.setItem(params.setItem.name, params.setItem.value);
    } */
    // UTILITY
    /** A storage solution aimed at replacing jQuerys data function.
     * Implementation Note: Elements are stored in a (WeakMap)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap].
     * This makes sure the data is garbage collected when the node is removed.
     */
    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;
        }
    };

    // JQUERY cross-over
    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;
        }
    };

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

    /*     this.actionResult = function(value, args) { // value -> params
    
            // Methods with a result
        
            if (value && typeof value == 'object') {
    
                    // jsonic methods
                    if (value.js) {
                        return self.js(value.js, args);
                    } else if (value.array) {
                        return self.array(value.array, args);
                    } else if (value.calendar) {
                        return self.calendar(value.calendar, args);
                    } else if (value.data) {
                        return self.data(value.data, args);
                    } else if (value.var) {
                        return self.var(value.var, args);
                    } else if (value.module) {
                        return self.module(value.module, args);
                    } else if (value.color) {
                        return self.color(value.color, args);
                    } else if (value.css) {
                        return self.css(value.css, undefined, args);
                    } else if (value.if) {
                        return self.if(value.if, undefined, args);
                    } else if (value.switch) {
                        return self.switch(value.switch, args);
                    } else if (value.database) {
                        return self.database(value.database, args);
                    // js methods
                    } else if (value.Math) {
                        return self.Math(value.Math, args);
                    } else if (value.Number) {
                        return self.Number(value.Number, args);
                    } else if (value.String) {
                        return self.String(value.String, args);
                    // plugin methods
                    } else if (value.ace) {
                        return self.ace(value.ace, args);
                    // else if (value.code) {
                    //    return self.code(value.code, args);
                    } else if (value.moment) {
                        return self.moment(value.moment, args);
                    } else if (value.dayjs) {
                        return self.dayjs(value.dayjs, args);
                    } else if (value.function) { // TO DO: remove (replaced by do)
                        return self.function(value.function, value.params);
                    } else {
                        return value;
                    }
            } else {
                return value
            }
            //} else if (typeof value == 'string') {
            //    return self.javascript(value, args);
    
        } */
    this.var = function (params, args) {


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

            //self.log('var');
            var name;
            var value;
            var varValue;

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

            if (typeof params == 'string') {
                // gives a variable value
                name = self.replaceProperties(params, args);
                /* self.log('name');
                self.log(name); */
                /* self.log('typeof params == string');
                self.log('params');
                self.log(params);
                self.log('name');
                self.log(params); */
                varValue = self.docElement('self.json.var.' + name);

                /* self.log('varValue');
                self.log(varValue); */
            } else {

                /* self.log('var');
                self.log('params');
                self.log(params);
                self.log('args');
                self.log(args);  */
                //self.log('params');
                //self.log(params);
                var paramsReplaced = self.replaceProperties(params, args);
                //self.log('paramsReplaced');
                //self.log(paramsReplaced);
                name = paramsReplaced.name;
                value = paramsReplaced.value;
                var operator = paramsReplaced.operator;
                var type = paramsReplaced.type;

                /* self.log('extend');
                self.log(extend); */
                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);

                    /* self.log('name');
                    self.log(name);
                    self.log('pathBase');
                    self.log(pathBase);
                    self.log('pathLast');
                    self.log(pathLast);
                    self.log('varObj');
                    self.log(varObj); */
                    if (value !== undefined) {

                        //if (value == 'string')
                        //    value = self.docElement('self.json.var.'+value); // todo
                        //else
                        value = self.actionResult(value, args);

                        // typization
                        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;
                        }

                        //self.log('value');
                        //self.log(value);
                        /*
                        // reset
                        if (value == undefined && typeof value == 'string') value = '';
                        if (value == undefined && typeof value == 'number') value = 0;
                        if (value == undefined && typeof value == 'object') value = {}; */
                        /* self.log('operator');
                        self.log(operator);
                        self.log('pathLast');
                        self.log(pathLast);
                        self.log('varObj[pathLast]');
                        self.log(varObj[pathLast]); */
                        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;
                            }

                            /* operators: [
                                '=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
                                '&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^', '%',
                                '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=', '^=',
                                '%=', '<<=', '>>=', '>>>='
                                ], */
                            //self.log('varObj[pathLast]');
                            varValue = varObj[pathLast];
                        } else {
                            /* self.log('pathString');
                            self.log(pathString); */
                            //varValue = self.docElement(pathString, value);
                            //varValue = value;
                            self.docElement(pathString, value);

                            /* self.log('name');
                            self.log(name);

                            self.log('var '+name);
                            self.log(self.var(name)); */
                        }

                        /* self.log('pathLast');
                        self.log(pathLast);
                        self.log('varObj[pathLast]');
                        self.log(varObj[pathLast]); */
                        /*  //alert(value);
                        //value = self.replaceProperties(value, args);
                        if (value == 'string') {
                            //self.json.var[name] = self.docElement('self.json.var.'+value);
                            varObj[pathLast] = self.docElement('self.json.var.'+value);
                        } else {
                            //self.json.var[name] = self.actionResult(value, args);
                            varObj[pathLast] = self.actionResult(value, args);
                        } */
                        //self.log('varObj[pathLast]');
                        //if (self.isJson(self.json.var[name])) 
                        //    self.json.var[name] = self.parse(self.json.var[name]);
                    } 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 {
                        // no "value" property: return the value of the var "name"
                        varValue = varObj[pathLast];
                        /* varValue = self.replaceProperties(varObj[pathLast], args);
                        alert(JSON.stringify(varValue)); */
                    }

                    //varValue = self.json.var[name] || {}; // to check
                } 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);
                    //alert(JSON.stringify(params.do));
                    //self.do(params.do, args);
                    self.do(params.do, undefined, args);
                }

            }

            return varValue;

        }
    };

    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 { // if (typeof params.value == 'string') {
                        for (var key in params.value) {
                            if (params.value[key] == item[key])
                                return item;
                        }
                    }
                }
            });
        }
    };

    /* this.data = function (params, args) {
        if (typeof params == 'string')
            return self.db(params, args);
        else
            self.firebase(params, args);
    } */
    this.array = function (params, args) {
        /* self.log("---- this.array ----");
        self.log('params');
        self.log(params);  */
        if (args)
            params = self.replacePropertyWithPrefix(params, 'result', args);

        for (var par in params) {
            var paramReplaced = self.replaceProperties(params[par], args);
            //value[par](params[par]) // ipotesi di eseguire in automatico qualunque funzione js su stringhe
            /* self.log("par");
            self.log(par);
            self.log("params[par]");
            self.log(params[par]);
            self.log("paramReplaced");
            self.log(paramReplaced); */
            params[par] = paramReplaced;
        }

        //self.log("params");
        //self.log(params);
        var value = self.element({ path: params.name }) || [];
        /* self.log("value");
        self.log(...value); */
        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 });

    };

    this.replace = function (params, args) {
        // self.log("---- this.replace ----");
        params = self.replacePropertyWithPrefix(params, 'result', args);
        /* self.log("params");
        self.log(params); */
        var value = self.element({ path: params.path }) || [];
        var to = params.to || '';

        var type = typeof value;
        /* self.log("type");
        self.log(type); */
        if (type !== "string") value = JSON.stringify(value);
        /* self.log("value 1");
        self.log(value); */
        if (params.from) value = self.replaceAll(value, params.from, to);
        /* self.log("value2 ");
        self.log(value); */
        switch (type) {
            case "number":
                value = Number(value);
                break;
            case "boolean":
                value = Boolean(value);
                break;
            case "object":
                value = JSON.parse(value);
                break;
        }
        /* self.log("value 3");
        self.log(value); */
        //return value;
        return self.element({ path: params.path, value: value });

    };


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

    this.set = function (params, args) {

        if (Array.isArray(params)) { // 1.0.5
            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); // TO DO: check
                if (value && args) value = self.replacePropertyWithPrefix(value, 'result', args); // backward compatibility TO DO: remove

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

        //return true
    };


    this.get = function (element) {
        //self.log('self.get');
        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
    };

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

    /*     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);
            }
        } */
    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://app.fixo.io:4002';
        
                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 multipleFiles = [];
    /*     this.fileDownload = function (url) {
            self.log('fileDownload');
            self.log(url);
    
            var fileArr = url.split('?');
            self.log(fileArr);
            //var file = fileArr[0];
            var urlArr = fileArr[0].split('/');
            self.log(urlArr);
            var name = urlArr[urlArr.length-1];
            self.log(name);
            var token = self.getParameterByName('token', url);
            self.log(token);
    
            window.location.href = '/app/php/proxy.php?name='+ name + '&url='+ url;
    
        }  */
    /*     this.fileUrl = function (params) {
            // da nome file a l'url Firebase Storage inclusiva del token
            self.log('fileUrl');
            self.log(params);
            // self.log('storage');
            // self.log(storage);
            if (storage) {
                var storageRef = storage.ref();
                var pathReference = storageRef.child(params.user+'/'+params.name);
                
                pathReference.getDownloadURL().then(function(url) {
                    if (params.callback) params.callback(url);
                }).catch(function(error) {
                    if (error && error.code && params.error) {
                        self.log(self.firebaseStorageError(error.code));
                        params.error(error.code);
                    }
                    // A full list of error codes is available at
                    // https://firebase.google.com/docs/storage/web/handle-errors
                });
            } else {
                self.log('storage undefined');
            }
        } */
    /*     this.firebaseStorageError = function (errorCode) {
            switch (errorCode) {
                case 'storage/object-not-found':
                    // File doesn\'t exist
                    return 'File doesn\'t exist';
                case 'storage/unauthorized':
                    // User doesn\'t have permission to access the object
                    return 'User doesn\'t have permission to access the object';
                case 'storage/canceled':
                    // User canceled the upload
                    return 'User canceled the upload';
                case 'storage/unknown':
                    // Unknown error occurred, inspect the server response
                    return 'Unknown error occurred, inspect the server response';
                default:
                    return 'No error'
            }
        } */
    var x = [{ if: 3 }, function () { }, 3, []];

    this.firebaseAddListener = function (params) {
        /* self.log('firebaseAddListener');
        self.log(params); */
        // {path, orderByChild, equalTo}
        //self.log(params.path);
        if (database) {
            // Aggiunge listener in ascolto su params.path che esegue params.action
            if (params.action) {
                if (!self.listeners[params.path]) self.listeners[params.path] = [];
                /* alert('params.action:'+params.action);
                alert('type params.action:' + typeof params.action);
                alert(params.path+':'+JSON.stringify({
                    action: params.action
                })); */
                //if (Array.isArray(params.action))
                //    self.listeners[params.path].concat(params.action);
                //else
                self.listeners[params.path].push(params.action);
            }
            /* if (params.success !== undefined) {
                alert('params.success:'+params.success);
                alert('type params.success:' + typeof params.success);
                alert(params.path+':'+JSON.stringify({
                    success: params.success
                }));
                self.listeners[params.path].push(params.success);
            } */
            //self.log('params.path: '+ params.path);
            //self.listeners[params.path] = params.action;
            //alert(self.listeners[params.path]);
            /*
            
            limitToFirst()	Sets the maximum number of items to return from the beginning of the ordered list of results.
            limitToLast()	Sets the maximum number of items to return from the end of the ordered list of results.
            
            startAt()	Return items greater than or equal to the specified key or value, depending on the order-by method chosen.
            startAfter()	Return items greater than the specified key or value depending on the order-by method chosen.
            endAt()	Return items less than or equal to the specified key or value, depending on the order-by method chosen.
            endBefore() endBefore()	Return items less than the specified key or value depending on the order-by method chosen.
            equalTo()	Return items equal to the specified key or value, depending on the order-by method chosen.
            
            orderByChild()	Order results by the value of a specified child key or nested child path.
            orderByKey()	Order results by child keys.
            orderByValue()	Order results by child values.
            orderByPriority() Order results by priority

            https://firebase.google.com/docs/reference/js/v8/firebase.database.Query

            */
            //Firestore.collection(collectionName).orderBy(field).where(field, ">=", keyword.toUpperCase()).where(field, "<=", keyword.toUpperCase() + "\uf8ff").get() 
            // https://medium.com/feedflood/filter-by-search-keyword-in-cloud-firestore-query-638377bf0123
            // https://firebase.google.com/docs/database/rest/retrieve-data?hl=it#range-queries
            // startAt , endAt , limitToFirst , limitToLast o equalTo
            //databaseReference.orderByChild('_searchLastName').startAt(queryText).endAt(queryText+"\uf8ff")
            // where ("string, "in"/"not-in", array)
            // https://firebase.google.com/docs/firestore/query-data/queries?hl=it#in_and_array-contains-any
            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]); // maybe params.where in enough

            var feedback = dbRef.on('value', firebaseEvent);
            //console.log(feedback);
            /* console.log('dbRef');
            console.log(dbRef);
    */
            /* if (params.orderByChild && params.equalTo)
                dbRef.orderByChild(params.orderByChild).equalTo(params.equalTo).on('value', firebaseEvent);
            else
                dbRef.on('value', firebaseEvent); */
        } else {
            self.log('database undefined');
            // Restart the db
        }
    };

    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();
            //if (!dbVal) dbVal = {};
            var pathArr = path.split(/\.\w+\//);
            var dbPath = pathArr[1];
            self.log('firebaseEvent: ' + dbPath);
            //if (var.databaseURL) dbPath = path.substr(var.databaseURL.length+1);
            //else dbPath = path;
            /* self.log('path: '+ path);
            //self.log('var.databaseURL: '+ var.databaseURL);
            self.log('dbPath: '+ dbPath); */
            if (dbVal)
                self.localDbUpdate(dbVal, dbPath);
            else {
                var dbObj = self.element({ path: 'self.json.var.db.' + self.replaceAll(dbPath, '/', '.') });
                if (dbObj)
                    dbObj = null;
                //delete dbObj;
            }


            //self.do(self.listeners[dbPath], dbVal);
        }
    };

    this.localDbUpdate = function (newObj, dbPath) {
        /* self.log('localDbUpdate');
        self.log(newObj);
        self.log(dbPath); */
        //var path = newObj.path;
        //delete newObj.path;
        /* self.do(
            {
                var: {
                    name: "db."+self.replaceAll(dbPath, '/', '.'),
                    value: newObj,
                    //success: function () {
                    //    self.do(self.listeners[dbPath], newObj);
                    //}
                }
            }
        ); */
        //alert('db.'+self.replaceAll(dbPath, '/', '.'));
        /* var dbUpdateAction = {
            var: {
                name: 'db.'+self.replaceAll(dbPath, '/', '.'),
                value: newObj
                //do: (self.listeners[dbPath] && self.listeners[dbPath].length > 0) ? self.listeners[dbPath] : undefined
            }
        };
            if (self.listeners[dbPath] !== undefined && self.listeners[dbPath] !== null && self.listeners[dbPath].length > 0)
            dbUpdateAction.var.do = self.listeners[dbPath];
        
        newObj.path = dbPath;

        self.do(dbUpdateAction, newObj); */
        // TO DO: decide l'utente dove salvare i dati 
        newObj = Object.fromEntries(Object.entries(newObj).reverse());

        //newObj = Object.keys(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], newObj);
                self.do(self.listeners[dbPath], undefined, newObj);
            }
        }


    };



    this.firebaseRemoveListener = function (params) {
        // Rimuove listener in ascolto su data.path
        self.log('firebaseRemoveListener');
        if (self.listeners[params.path]) {
            delete self.listeners[params.path];
            var dbRef = database.ref(params.path);
            dbRef.off();
        }
    };

    // Rimuove un elemento del database
    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) {
                //Dati impostati con successo
                if (error == null) {
                    self.log('set SUCCESS');
                    result = { success: true };
                } else {
                    self.log('set ERROR');
                    //Errore inserimento dati
                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    // Write or replace data to a defined path
    this.firebaseSet = function (path, value, callback) {
        /*
        set	Write or replace data to a defined path, like messages/users/<username>
        update	Update some of the keys for a defined path without replacing all of the data
        push	Add to a list of data in the database. Every time you push a new node onto a list, your database generates a unique key, like messages/users/<unique-user-id>/<username>
        transaction	Use transactions when working with complex data that could be corrupted by concurrent updates
        */
        if (database) {
            var result, dbRef;
            if (path) {
                dbRef = database.ref().child(path);
            } else {
                dbRef = database.ref();
            }
            return dbRef.set(value, function (error) {
                //Dati impostati con successo
                if (error == null) {
                    self.log('set SUCCESS');
                    result = { success: true };
                } else {
                    self.log('set ERROR');
                    //Errore inserimento dati
                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    // Update some of the keys for a defined path without replacing all of the data
    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) {
                //Dati impostati con successo
                if (error == null) {
                    self.log('update SUCCESS');
                    result = { success: true };
                } else {
                    self.log('update ERROR');
                    //Errore inserimento dati
                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };

    // Add to a list of data in the database. Every time you push a new node onto a list, your database generates a unique key, like messages/users/<unique-user-id>/<username>
    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) {
                //Dati impostati con successo
                if (error == null) {
                    self.log('push SUCCESS');
                    result = { success: true };
                } else {
                    self.log('push ERROR');
                    //Errore inserimento dati
                    result = { success: false, error: error };
                }
                if (callback) callback(result);
            });
        } else {
            self.log('database not initialized');
        }
    };



    // https://firebase.google.com/docs/database/security/rules-conditions
    // tutte le get / o listeners / potrebbero essere basate su orderByChild("owner")
    /*
            rooms": {
                // this rule applies to any child of /rooms/, the key for each room id
                // is stored inside $room_id variable for reference
                "$room_id": {
                    "topic": {
                    // the room's topic can be changed if the room id has "public" in it
                    ".write": "$room_id.contains('public')"
                    }
                }
                }
    */
    /*        this.firebaseQuery  = function(params) {
            self.log('firebaseQuery');
            self.log(params);
            // path = 'users/'+self.user('email')
            // value = 'guest'
            var dbRef = database.ref(params.path);
    
            if (!params.result) params.result = {};
            if (params.value && !Array.isArray(params.value)) {
                var buffer = params.value;
                params.value = [];
                params.value[0] = buffer;
            }
            //self.log('params.value');
            //self.log(params.value);
            if (params.value && params.value.length > 0) {
                var value = params.value.shift();
                //self.log('value');
                //self.log(value);
                //self.log('params.key');
                //self.log(params.key);
                dbRef.orderByChild(params.key).equalTo(value).once('value',  (object) => {
                    self.log('object');
                    self.log(object);
                    if(object) {
                        params.result = self.extend(params.result, object.val());
                        if (params.value.length > 0)
                            self.firebaseQuery(params);
                        else
                            params.callback({
                                success: true,
                                error: null,
                                data: params.result
                            });
                    } else {
                        //self.log('firebaseQuery ERROR');
                        //result = {success:false, error:8};
                        params.callback({
                            success:true,
                            error:null,
                            data: params.result
                        });
                    }
                });
            } else {
                params.callback({success:false, error:null});
            }
        } */
    this.firebaseGetPart = function (params) {
        self.log('firebaseGetPart');
        let remotePath = self.replaceAll(params.localPath, '.', '/');

        self.log('localPath:' + params.localPath);
        //self.log('remotePath:'+remotePath);
        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) {
                //Dati recuperati con successo
                if (object) {
                    self.log('firebaseGetPart SUCCESS');
                    self.log(object);
                    // local db update
                    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+\//); // split on the domain extension
                            let dbPath = pathArr[1]; // path after the domain extension
                            let localPath = self.replaceAll(dbPath, '/', '.');
                            //var dbPath = path.substr(var.databaseURL.length+1);
                            self.log('dbPath: ' + dbPath);
                            self.log('localPath: ' + localPath);
                            //self.log('self.partContainers[localPath]');
                            //self.log(self.partContainers[localPath]);
                            self.element({ path: localPath, value: dbVal });
                            for (partObj of self.partContainers[localPath]) {
                                /* self.log('partObj');
                                self.log(partObj); */
                                let part = self.replacePropertyWithPrefix(dbVal, 'setup', partObj.setup);
                                part = self.replacePropertyWithPrefix(part, 'arg', partObj.arg);
                                self.extendJsonFromElement(part); // or selectorParams


                                /* self.log('part');
                                self.log(part); */
                                self.html(part, partObj.container);
                                //self.run(part, partObj.container);
                                //self.run(dbVal, container);
                            }
                        }
                    }
                } else {
                    self.log('firebaseGetPart ERROR');
                    result = { success: false, error: 8 };
                }
                //self.log(result);
                //callback(result);
            });
        }
    };


    /* {
        "database": {
            "do": "get",
            "path": "apps/dev/{setup:key}",
            "on": {
                "success": [
                    { */
    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);
            }
        });
    };

    this.firebaseGet = function (path, callback) {
        self.log('firebaseGet:' + path);
        var result = { success: false, error: 7 };
        if (path) {
            // try, if path is wrong "Uncaught TypeError: Cannot read properties of undefined (reading 'ref')"
            dbRef = database.ref().child(path);
        } else {
            dbRef = database.ref();
        }

        /* if (false) { //path == 'files') {
            //alert(self.user('email'));
            return dbRef.orderByChild('users/'+self.user('email')).equalTo('guest').on('value',  (object) => {
            //return dbRef.orderByChild('prefs/authorId').equalTo(self.user('uuid')).on('value',  (object) => {
            //return dbRef.orderByChild('users').equalTo(self.user('email')).on('value', (object) => {
                //self.log(object);
                //var user = object.val();
                //Dati recuperati con successo
                if(object) {
                    result = {success:true, error:null, data: object.val()};
                } else {
                    self.log('get ERROR');
                    //Errore recupero dati
                    result = {success:false, error:8};
                }
                //self.log(result);
                callback(result);
            });
        } else { */
        // ATTENZIONE: va intercettato e loggato l'errore di accesso negato dalle regole
        return dbRef.once('value').then(function (object) {
            //Dati recuperati con successo
            if (object) {
                self.log('get SUCCESS');
                result = { success: true, error: null, data: object.val() };

                // local db update
                if (object.val() !== undefined) {
                    var dbVal = object.val();
                    var path = object.ref.toString();
                    if (!dbVal) dbVal = {};
                    var pathArr = path.split(/\.\w+\//); // split on the domain extension
                    var dbPath = pathArr[1]; // path after the domain extension

                    //var dbPath = path.substr(var.databaseURL.length+1);
                    self.log('dbPath: ' + dbPath);
                    self.localDbUpdate(dbVal, dbPath);
                }
            } else {
                self.log('get ERROR');
                //Errore recupero dati
                result = { success: false, error: 8 };
            }
            //self.log(result);
            callback(result);
        });

        //}
    };

    this.stringToKey = function (str) {
        return str.replace(/\./g, '%2E');
    };

    this.keyToString = function (key) {
        return key.replace(/%2E/g, '.');
    };

    this.firebaseKey = this.getKey = function (path) {
        var dbRef = database.ref(path);
        return dbRef.child(path).push().getKey();
    };

    // --------------
    //  SELECTOR
    // --------------
    this.exist = function (selector) {
        console.log('selector');
        console.log(selector);
        console.log(Boolean(self.query(selector)));
        return Boolean(self.query(selector));
    };

    this.count = function (selector) {
        //self.log('count:'+selector+ '='+ self.queryAll(selector).length);
        return self.queryAll(selector).length;

        /* if (selector) {
            var routes = selector.split(/[ >]+/);
            if (!element) element = document;
            
            for (var index in routes) {
                var route = routes[index];

                //onsole.log('route');
                //self.log(route);

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

                //self.log('routeParts');
                //self.log(routeParts);

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


                } else {
                    element = element.querySelectorAll(route)[0];
                }
            } */
        /* var elements = self.query(selector);
        if (elements)
            return elements.querySelectorAll().length;
        else
            return 0 */
        /* if (self.exist(selector))
            return document.querySelectorAll(selector).length;
        else
            return 0 */
    };

    var log = function (id, value) {
        self.log(id);
        self.log(value);
    };

    this.classSelector = function (selClass) {
        if (selClass.indexOf('.') == 0) { selClass = String(selClass).substr(1); } // backward compatibility
        let classes = selClass.split(' ');
        selClass = classes[0];
        /* self.log('selClass');
        self.log(selClass); */
        // avoid dynamic classes as selector
        var regex = new RegExp('[\-][\[]');
        if (selClass && selClass.match(regex)) selClass = classes[1];
        if (selClass && selClass.match(regex)) selClass = classes[2];
        return selClass;
    };

    this.selector = function (params, containerParams, selectAll) {
        // {container, class, value, id}
        var selector;

        if (params) {

            if (typeof params == 'string') {
                /* self.log('this.selector');
                self.log(params); */
                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 selContainer = container; //data.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 (selId) selId = self.compile(selId);
                if (selValue) selValue = self.compile(selValue);
                if (selClass) selClass = self.compile(selClass); */
                // use the first class as selector
                if (selClass)
                    selClass = self.classSelector(selClass);

                //if (!selectAll) selClass = undefined;
                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;

                    //if (!selectAll) 
                    //    selector += ':eq(' + self.count(selector) + ')';
                    // qui c'è / c'era un baco:
                    // quando proviamo a eseguire un'azione su un elemento esistente
                    // usando solo la classe come selector, $(selector).length = 1
                    // quindi prova a selezionare il secondo elemento eq(1), non il primo eq(0)
                } else if (params.tag) {
                    //if (selContainer) selector = selContainer;
                    selector = params.tag;
                    if (selContainer) {
                        selector = selContainer + ' > ' + params.tag;
                        if (!selectAll) {

                            //if (count !== $(selector).length) self.log('>> '+ selector + ' count:'+ self.count(selector) + ' !== ' + $(selector).length );
                            selector += ':eq(' + self.count(selector) + ')';
                            //selector += '[' + self.count(selector) + ']';
                            //selector += ':eq(' + $(selector).length + ')';
                        }
                    } else {
                        if (!selectAll) {
                            selector += ':eq(' + self.count(selector) + ')';
                            //selector += '[' + self.count(selector) + ']';
                        }
                    }
                } else {
                    selector = selContainer;
                };

            }
            return selector;
        }
    };

    // --------------
    //  TEXTS
    // --------------
    this.stringify = function (obj) {
        if (typeof obj == 'object')
            try {
                return JSON.stringify(obj);
            } catch (error) {
                return obj;
            }

        else
            return obj;

    };

    this.parse = function (str) {
        if (typeof str == 'string')
            try {
                return JSON.parse(str); // json
            } catch (error) {
                return str; // non json
            }

        else
            return str;
    };

  /*   this.removeEventProperties = function (params) {
        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;
        }
        return paramsObj;
    }; */


    self.setupId = 0;

    this.replacePropertyWithPrefix = function (params, prefix, args) {

        var paramsString;

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

        else
            paramsString = String(params);

        /* let argsWithProperties = (args) ? JSON.stringify(args) : '';
        paramsString = self.replaceAll(paramsString, '"{'+prefix+'}"', argsWithProperties); // obsolete */
        //if (typeof args == undefined)
        //    self.log('replacePropertyWithPrefix args is undefined', 'yellow'); 
        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 });
                    //let value = self.element({path:path, root: args});
                    /* if (typeof value == undefined) {
                        self.log(match + ' not found', 'orange');
                        value = '';
                    } else  */
                    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;
                }
            });

            /* if (paramsString && paramsString.indexOf('Contacts')>= 0) {
                self.log(paramsString, 'green');
            } */
            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 });
                    //let value = self.element({path:path, root: args});
                    /* if (typeof value == undefined) {
                        self.log(match + ' not found', 'orange');
                        value = '';
                    } else  */
                    if (value === undefined)
                        return '';
                    else if (typeof value == 'object') // when object inside a string?
                        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;
        }

    };

    /*     this.replaceProperty = function (params, name, args) { // TO DO: change name in replaceArguments
            //console.log('replaceProperty:'+name);
            //var actions = {};
    
            if (typeof params == 'object') {
    
                params = self.stringify(params);
            }
    
            if (typeof args !== 'string') {
    
                let argsWithProperties = (args) ? JSON.stringify(args) : '';
                params = self.replaceAll(params, '"{'+name+'}"', argsWithProperties); // obsolete
    
                // TO DO: get self.element({path:key, root: args});
    
                for (let key in args) {
                    if (typeof args[key] !== undefined) {
                        if (typeof args[key] == 'object')
                            params = self.replaceAll(params, '"{'+name+' '+key+'}"', JSON.stringify(args[key]));
                        else
                            params = self.replaceAll(params, '{'+name+' '+key+'}', args[key]);
                    } else {
                        params = self.replaceAll(params, '{'+name+' '+key+'}', '');
                        self.log('{'+name+' '+key+'}' + ' not found', 'yellow');
                    }
                }
    
                for (let key in args) {
                    if (args[key]) {
                        if (typeof args[key] == 'object')
                            params = self.replaceAll(params, '"{'+name+':'+key+'}"', JSON.stringify(args[key]));
                        else
                            params = self.replaceAll(params, '{'+name+':'+key+'}', args[key]);
                    } else {
                        params = self.replaceAll(params, '{'+name+':'+key+'}', '');
                    }
                }
    
            } else {
                //console.log('args STRINGA');
    
                params = self.replaceAll(params, '{'+name+'}', args);
    
            }
    
            if (self.isJson(params)) {// (typeof params == 'object')
                var paramsObj = self.parse(params);
                return paramsObj;
            } else {
                //alert(paramsString);
                return params;
            }
    
        }
        */
    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);
        }

        /* self.log('replaceResult');
        self.log('params');
        self.log(params); */
        if (self.isJson(params)) { // (typeof params == 'object')  
            var paramsObj = self.parse(params);
            if (paramOn) paramsObj.on = paramOn;
            if (paramSuccess) paramsObj.success = paramSuccess;
            if (paramError) paramsObj.error = paramError;
            return paramsObj;
        } else {
            //alert(paramsString);
            return params;
        }

    };

    /*
    this.replaceTags = function (params, selectorParams) {
        self.log('replaceTags');
        var undef = false;

        if (params.text && (typeof params.text == 'string' || typeof params.text == 'object')) {
            //alert(params.text);

            params.text = self.stringify(params.text);

            // i tag speciali sono contenuti tra i caratteri { e }
            if (params.text.indexOf('{') >= 0) {
                // alert(params.text.indexOf('{'));

                // <{visible:element}>
                params.text = params.text.replace(/\<\{visible\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = ($(p1).is(':visible'));
                    return value;
                });

                // <{var:var.object}>
                params.text = params.text.replace(/\<\{this\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = self.json[p1];
                    if (!value) undef = true;
                    return self.stringify(value);
                });

                // <{data:value}>
                    params.text = params.text.replace(/\<\{data\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = self.json.var[p1];
                    if (!value) undef = true;
                    return self.stringify(value);
                });

                params.text = params.text.replace(/\<\{var\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = self.get(p1);
                    if (!value) undef = true;
                    return self.stringify(value);
                });

                params.text = params.text.replace(/\<\{param\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = self.params[p2];
                    if (!value) undef = true;
                    return value;
                });

                // <{value:[id]}>
                params.text = params.text.replace(/\<\{value\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = $('#'+p1).val();
                    // https://www.geeksforgeeks.org/html-value-attribute/
                    if (!value) undef = true;
                    return value;
                });

                // <{user:[fieldName]}>
                params.text = params.text.replace(/\<\{user\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    var value = self.user(p1);
                    if (!value) undef = true;
                    return self.stringify(value);
                });

                // <{time:timestamp}>
                params.text = params.text.replace(/\<\{time\:timestamp\}\>/g, self.getTimestamp());

                // <{result}>
                if (params.args) {
                    params.args = self.stringify(params.args);
                    params.text = params.text.replace(/\<\{result\}\>/g, params.args);
                }

                // <{function:app.mac}>
                params.text = params.text.replace(/\<?\{function:([^\}]*)\}\>?/g, function(match, p1, offset, string) {
                    var functionName = self.docElement(p1);
                    return functionName(selectorParams);
                });

                // <{random}>
                params.text = params.text.replace(/\<\{random\:([^\-]+)\-([^\}]+)\}\>/g, function(match, p1, p2, offset, string) {
                    return Number(p1)+Math.round((p2-p1)*Math.random());
                });

                // <{firebaseKey:path}>
                params.text = params.text.replace(/\<\{firebaseKey\:([^\}]*)\}\>/g, function(match, p1, offset, string) {
                    return  self.firebaseKey(p1);
                });

            }
            if (undef) return undefined
            else return self.parse(params.text);
            
        } else {
            //self.log('params.text undefined or number or boolean');
            return params.text;
        }
    } */
    this.langText = function (textId) {
        // find text in language and follow the capitalization of textId
        // we could need text in langId language

        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]];
                //textValue = textObj[langId] || textObj[self.json.setup.language] || textObj['en'];
            }
            return textValue;
        } else {
            return textId;
        }

        /*  if (self.json.texts[textId])
            textObj = self.json.texts[textId];
        else if (self.json.texts[textId.toLowerCase()])
            textObj = self.json.texts[textId.toLowerCase()];
        //else if (self.json.texts[textId.toUpperCase()])
        //    textObj = self.json.texts[textId.toUpperCase()];
        if (textObj) {
            var textValue = textObj[self.json.setup.language] || textObj['en'] || textId;
            if (textId.charAt(0) == textId.charAt(0).toUpperCase()) // first char uppercase
                if (textId.charAt(1) == textId.charAt(1).toUpperCase())
                    return textValue.toUpperCase();
                else
                    return textValue.replace(/\b\w/, l => l.toUpperCase()); // capitalize
                    // textValue = textValue.charAt(0).toUpperCase() + name.slice(1)
                    // style: text-transform: capitalize;
            else
                return textValue.toLowerCase();
        } else {
            return textId;
        } */
        /*  } else {
                self.log('textId undefined in langText');
            } */
    };

    this.lang = function (params, args) {
        if (args) {
            var value = self.replaceProperties(params, args);
            self.json.setup.language = value;
            //alert(value);
            self.log('lang');
            self.log(value);

            document.querySelectorAll('[data-text]').forEach(function (element, index) {
                //$('*[data-text]').each(function( index ) {
                var dataParams = self.dataStorage.get(element, 'params');
                var selector = self.dataStorage.get(element, 'selector');
                /* self.log('index');
                self.log(index);
                self.log('dataParams');
                self.log(dataParams);
                */
                //alert(JSON.stringify(dataParams));
                self.html(dataParams, { selector: selector });
                //self.text($(this).data('params'), {container: this});
            });

            /*
                        var elements = document.querySelectorAll('[data-text]');
                        self.log('elements');
                        self.log(elements);
                        //var elements = document.querySelectorAll(selector);
                        if (elements)
                            elements.forEach(function (element, index) {
                                self.log(index);
                                //var dataParams = self.dataStorage.get(element, 'params');
                                var dataSelector = self.attribute(element, 'data-selector');
                                //self.text(dataParams, {container: dataSelector});
                                self.text($(dataSelector).data('params'), {container: dataSelector});
            
                            });
                                */
        } else {
            return self.json.setup.language;
        }
    };
    // Determine whether is possible to write an image/text to the clipboard.
    var isClipboardWritingAllowed = function () {
        return new Promise(function (resolve, reject) {
            try {
                navigator.permissions.query({ name: "clipboard-write" }).then(function (status) {
                    // PermissionStatus object
                    // {
                    //  onchange: null,
                    //  state: "granted" (it could be as well `denied` or `prompt`)
                    // }
                    self.log(status);

                    resolve((status.state == "granted"));
                });
            } catch (error) {
                // This could be caused because of a Browser incompatibility or security error
                // Remember that this feature works only through HTTPS
                reject(error);
            }
        });
    };

    this.codeSearch = function (params) {
        let selector = params.selector || params.container;
        // {container, word}
        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 // do not change selection
            });
            editor.session.selection.clearSelection();
            editor.setOption("highlightActiveLine", true);


            /* range.start.column = 0;
            range.end.column = Number.MAX_VALUE;
            //editor.session.replace(range, "x" + editor.session.getLine(range.start.row) + "x");
            editor.selection.setRange(range); */
        }
    };


    this.ace = function (params, selectorParams) {
        // https://codepen.io/zymawy/pen/XwbxoJ
        //self.log('code');
        //self.log(params);
        var selector = params.selector || params.container;
        var container = selector || self.selector(selectorParams);

        //paramsReplaced = self.cloneObject(params);
        //paramsReplaced.value = self.actionResult(paramsReplaced.value);
        var value = params.value;

        // item from library
        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);
        // json file
        if (params.module) {
            let moduleName = self.replaceProperties(params.module);
            if (moduleName && self.modules[moduleName] !== undefined) {
                value = self.modules[moduleName];

                /* self.log('value');
                self.log(value);
                self.log(value); */
            }
            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);
        //var containerId = $(container).attr('id');
        //self.log('containerId');
        //self.log(containerId);
        //var containerId = $(container).attr('id');
        if (element) {

            //var containerId = self.attribute(element, 'id'); // or assign a random unique id
            var editor = ace.edit(element);

            /* var keywordMapper = this.createKeywordMapper({
                "tag" : 'div|ul|p|a|button|input|span|img|video|audio|source|figure|figcaption|textarea|iframe|table|thead|tbody|tfoot|tr|th|td|svg|small|h1|h2|h3|h4|h5|h6|form|form-group|label|select|option|optgroup|nav|lottie-player|qrcode|tab|model-viewer'
            }, "identifier"); */
            /*   var staticWordCompleter = {
                    getCompletions: function(editor, session, pos, prefix, callback) {
                        var wordList = ['div', 'ul', 'p', 'a', 'button', 'input', 'span', 'img', 'video', 'audio', 'source', 'figure', 'figcaption', 'textarea', 'iframe', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'svg', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'form', 'form-group', 'label', 'select', 'option', 'optgroup', 'nav', 'lottie-player', 'qrcode', 'tab', 'model-viewer'];
                        callback(null, wordList.map(function(word) {
                            return {
                                caption: word,
                                value: word,
                                meta: "static"
                            };
                        }));
                
                    }
                }
                
                //langTools.setCompleters([staticWordCompleter])
                // or
                editor.completers = [staticWordCompleter] */
            /* if (editor)
                AceColorPicker.load(ace, editor); */
            //var value = paramsReplaced.value; // else value = params;
            if (!params.do || (params.do == 'set')) {

                //var langTools = ace.require('ace/ext/language_tools');
                if (params.theme)
                    editor.setTheme(params.theme); // "ace/theme/monokai"
                var editorMode = params.mode || "ace/mode/json";
                editor.getSession().setMode(editorMode);

                if (params.options)
                    editor.setOptions(params.options);
                //AceColorPicker.load(ace, editor, {hideDelay: 1000});
                //import AceColorPicker from 'ace-colorpicker'; 
                /* editor.getSession().setMode(editorMode, () => {
                    AceColorPicker.load(ace); // , {hideDelay: 1000}
                }); */
                var editorStyle = self.extend({
                    //background: "rgba(255,255,255,0)"
                }, params.style);
                //$(container).css(editorStyle); //editor.container.style
                self.css({
                    style: editorStyle
                }, {
                    selector: container
                });

                //var aceOptions = params.options;
                /* var aceOptions = self.extend({
                    fontSize: "12pt",
                    selectionStyle: "line",
                    //showGutter: false,
                    highlightGutterLine: false,
                    fixedWidthGutter: false,
                    //vScrollBarAlwaysVisible: true,
                    tabSize: 3,
                    //readOnly: true,
                    //enableBasicAutocompletion: false,
                    //enableSnippets: false,
                    //enableLiveAutocompletion: false,
                }, params.options); */
                // https://github.com/ajaxorg/ace/wiki/Configuring-Ace
                var codeString;
                if (typeof value == 'string')
                    if (editorMode == "ace/mode/json")
                        codeString = JSON.stringify(JSON.parse(value), null, '\t'); // json formatting

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

                // should be {lines:dividcode}
                if (codeString) {
                    var codeRows = codeString.split(/\r\n|\r|\n/).length;
                    /* $(container).css({
                        height: Number(21 * (codeRows)) + 'px'
                    }); */
                    /* self.log('element');
                    self.log(element);
                    self.log('container');
                    self.log(container);
                    self.log('codeRows');
                    self.log(codeRows); */
                    element.style.height = Number(21 * codeRows) + 'px';


                    //element.querySelector('.ace_scroller').querySelector('.ace_content').height = Number(21 * codeRows) + 'px';
                    /* self.css({
                        style: {
                            height: Number(21 * codeRows) + 'px'
                        }
                    }, {
                        container: container
                    }); */
                    editor.setValue(codeString, -1); // -1 unselect the code



                    //editor.renderer.updateFull(true);
                    //AceColorPicker.load(ace, ace.edit(containerId));
                    self.uiUpdate(); // resize event trigger


                    // workaround known ACE bug (.ace_content element doesn't follow the height until window resize)
                }

                if (params.on) {

                    var containerId = self.attribute(element, 'id'); // or assign a random unique id
                    self.codeChangeFunctions[containerId] = params.on;
                    //self.editorChangeFunctions[editor.id] = params.on;
                    if (params.on.change) {
                        editor.on('change', function (event, obj) {
                            /* self.log('event');
                            self.log(event);
                            self.log('obj');
                            self.log(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);
                                var methods = self.replaceResult(action, code);

                                //var methods = self.replaceProperties(action, code); 
                                // check on jsonic.io/play (methods is string instead of object)
                                /* self.log('code');
                                self.log(code);
                                self.log('method');
                                self.log(method);
                                self.log('-------------'); */
                                //self.do(methods);
                                self.do(methods);
                                //self.do(code, self.editorChangeFunctions[obj.id]);
                                //var container = self.editorChangeFunctions[obj.id].change.container;
                            } else {
                                // syntax error
                            }
                            //self.do(self.editorChangeFunctions[obj.id].change, JSON.parse(obj.getValue()));
                            //self.log(obj);
                            //self.blocks(JSON.parse(obj.getValue()), {container: });
                        });
                    }

                    // save key
                    // https://michilehr.de/overwrite-cmds-and-ctrls-in-javascript
                }


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

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

                self.uiUpdate(); // resize event trigger


                // workaround known ACE bug (.ace_content element doesn't follow the height until window resize)
            } else if (params.do == 'search') {
                if (params.word) {
                    var range = editor.find(params.word, {
                        wrap: true,
                        caseSensitive: true,
                        wholeWord: true,
                        regExp: false,
                        preventScroll: false // do not change selection
                    });
                    editor.session.selection.clearSelection();
                    editor.setOption("highlightActiveLine", true);
                    /* range.start.column = 0;
                    range.end.column = Number.MAX_VALUE;
                    //editor.session.replace(range, "x" + editor.session.getLine(range.start.row) + "x");
                    editor.selection.setRange(range); */
                }
            } else if (params.do == 'find') {
                // toggle
                editor.execCommand('find');
                //editor.searchBox.show();
            } else if (params.do == 'copy') {
                // Select the whole content of the editor
                editor.selectAll();
                // Store text that will be copied to clipboard
                let copyText = editor.getCopyText();
                // Verify if clipboard writing is allowed
                isClipboardWritingAllowed().then(function (allowed) {
                    // Write to clipboard if allowed
                    if (allowed) {
                        navigator.clipboard.writeText(copyText).then(function () {
                            editor.getSession().selection.clearSelection();
                            //self.log("Code copied!");
                        });
                    }
                }).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);
        }

    };

    // TO DO: should be in the app var and reset on a new execution 
    this.codeChangeFunctions = {};
    this.editorChangeFunctions = {};
    this.quillElements = {};

    this.text = function (params, selectorParams, args) {
        /*
            {
                "text": / "string": / textId -> textId   "{text:textId}"
                "case": [up, down, capitalize]
            }
            */


        /* if (Array.isArray(params)) {
            for (var obj of params)
                self.text(obj, selectorParams, args);
        } else { */
        var textKey;
        var textCase;
        var textArgs;
        var textLang;
        var langId;
        var selector, element;
        //var textAction;
        if (typeof params == 'object') {

            if (selectorParams && selectorParams.isConnected) { // Check if an element is attached to DOM
                element = selectorParams;
                //selector = self.dataStorage.get(element, 'selector');
                //selector = element.getAttribute('data-selector');
                selector = self.elementToSelector(element); // 1.0.12
            } else {
                selector = self.selector(self.extend({}, params, selectorParams));
                element = self.query(selector);
            }

            // todo: REMOVE (check the menu)
            //textAction = self.actionResult(params, textArgs);
            //textAction = params;
            //textKey = params.string; //  || params.text || params.title; // params.key to be removed?
            textCase = params.case; // use text-transform uppercase lowercase capitalize
            textArgs = params.args; // deve diventare args (secondo parametro) al posto di caseFunction


            //langId = params.langId;
            if (typeof params == 'object') {

                textKey = params.key || params.string || params; //  || params.text || params.title; // params.key to be removed?






                //textCase = params.case; // use text-transform uppercase lowercase capitalize
                //textArgs = params.args; // deve diventare args (secondo parametro) al posto di caseFunction
                //langId = params.langId;
            } else {
                textKey = params;
            }

            /* self.log('textKey');
            self.log(textKey); */
        } else if (typeof params == 'string') {
            textKey = params;

            /* self.log('textKey');
            self.log(textKey); */
            //textCase = caseFunction;
            if (selectorParams) {
                if (selectorParams.isConnected) { // Check if an element is attached to DOM
                    element = selectorParams;
                    selector = self.dataStorage.get(element, 'selector');
                } else {
                    selector = self.selector(selectorParams);
                    element = self.query(selector);
                }
            } else {
                // here when windowTitle += ' ' + self.text(page.title); // window title
            }
        }

        if (typeof textKey == 'string') {

            /* // TO DO: remove
            if (textKey.startsWith('js:')) textKey = self.js(textKey.substr(3));
            else if (textKey.startsWith('db:')) textKey = self.element({path: 'var.db.'+textKey.substr(3)});
            else if (textKey.startsWith('var:')) textKey = self.element({path: 'var.'+textKey.substr(4)});
            else if (textKey.startsWith('data:')) textKey = self.element({path: 'data.'+textKey.substr(5)});
                else
                // ----- */
            /* console.log('textKey');
            console.log(textKey); */
            textKey = self.replaceProperties(textKey, textArgs);
            /* console.log('textKey');
            console.log(textKey); */
        }

        //self.log('selector');
        //self.log(selector);
        //self.log(selector.isConnected);
        /* self.log('HOTSPOT');
        self.log(params);
        self.log(self.json.setup.language); */
        if (params && params.lang) { // multi-language text

            //self.log('params.lang definito');
            textLang = self.langText(params.lang); // , langId

            //textLang = params.lang[self.json.setup.language] || params.lang['en'] || params.lang[Object.keys[0]];
            textLang = self.replaceProperties(textLang, textArgs);

            /* self.log('textLang');
            self.log(textLang);
            self.log('element');
            self.log(element); */
            if (textCase) textLang = self.textCase(textLang, textCase);

            //self.log('textLang');
            //self.log(textLang);
            if (element) {
                element.textContent = textLang;
                self.attribute(element, 'data-text', true); // textKey instead of lang?
                self.dataStorage.set(element, 'params', params);
                //$(selector).data('params', params);
            }

        } else {
            //self.log('params.lang non definito');
            //if (!textKey) textKey = params;
            if (textKey) {
                //textKey = self.compile(textKey, textArgs); // TO DO: verify
                /* self.log('textKey');
                self.log(textKey); */
                //if (!textKey) alert(JSON.stringify(params));
                //if (typeof textKey == 'object') {
                //    textLang = JSON.stringify(textKey, null, '\t');
                //} else {
                //textLang = params.lang[self.json.setup.language] || params.lang['en'] || params.lang[Object.keys[0]];
                if (textKey.lang) // multi-language text
                    textLang = self.langText(textKey.lang); // , langId

                else
                    textLang = textKey;

                /* self.log('textLang1');
                self.log(textLang); */
                textLang = self.replaceProperties(textLang, textArgs); // move after textLang = self.langText(textKey.lang);
                if (textCase) textLang = self.textCase(textLang, textCase);

                //}
                /* self.log('textLang2');
                self.log(textLang); */
                //if (selector) { // set
                if (element) { // set

                    if (self.json.texts[textKey]) {
                        // save data-text for lang change
                        //$(selector).data('params', params);
                        //$(selector).attr('data-text', textKey);
                        self.attribute(element, 'data-text', true);
                        self.dataStorage.set(element, 'params', params);
                    }

                    // obsolete      
                    /* if (params.background) $(selector).css({'background': params.background});
                    if (params.color) $(selector).css({'color': params.color});
                    if (params.size) $(selector).css({'font-size': params.size}); */
                    if (params.style)
                        self.css({
                            style: params.style
                        }, {
                            selector: selector
                        });
                    //$(selector).css(params.style);
                    element.textContent = textLang; // put text in element


                    //$(selector).text(textLang);
                } else { // get
                    return textLang;
                }

            } else {
                /* self.log('"string" parameter required in text object');
                self.log(params); */
                return params;
            }

        }

    };

    var caseUp = function (string) {
        return string.toUpperCase();
    };

    var caseDown = function (string) {
        return string.toLowerCase();
    };

    var capitalize = function (string) {
        return self.capitalize(string);
    };

    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');
        }
    };

    /* this.textFit = function (selector,params) {
        if (params.alignHoriz == undefined) {
            params.alignHoriz = true;
        }
        if (params.alignVert == undefined) {
            params.alignVert = true;
        }
        if (params.minFontSize) {
            params.minFontSize = parseInt(params.minFontSize, 10); // remove px
        }
        if (params.maxFontSize) {
            params.maxFontSize = parseInt(params.maxFontSize, 10); // remove px
        }

        // textFit va in errore se l'oggetto diventa invisibile
        // mentre sta elaborando la dimensione del testo
        if (!$(selector).children('span').hasClass('textFitted')) {
            $(selector).each(function(i, obj) {
                if ($(obj).is(":visible")
                    && ($(obj).css("display") !== 'none')
                    && ($(obj).css('width'))
                    && ($(obj).css('height'))) {
                    textFit($(obj), params);
                }
            });
        }
    } */
    /*  this.data = function (path, value) {
        if (!value) return self.json.var;
    }

    this.setvar = function (data) {
        var = self.data();
        self.data(data);
    } */
    // --------------
    //  PAGES
    // --------------
    /* this.lastInteraction = function () {
        // uso: self.lastInteraction().asMinutes()
        var lastInteraction = 0;
        if (var.touchTime) lastInteraction = moment(var.touchTime);
        var thisMoment = moment();
        var duration = moment.duration(thisMoment.diff(lastInteraction));
        return duration
    } */
    // --------------
    //  icons e
    // --------------
    /*         this.icons = this.icon = function (params, selectorParams, args) {
            // { [container, class, value, id]
            // [html, text, append, prepend] }
    
            if (Array.isArray(params)) {
                    for (var index in params)
                    {self.icons(params[index], selectorParams);}
            } else {
                if (params.items) {
                    var itemsArray = params.items;
                    delete params.items;
                    for (var index in itemsArray) {
                        self.icons(self.replaceItems(params, itemsArray[index]), selectorParams);
                    }
                } else {
    
                    //if (selectorParams) params = self.extend(params, selectorParams);
    
                    var paramsReplaced = self.replaceProperties(params, args);
    
                    self.log('icons');
                    self.log('paramsReplaced');
                    self.log(paramsReplaced);
    
                    if (paramsReplaced) {
                        if (paramsReplaced.disabled == true)
                            self.disableIcon(paramsReplaced, selectorParams);
                        else if (paramsReplaced.disabled == false)
                            self.enableIcon(paramsReplaced, selectorParams);
                        else if (paramsReplaced.change)
                            self.changeItem(paramsReplaced, selectorParams);
                        else
                            self.addItem(paramsReplaced, selectorParams);
                    }
    
                    if (paramsReplaced.firebase) self.addFirebaseTag(paramsReplaced);
    
                    var selector = self.selector(self.extend(paramsReplaced, selectorParams));
    
                    if (paramsReplaced.span) // text near icon
                        self.addTag(paramsReplaced, {container: selector});
    
                }
            }
        } */
    /*         this.buttons = this.tab = function (params, selectorParams) {
    
            // { [container, class, value, id]
            // [html, text, append, prepend] }
            if (Array.isArray(params)) {
                    for (var index in params)
                        self.tab(params[index], selectorParams);
            } else {
                if (params.items) {
                    var itemsArray = params.items;
                    delete params.items;
                    for (var index in itemsArray) {
                        self.tab(self.replaceItems(params, itemsArray[index]), selectorParams);
                    }
                } else {
    
                    // se necessario, replaceProperties dovrebbe escludere il contenuto di events/action
                    // in quanto verrà ripetuto sulle azioni in tempo reale solo dove necessario
                    //params = self.replaceProperties(params);
    
                    if (!params.class) params.class = '';
                    var classes = params.class.split(' ');
                    
                    if (params.change) {
                        if (classes.includes('icon'))
                            self.changeItem(params, selectorParams);
                        else // if (classes.includes('tabs'))
                            self.changeTab(params, selectorParams);
                    } else {
                        if (classes.includes('icon'))
                            self.addItem(params, selectorParams);
                        else // if (classes.includes('tabs'))
                            self.addTab(params, selectorParams);
                    }
    
                    if (params.firebase) self.addFirebaseTag(params);
    
                }
            }
        }
    */
    /*
    this.replacePropertyNew = function (params, name, args) {
        console.log('replacePropertyNew');
        // like replaceProperties but only for {name:...} property
        var paramsString;

        if (typeof paramsString !== 'string') {paramsString = JSON.stringify(self.cloneObject(params));}
        
        // remove string quotes for object property
        if (args && paramsString && paramsString.indexOf('{')>= 0) {
        //if (args && paramsString && /{\w/.test(paramsString)) {
            paramsString = paramsString.replace(/\"\{(\w+\:)?([\w\s\.\:]+)\}\"/g, function(match, p1, p2, offset, string) {
                var value;
                if (p1 && p1.endsWith(':')) {p1 = p1.slice(0, -1);}
                console.log('match:'+match);
                console.log('p1:'+p1);
                console.log('p2:'+p2);
                if (p1 == name || (!p1 && p2 == name)) {
                    let nameFound = p1 || p2;
                    let pathFound = (p1) ? p2 : undefined;
                    console.log(nameFound+':'+pathFound);
                    if (typeof args !== 'string') {
                        //return self.replaceAll(self.stringify(args), '\"', '\'');
                        return self.stringify(args);
                    } else {
                        return '"' + args + '"';
                    }
                } else
                    return match;
            });
            //console.log('paramsString without quotes');
            //console.log(paramsString);
            paramsString = paramsString.replace(/\{(\w+\:)?([\w\s\.\:]+)\}/g, function(match, p1, p2, offset, string) {
                var value;
                if (p1 && p1.endsWith(':')) {p1 = p1.slice(0, -1);}
                if (p1 == name || (!p1 && p2 == name)) {
                    let nameFound = p1 || p2;
                    let pathFound = (p1) ? p2 : undefined;
                    console.log(nameFound+':'+pathFound);
                    self.partSetup = args;
                    if (pathFound) {
                        value = self.docElement('self.partSetup.'+pathFound);
                    } else {
                        value = args;
                    }
                    console.log('value:'+value);
                    delete self.partSetup;
                } else {
                    value = match
                }
                if (typeof value !== 'string') value = self.stringify(value);
                return value;
            });
            if (self.isJsonString(paramsString)) {// (typeof params == 'object')
                console.log('isJsonString');
                var paramsObj = self.parse(paramsString);
                return paramsObj;
            } else {
                console.log('NOT isJsonString');
                console.log(paramsString);
                // TO DO: log of wrong json string
                return paramsString;
            }
        } else {
            return params
        }
    }
    */
    this.replaceUndefined = function (params) {
        let value, defaultValue;
        //if (params.path == 'badge') alert();
        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) { // empty string is false
            if (typeof defaultValue !== undefined)
                value = defaultValue;
            else {
                if (params.removeUndefined) { //p1 == 'args' || p1 == 'selector') {
                    value = ''; // remove undefined values
                } else {
                    value = params.match;
                    self.log(value + ' not found', 'yellow');
                }
            }
        } else {
            //console.log('match:'+params.match);
            //if (params.removeUndefined) value = '';
        }
        return value;
    };
    //value = self.replaceUndefined({match: '{'+match+'}', id:p1, removeUndefined: removeUndefined});
    this.replaceProperties = function (params, args, removeUndefined) {
        // TO DO: remove fieldsObj. can't find replaceProperties with 3 args
        // the third parameter can be removeUndefined
        // test with a firebase app like elio

        var paramsString;
        var paramOn, paramDo, paramSuccess, paramError, paramEvents, paramConfirm;
        if (typeof params == 'string') {
            paramsString = params;
        } else {
            //paramsString = JSON.stringify(params);
            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; } // TO DO: remove
                if (paramsObj.confirm) { paramConfirm = paramsObj.confirm; delete paramsObj.confirm; } // TO DO: check it
                if (paramsObj.do) { paramDo = paramsObj.do; delete paramsObj.do; }
            }
            paramsString = JSON.stringify(paramsObj);
        }
        /* self.log("paramsString");
        self.log(paramsString); */
        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 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
                                    //value = value.replaceAll(/([^\\\"])\"/g, '$1\\"');
                                    break;

                                case 'plugins':
                                    value = self.json.resources.pluginsFunctions[p2];
                                    if (value && !Array.isArray(value)) value = [value];
                                    else value = [];
                                    /* self.log('plugins');
                                    self.log('value');
                                    self.log(value); */
                                    /* value = plugins;
                                    for (plugin of plugins) {
                                        value = self.json.resources.pluginsFunctions[p2].name || 'unknown plugin required';
                                    } */
                                    break;

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

                                // shorteners (useful?)
                                case 'media': // or m or src
                                    if (self.json.setup && self.json.data.media)
                                        value = self.docElement('self.json.data.media.' + p2) || match;

                                    else
                                        value = match;
                                    break;

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

                                    else
                                        value = match;
                                    break;

                                case 'class': // or cla
                                    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] || ''; // || p2+'=NULL';
                                    break;

                                case 'height':
                                    /* self.log('{height:p2}');
                                    self.log('p2');
                                    self.log(p2); */
                                    var element = document.querySelector(p2); // self.query(p2);
                                    if (element) // && self.isVisible(element)
                                        //value = getComputedStyle(element, null).height;
                                        value = element.offsetHeight + 'px';



                                    //if ($(p2).is(':visible'))
                                    //value = $(p2).height() + 'px'
                                    else
                                        value = '0px';
                                    break;

                                case 'offsetWidth':
                                    var element = document.querySelector(p2); // self.query(p2);
                                    if (element) // && self.isVisible(element)
                                        //value = getComputedStyle(element, null).height;
                                        value = element.offsetWidth;



                                    //if ($(p2).is(':visible'))
                                    //value = $(p2).height() + 'px'
                                    else
                                        value = '0';
                                    break;

                                case 'width':
                                    var element = document.querySelector(p2); // self.query(p2);
                                    if (element) // && self.isVisible(element)
                                        //value = getComputedStyle(element, null).height;
                                        value = element.offsetWidth + 'px';



                                    //if ($(p2).is(':visible'))
                                    //value = $(p2).height() + 'px'
                                    else
                                        value = '0px';
                                    break;

                                case 'local':
                                    value = self.methods.local.get(p2);
                                    //self.log('{localStorage:cart}');
                                    //self.log(value);
                                    break;

                                case 'ace':
                                    var element = document.querySelector(p2);
                                    if (element) {
                                        var editor = ace.edit(element);
                                        value = JSON.parse(editor.getValue());
                                    }
                                    //self.log('{localStorage:cart}');
                                    //self.log(value);
                                    break;

                                /* case 'innerText':
                                    var element = document.querySelector(p2);
                                    value = (element) ? element.innerText : undefined;
                                    self.log('p1:'+p1);
                                    self.log('p2:'+p2);
                                    self.log('value:'+value);
                                    //.trisBox[data-value="0"]
                                break;

                                case 'innerHtml':
                                    var element = document.querySelector(p2);
                                    value = (element) ? element.innerHTML : undefined;
                                break; */
                                case 'shuffle':
                                    value = self.element({ path: p2 });
                                    value = self.methods.shuffle(value);
                                    break;

                                case 'select':
                                case 'input':
                                case 'value': // deprecated

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

                                    //value = $('#'+p2).val() || ''; //|| p2+'=NULL';
                                    // input field value
                                    // https://www.geeksforgeeks.org/html-value-attribute/
                                    break;

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

                                    else
                                        value = self.user(p2) || match; //|| p2+'=NULL';
                                    break;

                                // can go in objects
                                case 'length':
                                    var varObj = self.element({ path: p2 }); // to get also js objects length

                                    //var varObj = self.docElement('self.json.'+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(); // timestamp
                                    else if (p2 == 'getSeconds') value = new Date().getSeconds();
                                    else if (p2 == 'getMinutes') value = new Date().getMinutes();
                                    else if (p2 == 'getHours') value = new Date().getHours();

                                    // second, minute, hour, weekday, day, month, year
                                    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': // format: long, short, narrow
                                    var day = (p2 == 'today' || p2 == undefined) ? new Date().getDay() : Number(p2);
                                    value = self.calendar({ index: Number(day), unit: 'weekday', format: 'long' });
                                    break;

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

                                case 'weekdayShort': // format: long, short, narrow
                                    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': // TO DO: check
                                    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); // || match; // TO DO: log error
                                    value = self.do(p2); // || match; // TO DO: log error




                                    //var functionName = self.docElement(p2);
                                    //value = (functionName) ? functionName(functionName) : p2+'=NULL'
                                    break;

                                /* WITH ARGS */
                                case 'method':
                                    var method = self.element({ path: p2, root: self.methods });
                                    if (method) {
                                        value = method(args); // how to pass args? needs p3
                                    } else {
                                        value = match;
                                    }
                                    break;

                                case 'function': // TO DO: remove
                                    var func = self.element({ path: p2, root: self.functions });
                                    if (func) {
                                        value = func(args); // how to pass args? needs p3
                                    } else {
                                        value = match;
                                    }
                                    break;

                                case 'on':
                                    /* self.log('args');
                                    self.log(args); */
                                    if (args) {
                                        self.onResult = args;
                                        value = self.docElement('self.onResult.' + p2);
                                        //alert(JSON.stringify(value));
                                        delete self.onResult;
                                    } else {
                                        //alert(JSON.stringify(value));
                                        value = match;
                                    }
                                    //value = ($(p2).is(':visible'));
                                    break;

                                case 'ajax':
                                    if (args) {
                                        self.ajaxResult = args; // -> self.json.var.ajaxData[self.json.var.ajaxId] = args.response

                                        //alert(JSON.stringify(args));
                                        value = self.docElement('self.ajaxResult.' + p2);

                                        delete self.ajaxResult;
                                    } else {
                                        //alert(JSON.stringify(value));
                                        value = match;
                                    }
                                    //value = ($(p2).is(':visible'));
                                    break;

                                /*   case 'function': // TO DO: remove
                                    //self.log('function');
                                    //self.log('p2');
                                    //self.log(p2); // app.mac
                                    value = self.function(p2) || match; // TO DO: log error

                                    //var functionName = self.docElement(p2);
                                    //value = (functionName) ? functionName(functionName) : p2+'=NULL'

                                break; */
                                case 'if': // TO DO: remove (can't manage spacial characters !<>'' etc)
                                    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':
                                    /* var random = p2.replace(/([^\-]+)\-([^\}]+)/g, function(match, n1, n2, offset, string) {
                                        return Number(n1)+Math.round((n2-n1)*Math.random());
                                    }); */
                                    value = String(Math.floor(Number(p2) * Math.random()));
                                    //self.log('random value: '+value);
                                    //value = random; //p2+' format is wrong'
                                    // CHANGE TO {math.random:max}
                                    break;

                                case 'getKey':
                                case 'firebaseKey':
                                    value = self.firebaseKey(p2);
                                    // https://firebase.google.com/docs/reference/android/com/google/firebase/database/DataSnapshot
                                    // CHANGE TO {data.exists:path/to/record}
                                    //"alert.close" functions(alert, do:close)
                                    break;

                                case 'window':
                                    /* try {
                                        value = eval('window.'.p2);
                                    } catch (error) {
                                        self.log('javascript error on '+ p2);
                                        value = match;
                                    } */
                                    //value = self.js('window.'.p2) || match;
                                    value = self.docElement(p2) || match;
                                    break;


                                /* case 'args':
                                    // often replaceProperties is called without args
                                    value = match;
                                break;  */
                                default:
                                    // ci finiamo dentro con {setup:value}
                                    /* var element = document.querySelector(p2);
                                    value = (element) ? element[p1] : '';
                                    self.log('p1 ' + p1);
                                    self.log('p2 ' + p2);
                                    self.log('value ' + value);
                                    if (!element)
                                        self.log('can\'t match ' + match, 'color:red'); */
                                    break;


                                /*                         default:
                                                        
                                                        var node = self.docElement('self.json.'+p1);
                                                        self.log('node');
                                                        self.log(node);
                                                        if (node) value = node[p2] // example: p1 = styles.colors p2 = colorName
                                                        else value = '<{'+p1+':'+p2+'}>';
                                                    break;
                                */
                            }
                        } // not self.functions[p1]
                    }
                } else if (p2) {

                    // can be a FUNCTION: from modules
                    let keyPrefix = p2.split(' ')[0]; // /\s\./
                    if (self.functions[keyPrefix]) {
                        value = self.function({ name: keyPrefix });
                        value = self.element({ path: p2, root: value }); // example: {pathFromHash 0}
                        console.log(keyPrefix);
                        console.log(value);
                    }
                    else
                        value = self.replaceUndefined({ match: match, path: p2, removeUndefined: removeUndefined });
                    //value = self.element({path: p2});
                }

                // value is a DOM element
                if (value && value.isConnected) value = self.elementToSelector(value); // DOM element to CSS selector

                if (!value) {
                    // TO DO: should be (typeof value == undefined)
                    // t's a critical change. It changes the result of "is": "'{data id}' !== ''"
                    if (removeUndefined)
                        value = '';
                    else {
                        if (typeof value == undefined)
                            self.log(match + ' is undefined', 'yellow');
                    }
                }
                if (typeof value !== 'string') value = self.stringify(value); // object or array to string (TO DO: check numbers)

                return value;
            });


            // js property TO DO: check it
            if (paramsString.startsWith('{js:') && paramsString.endsWith('}'))
                paramsString = self.js(paramsString.substr(4, paramsString.length - 1 - 4));

            //if (self.isJson(paramsString)) {// (typeof params == 'object')  
            if (self.isJsonString(paramsString)) { // (typeof params == 'object')  
                var paramsObj = self.parse(paramsString);

                // restore event nodes
                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; // obsolete
                return paramsObj;
            } else {
                // TO DO: log of wrong json string
                //alert(paramsString);
                /*  if (Array.isArray(self.parse(paramsString)))
                        return self.parse(paramsString)
                    else  */
                return paramsString;
            }
        } else {
            // restore event nodes
            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; // obsolete
            return params;
        }

    };

    /*      if (!value) undef = true;
            if (undef) return undefined
            else return params;
    */
    this.isJsonString = function (str) {
        try {
            JSON.parse(str);
        } catch (e) {
            return false;
        }
        return true;
    };

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

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

    /* this.replaceResult = function (params, args) {

        paramsString = paramsString.replace(/(\"\s*)\{[result]\:(\w+)\}(\s*\")/g, function(match, p1, p2, p3, offset, string) {
        });
    }
    */
    this.replaceItems = function (params, item, name) {
        // check this: http://mir.aculo.us/2011/03/09/little-helpers-a-tweet-sized-javascript-templating-engine/
        // params: <li><{item:name}></li>, item: Vito or item: {name: Vito}
        //var paramString = self.replaceProperties(params, undefined, item);
        var paramsString;
        var specialPattern;
        var itemName = name || 'item';
        /*  if (name) {
                self.log('replaceItems');
                self.log('params');
                self.log(params);
                self.log('item');
                self.log(item);
                self.log('name');
                self.log(name);
            }  */
        if (typeof params == 'string') paramsString = params; else paramsString = JSON.stringify(params);

        /* paramsString = paramsString.replace(/\{item\}/g, function(match, p1, offset, string) {

            var value;
        
            if (item) {
                value = item;
            } else {
                value = '';
            }
        
            if (typeof value == 'object') value = self.stringify(value);
            return value;
        }); */
        if (typeof item == 'object' || Array.isArray(item)) { //  || Array.isArray(item) not needed
            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));
        }
        //if (itemName == 'hour') alert(paramsString);
        /* if (itemName !== 'item') { // TO DO: remove
            var propertyPattern = new RegExp('\{'+itemName+'\:([^\}]+)\}', 'g');
            paramsString = paramsString.replace(propertyPattern, function(match, p1, p2, p3, offset, string) {


                var value = self.docElement(p1, undefined, item);
                if (value)
                    return value
                else
                    return match;

            });
        } */
        //specialPattern = new RegExp(/\"\{(\w+)\}\"/, "g"); 
        // item: inside double quotes (to manage objects inside double string)
        //specialPattern = new RegExp(`(\")\{${itemName}\:(\w+)\}(\")`, 'g');
        //paramsString = paramsString.replace(specialPattern, function(match, p1, p2, p3, offset, string) {
        //specialPattern = new RegExp('(\"\s*)\{'+itemName+'\:(\w+)\}(\s*\")', 'g');
        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;
    };

    /* 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;
    } */
    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
    // ----------------------------
    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);
        }
    };

    this.isArray = function (value) {
        return Array.isArray(self.js(value));
    };

    this.isVisible = function (el) {
        return (el.style.display !== 'none' && !self.hasClass(el, 'd-none'));
    };

    this.isHidden = function (el) {
        return (el.offsetParent === null);
    };

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

    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)

        );
    };

    this.outViewport = function (el) {
        return !self.outViewport(el);
    };

    this.notClass = function (el, className) {
        return !self.hasClass(el, className);
    };

    this.hasClass = function (el, className) {
        if (el.classList)
            return el.classList.contains(className);

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

    this.addClass = function (el, className) {
        if (el.classList)
            el.classList.add(className);
        else if (!self.hasClass(el, className)) el.className += " " + className;
    };

    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, ' ');
        }
    };

    this.toggleClass = function (el, className) {
        if (self.hasClass(el, className))
            self.removeClass(el, className);

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

    this.choose = this.select = function (params, selectorParams, args) {
        // "choose OR options / "select" is deprecated because select is an html tag
        // {selection: selection class, id: selected id}
        // or: choosed, choosable, selection
        // or: selector, selectable, selection
        // or: opted, options, selection
        /* self.log('select');
        self.log('params');
        self.log(params);
        self.log('args');
        self.log(args);  */
        if (Array.isArray(params)) {
            for (var obj of params)
                self.choose(obj, args);
        } else {

            //if (selectorParams) params = self.extend({}, selectorParams, params);
            var selId, selClass, selValue;

            selValue = params['data-value']; // || params.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);

            //var selector = self.selector(params.selector, undefined, true) || self.selector(selectorParams, undefined, true);
            if (params.selection) {

                var selectionClass = params.selection;
                /* self.log('selClass');
                self.log(selClass);
                self.log('selValue');
                self.log(selValue);
                self.log('selectionClass');
                self.log(selectionClass); */
                var selector = self.selector({ class: selClass });
                var elements = document.querySelectorAll('.' + selClass); // convert to any string selector (move the point to the code)
                elements.forEach(function (element, index) {
                    self.removeClass(element, selectionClass);
                });

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

                    //self.log('selector');
                    //self.log(selector);
                    //var elements = self.query(selector);
                    //self.log('elements');
                    //self.log(elements);
                    //if (elements)
                    //self.addClass(elements, selectedIconClass);
                    var elements = document.querySelectorAll(selector);
                    elements.forEach(function (element, index) {
                        self.addClass(element, selectionClass);
                    });
                }
            } else {
                /* self.log('select');
                self.log('selClass');
                self.log(selClass);
                self.log('selValue');
                self.log(selValue);
                self.log('selectionClass');
                self.log(selectionClass); */
                self.hide({
                    "selector": {
                        "class": selClass
                    }
                });
                self.show({
                    "selector": {
                        "id": selId,
                        "class": selClass,
                        "data-value": selValue
                    }
                });

                //self.log('selectselection" class parameter is required in "select" object');
            }

            //self.updateCodeEditors(); // workaround known ACE bug
            self.uiUpdate(); // resize event trigger

        }

    };

    /* this.select = function (params, selectorParams) {
        // { [container, class, value, id]
        self.log('self.select');
        if (Array.isArray(params)) {
                for (var index in params)
                self.choose(params[index], selectorParams);
        } else {

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

            params = self.replaceProperties(params);
            //params = self.replaceTags({text:params});
            
            self.selectItem(params);
        }
    }
*/
    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);
            /* self.log('selector');
            self.log(selector);
            self.log('element');
            self.log(element);
            self.log('self.isVisible(element)');
            self.log(self.isVisible(element)); */
            //if ($(selector).is(":visible"))
            if (self.isVisible(element))
                //self.fadeOut(params, selectorParams);
                self.out(params, selectorParams, args);

            else
                //self.fadeIn(params, selectorParams);
                self.in(params, selectorParams, args);
            //$(selector).toggle(500); // jquery bugs // , self.resizeEvent
        }
    };

    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';

            //self.log('selector');
            //self.log(selector);
            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, args);
            self.do(animation, undefined, args);
            //self.resizeEvent();
        }

    };

    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, args);
            self.do(animation, undefined, args);
        }
    };

    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'); // bootstrap

                //element.style.display = 'block';
                element.style.visibility = 'visible';
                /*
                container = element.getAttribute('data-selector');
                //container = self.dataStorage.get(element, 'selector');
                var action = [
                    {
                        "if": {
                            "container": container,
                            "hasClass": "opacity-0",
                            "then": {
                                "attr": {
                                    "container": container,
                                    "removeClass": "opacity-0"
                                }
                            }
                        }
                    },
                    {
                        "if": {
                            "container": container,
                            "hasClass": "invisible",
                            "then": {
                                "attr": {
                                    "container": container,
                                    "removeClass": "invisible" // bootstrap & tailwind
                                }
                            }
                        }
                    },
                    {
                        "if": {
                            "container": container,
                            "hasClass": "d-none",
                            "then": {
                                "attr": {
                                    "container": container,
                                    "removeClass": "d-none" // bootstrap
                                }
                            }
                        }
                    },
                    {
                        "if": {
                            "container": container,
                            "hasClass": "hidden",
                            "then": {
                                "attr": {
                                    "container": container,
                                    "removeClass": "hidden" // taliwind
                                }
                            }
                        }
                    },
                    {
                        "if": { // should be if style.display == none
                            "container": container,
                            "isDisplay": "none",
                            "then": {
                                "attr": {
                                    "container": container,
                                    "style": {
                                        "display": ""
                                    }
                                }
                            }
                        }
                    }
                ];
                //element.style.display = ' ';
                //alert(JSON.stringify(element.style));
                if (element.style.visibility && element.style.visibility !== 'visible')
                    element.style.visibility = 'visible';
                //self.do(action, args);
                //self.do(action, undefined, args);
                self.do(action, undefined, args); */
            });
        }
    };

    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');

                /* container = element.getAttribute('data-selector');
                var action = [
                    {
                        "attr": {
                            "container": container,
                            "addClass": "d-none" // bootstrap (to be removed)
                        }
                    },
                    {
                        "attr": {
                            "container": container,
                            "addClass": "hidden" // tailwind
                        }
                    }
                ];
                //self.do(action, args);
                self.do(action, undefined, args); */
            });
        }
    };


    /* this.fadeIn = function (params, selectorParams, args) {
        //self.log('fadeIn');
        //self.log(params);
        if (Array.isArray(params)) {
                for (var index in params)
                self.fadeIn(params[index], selectorParams);
        } else {
            //params.container = params.container || self.selector(selectorParams);
            //var selector = self.selector(params);
            var selector = self.selector(self.extend({}, params, selectorParams), {}, true);
            var duration = params.duration || 500;
            var done = (params.done) ? function  () {
                self.resizeEvent();
                self.do(params.done);
            } : null;
            $(selector).stop().fadeIn(duration, done);
        }
    }

    this.fadeOut = function (params, selectorParams, args) {
        //self.log('fadeOut');
        //self.log(params);
        // add onComplete and duration
        if (Array.isArray(params)) {
                for (var index in params)
                self.fadeOut(params[index], selectorParams);
        } else {
            //params.container = params.container || self.selector(selectorParams);
            //var selector = self.selector(params);
            var selector = self.selector(self.extend({}, params, selectorParams), {}, true);
            var duration = params.duration || 500;
            var done = (params.done) ? function  () {
                self.do(params.done);
            } : null;
            $(selector).stop().fadeOut({
                duration: duration,
                done: function  () {
                    self.resizeEvent();
                    self.do(params.done);
                }
            });
        }
    } */
    /*     this.show = function (params, selectorParams, args) {
            //self.log('show');
            //self.log(JSON.stringify(params));
            if (Array.isArray(params)) {
                    for (var index in params)
                    self.show(params[index], selectorParams);
            } else {
                //params.container = params.container || self.selector(selectorParams);
                //var selector = self.selector(params);
                var selector = self.selector(self.extend({}, params, selectorParams), {}, true);
                //self.log('selector');
                //self.log(selector);
                $(selector).show();
                self.resizeEvent();
            }
        }
    
        this.hide = function (params, selectorParams, args) {
            //self.log('hide');
            //self.log(JSON.stringify(params));
            if (Array.isArray(params)) {
                    for (var index in params)
                    self.hide(params[index], selectorParams);
            } else {
                //params.container = params.container || self.selector(selectorParams);
                //var selector = self.selector(params);
                var selector = self.selector(self.extend({}, params, selectorParams), {}, true);
                //self.log('selector');
                //self.log(selector);
                $(selector).hide();
                self.resizeEvent();
            }
        } */
    this.matchMedia = function (mediaAction, mediaEvent) {
        const mediaQuery = window.matchMedia(mediaEvent);
        mediaQuery.addListener(function (e) {
            /*  self.log('matchMedia');
                self.log('e');
                self.log(e); */
            if (e.matches) {
                self.do(mediaAction);
                //self.do(mediaAction);
            }
        });
        //self.do(mediaAction);
    };


    /*     this.firebaseEventAction = 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'), '/', '.'); // data('firebase')
                var dbObj = self.docElement(pathString);
    
                var firebaseValue = newObj[element.getAttribute('data-value')]; // TODO: not works for nested nodes
                var container = element.getAttribute('data-selector');
                //var template = element.getAttribute('data-template');
                var template = self.dataStorage.get(element, 'data-template');
    
                if (element.tagName == 'UL') { // se $(this) è un ul
    
                    var liTemplate = template.li;
                    //$(container).empty();
                    element.innerHTML = '';
                    
                    var items = newObj;
                    for (itemKey in items) {
                        var item = items[itemKey];
                        item.key = itemKey;
                        //if (dbPath == 'lessons')
                        //prompt(JSON.stringify(liTemplate));
                        var dbParams = self.replaceItems(liTemplate, item, 'item') || '';
                        // var dbParams = self.replaceProperties(liTemplate, undefined, item) || '';
                        self.li(dbParams, {container: container});
                    }
    
                //} else if ( $(element).is('span') || $(element).is('p')) {
                } else if (element.tagName == 'SPAN' || element.tagName == 'P') {
    
                    //$(element).text(firebaseValue);
                    element.textContent = firebaseValue;
                        
                } else { // se $(this) non è un ul
    
                    if (template.blocks || template.html) {
                        var blocksTemplate = template.html || template.blocks;
                        
                        //$(container).empty();
                        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, {container: container});
                        }
                    }
    
                    if (template.init) self.do(template.init);
    
                }
    
    
            });
        }; */
    /*     self.jsonic = function () {
        } */
    this.addFirebaseTag = function (params) {
        // self.log('addFirebaseTag');
        var dataObj = params.database;

        if (dataObj) {

            dataObj = self.replaceProperties(dataObj);
            /* self.log('dataObj');
            self.log(dataObj); */
            //var path = self.replaceTags({text:params.firebase});
            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 element = self.query(selector);
                var elements = self.queryAll(selector);
                //var elements = document.querySelectorAll(selector);
                if (elements)
                    elements.forEach(function (element, index) {

                        var listenerObj = {
                            path: path
                            //success: dataObj.success
                        };

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

                        //if ($('[data-firebase="'+path+'"]').length == 0) {
                        if (elementsDataPath.length == 0) {
                            listenerObj.action = [
                                /*  {
                                        "firebaseEvent.action": {}  // should use self.get function to parse "self"
                                        // todo: remove "jsonic". The function firebaseEventAction can be stored in self.do (or?)
                                    } */
                            ];

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

                        }

                        //$(selector).attr('data-firebase', path);
                        //$(selector).data('data-template', params);
                        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 (params.on && params.on.data) $(selector).data('ondata', params.on.data); // maestry
                        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);
            }
        }
    };

    this.dbObject = function (path) {
        var pathString = 'self.json.var.db.' + self.replaceAll(path, '/', '.');
        return self.docElement(pathString);
    };

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

    this.extendJsonFromElement = function (p) {
        /* self.log('extendJsonFromElement');
        self.log(p); */

        var params = self.cloneObject(p);

        // properties
        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 }); // obsolete
        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 }); // maybe used in self.text


        // can't be extended "setup" (is relative to the full app) and "on" (is relative to the element)
    };


    this.query = function (selector, element) {
        //self.log('query: '+selector);
        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];



                    /* route.replace(/([^\s])\[/g, function(match, p1) { // -> .class [data-value=...]
                        return p1 + ' ['
                    }); */
                    //self.log('route');
                    //self.log(route);
                    var routeParts = route.match(/(.*):eq\((\d*)\)/);

                    //self.log('routeParts');
                    //self.log(routeParts);
                    if (routeParts) {
                        var routeElement = String(routeParts[1]);
                        var routeIndex = Number(routeParts[2]);

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

                        //if (self.exist(routeElement))
                        element = element.querySelectorAll(routeElement)[routeIndex];

                        /* self.log('routeElement');
                        self.log(routeElement);
                        self.log('routeIndex');
                        self.log(routeIndex); */
                    } else {
                        //if (self.exist(route))
                        if (route.startsWith('.'))
                            route = '.' + self.classSelector(route);
                        element = element.querySelectorAll(route)[0];
                    }
                } else {
                    element = undefined;
                }
            }
            //element.innerHTML = 'test';
            //element.style.display = 'none';
            //self.log(element);
            //if (element && !Array.isArray(element)) element = [element];
            if (!element)
                self.log('can\'t find the element "' + selector + '" ("query" function)');

            return element;
        } else {
            //self.log('function "query" without selector parameters');
            // BUG: many calls from addtag
        }
    };

    /*     this.queryAllTest = function (selector, element) {
            //self.log('query: '+selector);
            if (selector) {
    
                //route = self.replaceAll(route, '[', ' [');
                selector.replace(/([^\s])\[/g, function(match, p1) { // -> .class [data-value=...]
                    return p1 + '['
                });
    
    
                var routes = selector.split(/[ >]+/);
                if (!element) element = document;
    
                self.log('routes');
                self.log(routes);
                
                for (var index in routes) {
                    self.log('index');
                    self.log(index);
    
                    if (element.isConnected) {
        
                        var route = routes[index];
    
                        var routeParts = route.match(/([^:]*):eq\((\d*)\)/);
    
                        self.log('routeParts');
                        self.log(routeParts);
    
                        if (routeParts) {
                            var routeElement = String(routeParts[1]);
                            var routeIndex = Number(routeParts[2]);
                            //if (self.exist(routeElement))
                            element = element.querySelectorAll(routeElement)[routeIndex];
    
                            self.log('routeElement');
                            self.log(routeElement);
                            self.log('routeIndex');
                            self.log(routeIndex);
        
                        } else {
                            //if (self.exist(route))
                            self.log('route');
                            self.log(route);
                            element = element.querySelectorAll(route);
                        }
                    } else {
                        self.log('Not in the DOM '+ selector);
                        self.log('route');
                        self.log(route);
                        element = [];
                    }
                }
                //element.innerHTML = 'test';
                //element.style.display = 'none';
                //if (element && !Array.isArray(element)) element = [element];
                //self.log(element);
    
                return element;
            } else {
                self.log('DOM "query" without selector parameters');
            }
        } */
    /*     this.convertRoute = function (route) {
            var routeElement, routeValue;
            
            route = route.replace(/([^\s])\[/g, function(match, p1) { // -> .class [data-value=...]
                return p1 + ' ['
            });
    
            if (route.indexOf(' ')) {
                var parts = route.split(' ');
                routeElement = parts[0];
                routeValue = parts[1];
            } else {
                routeElement = route
            }
            self.log(routeElement);
            self.log(routeValue);
    
            if (routeElement.startsWith('.'))
                routeQuery = '[class='+routeElement+']';
            else if (routeElement.startsWith('#'))
                routeQuery = '[id='+routeElement+']';
            else
                routeQuery = routeElement + routeValue;
    
            if (routeValue)
                routeQuery += ' ' + routeValue;
    
            return routeQuery;
        } */
    /*
        // https://www.examplefiles.net/cs/1222124
        var querySelectorAllWithEq = function(selector, document) {
            var remainingSelector = selector;
            var baseElement = document;
            var firstEqIndex = remainingSelector.indexOf(':eq(');
        
            while (firstEqIndex !== -1) {
            var leftSelector = remainingSelector.substring(0, firstEqIndex);
            var rightBracketIndex = remainingSelector.indexOf(')', firstEqIndex);
            var eqNum = remainingSelector.substring(firstEqIndex + 4, rightBracketIndex);
            eqNum = parseInt(eqNum, 10);
        
            var selectedElements = baseElement.querySelectorAll(leftSelector);
            if (eqNum >= selectedElements.length) {
                return [];
            }
            baseElement = selectedElements[eqNum];
        
            remainingSelector = remainingSelector.substring(rightBracketIndex + 1).trim();
            // Note - for now we just ignore direct descendants:
            // 'a:eq(0) > i' gets transformed into 'a:eq(0) i'; we could maybe use :scope
            // to fix this later but support is iffy
            if (remainingSelector.charAt(0) === '>') {
                remainingSelector = remainingSelector.substring(1).trim();
            }
        
            firstEqIndex = remainingSelector.indexOf(':eq(');
            }
        
            if (remainingSelector !== '') {
            return Array.from(baseElement.querySelectorAll(remainingSelector));
            }
        
            return [baseElement];
        }; */
    this.queryAll = function (selector, element) {
        //self.log('queryAll: '+selector);
        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)));

                    //elements = elements.concat(self.queryAll(str));
                }
                /* console.log('queryAll');
                console.log(elements); */
                return elements;

            } else {

                /*
                if (selector.indexOf(',')>= 0) {
                    return element.querySelectorAll(selector)
                }
                // TO DO: check why all the rest
                */
                var routes = selector.split(/[ >]+/);
                if (!element) element = document;

                /* self.log('routes');
                self.log(routes); */
                /* self.log('element[0]');
                self.log(element[0]); */
                for (var index in routes) {

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

                    /* self.log('element');
                    self.log(element); */
                    if (element && element.isConnected) {

                        var route = routes[index];

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

                        /* self.log('route');
                        self.log(route);
                        self.log('routeParts');
                        self.log(routeParts); */
                        if (routeParts) {
                            var routeElement = String(routeParts[1]);
                            var routeIndex = Number(routeParts[2]);
                            /* self.log('routeElement');
                            self.log(routeElement);
                            self.log('routeIndex');
                            self.log(routeIndex);  */
                            if (routeElement.startsWith('.'))
                                routeElement = '.' + self.classSelector(routeElement);

                            //if (self.exist(routeElement))
                            element = element.querySelectorAll(routeElement)[routeIndex];

                            if (index == (routes.length - 1) && element) {
                                /* console.log('index:'+index);
                                console.log('routes.length:'+routes.length); */
                                element = [element];
                            }

                        } else {
                            //if (self.exist(route))
                            if (route.startsWith('.'))
                                routeElement = '.' + self.classSelector(route);

                            /* self.log('route');
                            self.log(route); */
                            element = element.querySelectorAll(route);
                        }
                    } else {
                        //self.log('!element.isConnected');
                        element = [];
                    }
                }

                //if (element && !Array.isArray(element)) element = [element];
                //if (element && !element.length) element = [element];
                if (!element)
                    self.log('No element selected by ' + selector);
                //else
                //    if (!Array.isArray(element))
                //        element = [element];
                return element;
            }
        } else {
            console.log('%cDOM queryAll requires selector argument', 'color: orange');
            //self.log('DOM "query" without selector parameters');
        }
    };

    /* this.queryAll = function (selector, element) {
        //self.log('query: '+selector);
        if (selector) {
            var routes = selector.split(/[ >]+/);
            if (!element) element = document;
            
            for (var index in routes) {
                var route = routes[index];

                //onsole.log('route');
                //self.log(route);

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

                //self.log('routeParts');
                //self.log(routeParts);

                if (routeParts) {
                    var routeElement = String(routeParts[1]);
                    var routeIndex = Number(routeParts[2]);
                    //if (self.exist(routeElement))
                        element = [element.querySelectorAll(routeElement)[routeIndex]];
                } else {
                    //if (self.exist(route))
                        element = element.querySelectorAll(route);
                }
            }
            //element.innerHTML = 'test';
            //element.style.display = 'none';
            //if (element && !Array.isArray(element)) element = [element];
            //self.log(element);

            return element;
        } else {
            self.log('DOM "query" without selector parameters');
        }
    } */
    /* var querySelectorAllWithEq = function(selector, document) {
        var remainingSelector = selector;
        var baseElement = document;
        var firstEqIndex = remainingSelector.indexOf(':eq(');
        
        while (firstEqIndex !== -1) {
            var leftSelector = remainingSelector.substring(0, firstEqIndex);
            var rightBracketIndex = remainingSelector.indexOf(')', firstEqIndex);
            var eqNum = remainingSelector.substring(firstEqIndex + 4, rightBracketIndex);
            eqNum = parseInt(eqNum, 10);
        
            var selectedElements = baseElement.querySelectorAll(leftSelector);
            if (eqNum >= selectedElements.length) {
            return [];
            }
            baseElement = selectedElements[eqNum];
        
            remainingSelector = remainingSelector.substring(rightBracketIndex + 1).trim();
            // Note - for now we just ignore direct descendants:
            // 'a:eq(0) > i' gets transformed into 'a:eq(0) i'; we could maybe use :scope
            // to fix this later but support is iffy
            if (remainingSelector.charAt(0) === '>') {
            remainingSelector = remainingSelector.substring(1).trim();
            }
        
            firstEqIndex = remainingSelector.indexOf(':eq(');
        }
        
        if (remainingSelector !== '') {
            return Array.from(baseElement.querySelectorAll(remainingSelector));
        }
        
        return [baseElement];
        }; */
    this.jitcss = function (classes) {
        // https://tailwindcss.com/docs/just-in-time-mode
        if (classes.indexOf('-[') > -1) {
            let classesArr = classes.split(' '); // change to one or more spaces

            // (sm\:|md\:|lg\:|xl\:|2xl\:)?
            self.log('jitcss');

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

        /* sm:w-1
        
        .w-1 {
            width: 1em;
        }
        
        @media (min-width: 640px) {
            .sm\:w-1 { width: 1em }
        }
            */
    };

    this.compileClasses = function (string) {

        var classesString = string;

        // classes shortcuts
        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]);
                }
            }
        }

        // dynamic classes
        if (string.indexOf('(') >= 0) {
            //self.log('compileClasses');
            classesString = '';
            let classesArr = string.split(' ');
            for (cl of classesArr) {
                /* self.log('cl');
                self.log(cl); */
                if (cl.indexOf('(') >= 0 && cl.endsWith(')')) {
                    let classArr = cl.split('(');
                    classArr[1] = classArr[1].slice(0, -1); // remove )







                    //let par = [...cl.match(new RegExp('([^\(]+)\(([^\)]+)\)$'))]; // doesn't work
                    /* self.log('classArr');
                    self.log(classArr);
                    self.log('classArr[1]');
                    self.log(classArr[1]);
                    self.log('self.js(classArr[1])');
                    self.log(self.js(classArr[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;

    };

    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); // from 0.9.6: must stay before document.createElement






        /* console.log('append');
        console.log('selector:'+selector);
        console.log('container:'+container);
        console.log('newSelector:'+newSelector); */
        var newElements = [];

        //if (containerElement)
        //containerElements.forEach(function (containerElement, index) {
        if (containerElement) {

            if (!params) params = {};

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

            containerElement.appendChild(element);

            if (params.attr) {
                // shortcuts (windy property not in twind) and dynamic classes with js condition
                if (params.attr.class) {
                    // replace shortcuts and dynamic classes: class(condition)
                    params.attr.class = self.compileClasses(params.attr.class);
                }
            }

            //if (container) element.setAttribute('data-selector', self.selector(params, selectorParams));
            //if (container) element.setAttribute('data-selector', newSelector); // from 0.9.6. removed 1.0.12
            // for IDE
            self.dataStorage.set(element, 'code', params);
            self.dataStorage.set(element, 'key', params.key);

            /*  if (params.attr && params.attr.class)
                    self.jitcss(params.attr.class); */
            newElements.push(element);

            if (params.attr) self.setAttributes(newElements, params.attr);
            if (params.style) self.css(params, newSelector); // TO DO: deprecated / to be removed;








            /* // attributes TO DO: move up / call self.attr
            if (params.attr)
                for (var attrId in params.attr)
                    self.attribute(newElements, attrId, self.replaceProperties(params.attr[attrId]));
                    //self.log(self.replaceProperties(params.attr[attrId]));
                    //$(selector).attr(attrId, params.attr[attrId]); */
        }

        //});
        return newElements;

        /* for (let element of elements) {
            element.appendChild(string);
        } */
    };

    /* this.newSelector = function (params, selectorParams) {
        var container = self.selector(self.extend({}, params, selectorParams), undefined, true);
        if (container) {
            var selectorObj = {
                container: container,
                //class: params.class,
                //value: params.value,
                //'data-value': params['data-value']
            }; // , selectorParams);
            self.log('container');
            self.log(container);
            var index = document.querySelectorAll(container).length;
            if (index > 1) selectorObj.index = index-1;
            self.log('newSelector');
            self.log(selectorObj);
            return selectorObj;
        } else {
            self.log('container undefine in newSelector');
            self.log('params');
            self.log(params);
            self.log('selectorParams');
            self.log(selectorParams);
        }
    }
*/
    /*
        this.editor = function (params, selectorParams, args) {
            self.log('editor');
            self.log('params');
            self.log(params);
            var selector = self.selector(selectorParams, undefined, true); // self.selector(params, undefined, true) ||
            if (selector) {
            var element = self.query(selector);
    
                if (element) {
            
                    if (!self.quillElements[selector]) {
                        self.quillElements[selector] = new Quill(element, {
                            modules: params.modules || { // or default modules
                                toolbar: [
                                [{ header: [1, 2, false] }],
                                ['bold', 'italic', 'underline'],
                                ['image', 'code-block']
                                ]
                            },
                            // scrollingContainer: '#scrolling-container',
                            placeholder: params.placeholder || 'Write here',
                            theme: params.theme || 'bubble'
                        });
                    }
    
                    if (self.quillElements[selector]) {
    
                        if (params.setContents) {
                            var contents = self.replaceProperties(params.setContents, args);
                            self.quillElements[selector].setContents(contents);
                        }
    
                        if (params.getContents) {
                            self.quillElements[params.getContents].getContents();
                        }
    
                        if (params.on) {
    
                            for (var eventId in params.on) {
                                self.editorChangeFunctions[selector] = params.on;
    
                                if (params.on[eventId]) {
    
                                    self.quillElements[selector].on(eventId, function(delta, oldDelta, source) {
                                        var action = self.editorChangeFunctions[selector][eventId];
                                        var actionWithResult = self.replaceResult(action, delta);
                                        //self.do(actionWithResult);
                                        self.do(actionWithResult);
                        
                                    });
                                }
                            }
                        }
                    }
                } else {
                    self.log('can\'t find the element "'+ selector + '" ("editor" function)');
                    self.log(params);
                }
            } else {
                self.log('the "editor" function has a wrong selector');
                self.log(params);
            }
    
        }
    */
    this.do = this.action = this.execute = function (params, selectorParams, args) {
        /* self.log('params');
        self.log(params);
        self.log('selectorParams');
        self.log(selectorParams);
        self.log('args');
        self.log(args); */

        if (params !== undefined && params !== null) { // empty string is ok

            /*
            // problematic for nested {args} / {selector}
            if (selectorParams && typeof selectorParams == 'string') {
                params = self.replaceProperty(params, 'selector', selectorParams);
            }
            if (args) {
                params = self.replaceProperty(params, 'args', args);
            }
            */
            if (typeof params == 'string')
                params = self.replaceProperties(params, args);

            let pluginsRequired = self.pluginsRequired(params);
            /* self.log('pluginsRequired');
            self.log(pluginsRequired); */
            if (pluginsRequired.length > 0)
                //let params = [params, args, selectorParams];
                self.pluginsLoader(pluginsRequired, self.do, [params, selectorParams, args]);
            else {

                if (Array.isArray(params))
                    for (var obj of params)
                        self.do(obj, selectorParams, args); // -> execute dopo aver portato qui pluginsLoaded
                else if (typeof params == 'object') {

                    /* if (params.after) {
                        let paramsAfter = params.after;
                        let paramsWithoutAfter = self.cloneObject(params);
                        delete paramsWithoutAfter.after;
                        self.timer({
                            after: paramsAfter,
                            do: paramsWithoutAfter
                        });
                    } else {
    */
                    if (params.if == undefined || self.if(params.if)) { // || Boolean(self.js(params.if))) { // , args
                        /* console.log('IF---------------');
                        console.log(params.if);
                        console.log(params); */
                        if (self.only(params.roles, params.user)) {

                            //if (!params) params = {};
                            //var newContainer; //self.newSelector(params, selectorParams); // {container: selector}
                            // Add an element in the DOM
                            if (params.tag) {

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

                                /* self.log('selector');
                                self.log(selector); */
                                self.extendJsonFromElement(params); // or selectorParams





                                //newContainer = {selector: selector};
                                //if (1) //self.user('uid') == 'L0y20rjPp6a6ejqVuDXuTWCU2rH3') //'ocXuFBV4aWcy7KZMAmi6mICFi872')
                                // create new html element
                                var newElements = self.createElement(params, selectorParams);

                                /* if (params.key == 'text')
                                    //let element = self.query(self.selector(params.selector));
                                    newElements.innerText = self.text(nested); */
                                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 {

                                    // nested html tags as property
                                    for (var par in params) //  && (par !== 'attr')
                                        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 });

                            }


                        } // end of self.only

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

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

        }
    };

    this.callback = function (params) {
        /* console.log('callback');
        console.log(params); */
        // {do, find, replace}
        if (params) {
            var path = params.to;
            if (params.var) path = 'var ' + params.var; // TO DO: remove
            let selector;
            let element = params.element; // elementToSelector ?
            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);
        }
    };

    this.nested = function (params) {
        /* console.log('nested');
        console.log(params); */
        // check plugins required
        let pluginsRequired = self.pluginsRequired(params.key);
        if (pluginsRequired.length > 0) {
            self.pluginsLoader(pluginsRequired, self.nested, [params]);
        } else {



            // TO DO: regular expression like in this.element (should include setup and also :)
            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 ')) {
                // TO DO: remove this option. Is too ambiguous for debug -> use set
                // deprecated from 1.0.4
                console.log('%cdeprecated key: "' + params.key + '". Use "set key": "value"}', 'color:orange');

                // Data setting prefix
                // should be removed and replaced with the method "set" to improve the compatibility
                // with the other possible keys (js functions, jsonic methods, html tags ex. <data class=""></data>)
                // https://www.w3schools.com/tags/tag_data.asp
                self.setData({ key: params.key, value: params.obj });

            } else {

                // CHECK IF KEY IS METHOD
                //console.log('check if is a method');
                var method = self.element({ path: params.key, root: self.methods });

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

                    /* if (params.key == 'swiper init') {
                        self.log('method: ' + params.key);
                        self.log('methodArgs');
                        self.log(methodArgs);
                        self.log('params.selector');
                        self.log(params.selector);
                        self.log('-----------');
                    } */
                    //methodArgs = self.replaceProperty(methodArgs, 'selector', params.selector);
                    method(methodArgs, params.selector, params.args);


                    /* } else if (params.key == 'set') { // TO DO: REMOVE
    
                        // JS METHOD (old)
    
                        var actionObj = {};
                        actionObj[params.key] = params.obj;
                        self.do(actionObj, params.selector); */
                    /* } else if (nodes.functions.indexOf(params.key) >= 0) { // TO DO: REMOVE
    
                        // JS METHOD (old)
    
                        var actionObj = {};
                        actionObj[params.key] = params.obj;
                        //self.do(actionObj, undefined, params.selector);
                        self.do(actionObj, params.selector); */
                } else {

                    // CHECK IF KEY IS A PARTS
                    //console.log('check if is a part');
                    // shortcut: content
                    let nested;
                    let part;

                    var jsonicPart = false;
                    if (self.json.parts) { // && par.match(/[.>]+/)) 
                        part = self.element({ path: self.replaceAll(params.key, ':', '.'), root: self.json.parts });
                        /* if (par.indexOf(':')>0 && !part) {
                            jsonicPart = true;
                            self.firebaseGetPart({localPath:localPath, container: params.selector, arg:params.obj});
                        } */
                    }

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

                        /* console.log('params');
                        console.log(params); */
                        var partObj = params.obj;
                        //nested = self.replaceProperty(part, 'value', params.obj); // obsolete?
                        //nested = self.replaceProperty(part, 'arg', partObj); // obsolete
                        //if (typeof partObj == 'string' && partObj.indexOf('{') >= 0) { // obsolete?
                        // TO DO: this should be already inside self.do
                        partObj = self.replaceProperties(partObj);
                        // for backward compatibility can be partObj, null, true) // true is removeUndefined
                        //}
                        /* console.log('partObj');
                        console.log(partObj);
                            */
                        //var partSelector = params.selector || partObj.selector;
                        // TO DO: Check this
                        // seems a workaround for fantacards/ui:qrcode
                        // parts can receive the parameters {selector:..., setup:...}
                        partObj.selector = partObj.selector || params.selector;

                        // TO DO: deprecated fantacards:setPlayer
                        if (partObj.setup) partObj = partObj.setup;
                        // overwrites partObj.selector but execute receive params.selector
                        // TO DO: replaceProperty should be improved to get also sub values like {arguments:on.success}
                        //nested = self.replaceProperty(part, 'setup', partObj); // {setup} -> {arguments}
                        //nested = self.replaceProperty(nested, 'arguments', partObj);
                        //nested = self.replacePropertyWithPrefix(part, 'setup:', partObj); // {setup} -> {arguments}
                        //nested = self.replacePropertyWithPrefix(nested, 'arguments:', partObj);// to tix
                        nested = self.replacePropertyWithPrefix(part, 'setup', partObj); // {setup} -> {arguments}
                        nested = self.replacePropertyWithPrefix(nested, 'arguments', partObj); // to tix


                        self.extendJsonFromElement(nested); // or selectorParams

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

                        /*
                        self.log('nested');
                        self.log(nested);
                        
                        self.log('part key');
                        self.log(params.key);
                        self.log('part obj');
                        self.log(part);
                        */
                    } else { // if (!jsonicPart) {  // should be if (par !== 'on', 'attr', 'style', 'items/data')
                        // params.key is not var/data, a method, a part
                        //var jsFunction = self.element({path: params.key, root: self.functions});
                        if (self.json && self.json.functions && params.key && self.json.functions[params.key]) {
                            //if (self.json.functions[params.key]) {
                            self.log('FUNCTION: ' + params.key, 'grey');


                            /* self.log('pluginsRequired');
                            self.log(pluginsRequired); */
                            let args = self.replaceProperties(params.obj, undefined, true); // undefined properties -> ''




                            /* console.log('%cargs', 'color:red');
                            console.log(args); */
                            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.functions[params.key], args);
                                    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 });
                                    //self.functions[params.key](...args);
                                }
                            } else {
                                /* let args = self.replaceProperties(params.obj);
                                if (!args || !Array.isArray(args)) args = [args]; */
                                self.log('Function without plugin required: ' + params.key, 'grey');
                                self.function({ name: params.key, arguments: args }); // TO DO: check why here it can't have []

                                //self.functions[params.key](...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)); // undefined properties -> ''

                                //windowFunction(self.replaceProperties(params.obj));
                            } else if (windowFunction !== undefined) {
                                self.log('window.' + params.key + ' is not a function', 'red');
                                //self.log(params.key + ' not found', 'red');
                                // use set for window assignment
                                /*
                                self.element({path: params.key, value:params.obj, root: window}); */
                            } else {
                                // NESTED HTML
                                // Need to be improved. with nestedAction and nestedTag
                                // here we can search only nestedAction
                                // and from html we can search only for nestedTag's
                                //nested = params.obj; // removed in 1.0.1
                                // if is a nested html params.selector is required
                                // impossible to understand if we are inside an html method
                                // because the html tags can be nested and with any name
                                if (params.selector) { // added in 1.0.1
                                    self.log('TAG: ' + params.key, 'grey');
                                    // should be also in this.html function if params is an array
                                    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); // tagParam = params.key

                                        //if (nested.database) self.addFirebaseTag(self.extend({}, nested, params.selector));
                                    }
                                } else {
                                    self.log(params.key + ' not found', 'red');
                                }
                            }
                        }
                    }
                }
            }

        }
    };

    this.setData = function (params) {
        //self.log('setData');
        let setObj = {};
        setObj[params.key] = params.value;
        self.set(setObj);

        /* if (params.path.indexOf('{') >= 0)
            params.path = self.replaceProperties(params.path);

        if (typeof params.value == 'string' && params.value.indexOf('{') >= 0)
            params.value = self.replaceProperties(params.value);

        self.element({path: params.path, value: params.value}); */
        /* self.log(params.path);
        self.log(self.element({path: params.path})); */
    };

    this.partContainers = {};



    this.pluginsRequiredByTag = function (params, tagKey) {
        // self.log('pluginsRequiredByTag');
        /* self.log('params');
        self.log(params);
        self.log('par');
        self.log(par);  */

        if (self.json.resources) {

            // analize function object or array of objects
            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') {
                // for example: "tagName": "string" where tagName requires a plugin
            } else if (typeof params == 'object') {

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

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

    /*
        
        'locale': {
            unit: month, weekday
            format: long, short, narrow
            index: 1,
            lang: page:language
    */
    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');
        }

    };



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

    this.weekday = function (params, weekdayFormat) {
        //weekdayFormat = weekdayFormat || 'long';
        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;

        //if (weekdayFormat == 'short') result = result.substring(0,3);
        return result;

    };

    /*     this.array = function (params, args) { // TO DO: to be removed
            self.log('array');
            self.log(params);
    
            if (Array.isArray(params))
                return params;
            else if (typeof params == 'string')
                return self.docElement(params);
            else if (typeof params == 'object') {
                if (params && Object.keys(params).length > 0) { // actionObj !== {}
                    var actionKey = Object.keys(params)[0]; // -> for
                    var actionFunction = self.docElement('Array.prototype.' + actionKey); // method in window
                    var actionValue = params[actionKey];
    
                    //actionValue = self.compile(actionValue, args); // array -> undefined
                    if (!Array.isArray(actionValue))
                        actionValue = [actionValue];
    
                    if (actionFunction) {
                        if (typeof actionFunction == 'function')
                            return actionFunction.call(actionValue);
                        else
                            return actionFunction;
                    } else {
                        self.log('unknown window function');
                        return params; // return the same object
                    }
                } else {
                    self.log('empty function');
                    //self.log(actionObj);
                    return undefined
                }
            }
        } */
    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]; // -> for 
                    var actionFunction = self.docElement(actionKey); // method in window
                    var actionValue = params[actionKey];
                    //actionValue = self.compile(actionValue, args);
                    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; // return the same object
                    }
                } else {
                    //self.log('empty function');
                    //self.log(params);
                    return undefined;
                }
            }
        }

        /*
        var methodArgs = self.compile(actionValue, args);

        if (value !== undefined) {
        
            if (!Array.isArray(methodArgs))
                methodArgs = [methodArgs];

            self.log('params');
            self.log(params);

            for (let method of methodArgs) {
                for (let key in method) {
                    if (key !== 'args') {
                        self.log('key');
                        self.log(key);
                        if (value[key])
                            if (method[key] && method[key] !== '') { // va aggiunto? ->  && value[key](method[key]) method[key] !== undefined &&
                                methodArgs = method[key];
                                self.log('methodArgs');
                                self.log(methodArgs);

                                if (!Array.isArray(methodArgs))
                                    methodArgs = [methodArgs];
                                for (let index in methodArgs) {
                                    let methodArg = self.compile(methodArgs[index], args);
                                    //if (typeof methodArg == 'string' && methodArg !== '' && methodArg == Number(methodArg)) // can be in compile
                                    methodArgs[index] = methodArg; // Number(methodArg)
                                }
                                //try {
                                //} catch (error) {
                                //    self.log('Dayjs function error');
                                if (typeof value[key] == 'function')
                                    value = value[key](...methodArgs); // ES6 way
                            } else
                                if (typeof value[key] == 'function')
                                    value = value[key]();
                                else
                                    value = value[key];
                    } else {
                        self.log('Method ' + key + ' in ' + methodName + ' not exist');
                    }
                }
            }

        } else {
            self.log('method '+ methodName +' undefined');

        }
        */
    };

    this.Number = function (params, args) {
        self.log('Number');
        return self.method({ "Number": params }, args);
    };

    this.String = function (params, args) {
        self.log('String');
        return self.method({ "String": params }, args);

        // jsonic.compile({String:{args:'1',padStart:[2, '0']}})
        /*
                "text": {
                    "String": {
                        "args": "{hour}",
                        "padStart": [
                            "0",
                            2
                        ]
                    }
                }
        */
    };

    this.Math = function (params, args) {
        return self.method(params, args, 'Math');

        /* if (!Array.isArray(params))
            params = [params];

        for (let method of params) {
            for (let key in method) {
                self.log(key);
                if (method[key] && method[key] !== '' && value[key](method[key])) {
                    //methodArgs = self.compile(method[key]);
                    methodArgs = method[key];
                    if (!Array.isArray(methodArgs))
                        methodArgs = [methodArgs];
                    for (let index in methodArgs) {
                        let methodArg = self.compile(methodArgs[index]);
                        if (typeof methodArg == 'string' && methodArg !== '' && methodArg == Number(methodArg)) // can be in compile
                            methodArgs[index] = Number(methodArg);
                    }
                    value = value[key](...methodArgs); // ES6 way
                } else
                    value = value[key]();
            }
        }

        return value; */
    };


    this.dayjs = function (params, args) {

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

        /* self.log('dayjs');

        var value = dayjs();
        if (params.time)
            value = dayjs(params.time);
        //value = self.compile(value);

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


        for (let method of params) {
            for (let key in method) {

                if (key == 'time') {
                } else if (value[key]) {
                    if (method[key] && method[key] !== '' && value[key](method[key])) {
                        //methodArgs = self.compile(method[key]);
                        methodArgs = method[key];
                        if (!Array.isArray(methodArgs))
                            methodArgs = [methodArgs];
                        for (let index in methodArgs) {
                            let methodArg = self.compile(methodArgs[index]);
                            if (typeof methodArg == 'string' && methodArg !== '' && methodArg == Number(methodArg)) // can be in compile
                                methodArgs[index] = Number(methodArg);
                        }
                        value = value[key](...methodArgs); // ES6 way
                    } else
                        value = value[key]();
                } else {
                    try {
                    } catch (error) {
                        self.log('Dayjs function error');
                        self.log(error);
                        self.log('key');
                        self.log(key);
                        self.log('method[key]');
                        self.log(method[key]);
                    }

                }
            }
            self.log('value');
            self.log(value);
        }
        return value; */
    };

    this.moment = function (params, args) {

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

        /* self.log('moment');

        var value = moment();
        if (params.time)
            value = moment(params.time);

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

        for (let method of params) {
            for (let key in method) {
                if (key !== 'time') {
                    if (key == 'add') {
                        value = value.add(Number(method[key].value), method[key].unit);
                    } else if (key == 'subtract') {
                        value = value.add(Number(method[key].value), method[key].unit);
                    } else if (method[key] && method[key] !== '' && value[key](method[key])) {
                        value = value[key](method[key]);
                    } else {
                        self.log('Moment function error: ');
                        self.log('key');
                        self.log(key);
                        self.log('method[key]');
                        self.log(method[key]);
                    }
                }
            }
            self.log('value');
            self.log(value);
    
        }
        return value;
    */
    };


    /* const methodsList = ['day, format', 'year', 'years', 'quarter', 'dayOfYear', 'month', 'months', 'date', 'minute', 'minutes', 'hour', 'hours', 'second', 'seconds', 'millisecond', 'milliseconds', 'weekday', 'weekdays', 'isoWeekday', 'weekYear', 'isoWeekYear', 'weeksInYear', 'week', 'isoWeek', 'set', 'max', 'min', 'local', 'parseZone', 'unix', 'utc', 'utcOffset', 'zone', 'isValid', 'invalid', 'valueOf', 'startOf', 'endOf', 'fromNow', 'toNow', 'from', 'to', 'calendar', 'diff', 'daysInMonth', 'toDate', 'toArray', 'toJSON', 'toString', 'toISOString', 'toObject', 'inspect', 'isBefore', 'isSame', 'isAfter', 'isSameOrBefore', 'isSameOrAfter', 'isBetween', 'isDST', 'isLeapYear', 'isDate', 'locale', 'localeData', 'lang', 'monthsShort', 'weekdaysShort', 'weekdaysMin', 'defineLocale', 'updateLocale', 'monthsParse', 'weekdaysParse', 'longDateFormat', 'isPM', 'meridiem', 'relativeTime', 'pastFuture', 'ordinal', 'preparse', 'postformat', 'invalidDate', 'firstDayOfWeek', 'firstDayOfYear', 'duration', 'clone', 'humanize', 'asMilliseconds'];
    for (var method in methodsList) {
        if (params[method]) value = value[method](params[method]);
    } */
    /*  for (let param in params) {
            if (param !== 'time') {
                if (typeof value == 'object') {
                    //if (param == 'day') value = value.day(Number(params[param]));
                    if (param == 'add') {
                        self.log('params.add.value');
                        self.log(params.add.value);
                        self.log('params.add.unit');
                        self.log(params.add.unit);
                        self.log('value');
                        self.log(value);
                        value.add(Number(5), params.add.unit);
                        self.log('value.format(H)');
                        self.log(value.format('H'));
                    }
                    else if (param == 'subtract') value.add(Number(params.subtract.value), params.subtract.unit);
                    else if (value[param] && value[param](params[param]))
                        value = value[param](params[param]);
                    else
                        self.log('Moment function error: '+ params[param]);
                }
                self.log('value');
                self.log(value);
            }
        }
*/
    /* this.addNestedTag = function (params, selectorParams) {
        var selector = self.selector(self.extend({}, params, selectorParams));
        // remove tag params
        var nestedParams = self.cloneObject(params);
        var tagParams = ['tag', 'container', 'id', 'class', 'value', 'data-value', 'style', 'attr', 'title', 'text', 'animation', 'action', 'on', 'src', 'media', 'color', 'background', 'width', 'height', 'margin', 'padding', 'firebase'];
        for (var par in nestedParams) {
            if (tagParams.indexOf(par) >= 0)
                delete nestedParams[par]
        }

        // serach for nested params
        var mainTags = ['div', 'ul', 'p', 'span', 'a', 'button', 'input', 'textarea', 'iframe', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'figure', 'figcaption', 'svg', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'icons'];
        for (var par in nestedParams) {
            self.log(par + ' NESTED IN ' + selector);
            if (mainTags.indexOf(par) >= 0) {
                self.htmlTag(nestedParams[par], {container: selector}, mainTags[mainTags.indexOf(par)]);
            }
        }
    } */
    this.dynamicCss = function (selector, property, value) {
        if (value.indexOf('{') >= 0) {
            var resizeData = self.dataStorage.get(element, "resize");
            //var resizeData = $(selector).data('resize');
            if (!resizeData) resizeData = {};
            resizeData[property] = value;
            var resizeData = self.dataStorage.set(element, "resize", resizeData);
            //$(selector).data('resize', resizeData);
            if (!self.resizeActions[selector]) self.resizeActions[selector] = {};
            self.resizeActions[selector][property] = value;
            var result = self.replaceProperties(value) || '';
            //$(selector).css(property, result); // al cambio di pagina su this.page applichiamo self.resizeEvent()
            /*                     self.log('----------------------');
                        self.log('dynamicCss');
                        self.log('selector');
                        self.log(selector);
                        self.log('property');
                        self.log(property);
                        self.log('value');
                        self.log(value);
                        self.log('result');
                        self.log(result);
                        self.log('----------------------');
            */
        } else {
            var style = {};
            style[property] = value;
            self.css({
                style: style
            });
            //$(selector).css(property, value);
        }
    };

    /*  this.calc = function (selector, property, value) {
        if (value.indexOf('{')>= 0) {
            //var calcFunction = String(element.match(/calc\(([^\)]*)\)/)[0]);
            var result = self.replaceProperties(value) || '';
            return result;
        } else {
            return value;
        }
    } */
    /* this.hTag = function (params, selectorParams, index) { // this.img already exist
        if (Array.isArray(params)) {
                for (var index in params)
                self.hTag(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.hTag(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {text: params}};
                params.tag = 'h'+index;
                var paramsReplaced = self.replaceProperties(params);
                self.addTag(paramsReplaced, selectorParams);
            }
        }
    } */
    /* this.img = function (params, selectorParams, args) {
        //self.log('img');
        var tagParam = 'img';
        var mainParam = 'src';

        if (Array.isArray(params)) {
                for (var index in params)
                self.img(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.img(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {src: params}};
                params.tag = 'img';
                //if (params.media) params.src = '{media:' + params.media + '}'; // can go under replaceProperties
                var paramsReplaced = self.replaceProperties(params, args);
                //var container = params.container || self.selector(selectorParams);

                self.addTag(paramsReplaced, selectorParams);
            }
        }
    } */
    /* this.small = function (params, selectorParams) { // bootstrap
        var mainParam = 'text';
        if (Array.isArray(params)) {
                for (var index in params)
                self.small(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.small(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {text: params}};
                params.tag = 'small';
                self.addTag(params, selectorParams);
                if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
            }
        }
    } */
    /* this.button = function (params, selectorParams) { // bootstrap
        var mainParam = 'text';
        if (Array.isArray(params)) {
                for (var index in params)
                self.button(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.button(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {text: params}};
                params.tag = 'button';
                self.addTag(params, selectorParams);
                if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
            }
        }
    } */
    /*
        this.input = function (params, selectorParams) {
            var tagParam = 'input';
            var mainParam = 'text';
            if (Array.isArray(params)) {
                    for (var index in params)
                    self.input(params[index], selectorParams);
            } else {
                if (params.items) {
                    var itemsArray = params.items;
                    delete params.items;
                    for (var index in itemsArray) {
                        self.input(self.replaceItems(params, itemsArray[index]), selectorParams);
                    }
                } else {
                    self.addTag(self.createTagParams(params, tagParam, mainParam), selectorParams);
                    if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
                }
            }
        }
    
        this.span = function (params, selectorParams) {
            var mainParam = 'text';
            if (Array.isArray(params)) {
                    for (var index in params)
                    self.span(params[index], selectorParams);
            } else {
                if (params.items) {
                    var itemsArray = params.items;
                    delete params.items;
                    for (var index in itemsArray) {
                        self.span(self.replaceItems(params, itemsArray[index]), selectorParams);
                    }
                } else {
                    params.tag = 'span';
                    // qui ci andrebbe replaceProperties
                    if (typeof params == 'string') {params = {text: params}};
                    self.addTag(params, selectorParams);
                    if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
                }
            }
        }
            */
    /*  this.a = function (params, selectorParams) {
        var mainParam = 'text';
        var tagParam = 'a';
        if (Array.isArray(params)) {
                for (var index in params)
                self.a(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.a(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {text: params}};
                params.tag = 'a';
                self.addTag(params, selectorParams);
                if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
            }
        }
    } */
    /* this.p = function (params, selectorParams) {
        var mainParam = 'html';
        if (Array.isArray(params)) {
                for (var index in params)
                self.p(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.p(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {html: params}};
                params.tag = 'p';
                self.addTag(params, selectorParams);
                if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
            }
        }
    } */
    /* this.lottie = function (params, selectorParams, args) {
        self.htmlTag(params, selectorParams, 'lottie-player', 'src');
    } */
    //this.lottie = function (params, selectorParams, args) {
    //    self.htmlTag(params, selectorParams, 'lottie-player', 'src');
    /* self.log('lottie');
    var mainParam = 'src';
    var tagParam = 'lottie-player';
    if (Array.isArray(params)) {
            for (var index in params)
            self.lottie(params[index], selectorParams);
    } else {
        if (params.items) {
            var itemsArray = params.items;
            delete params.items;
            for (var index in itemsArray) {
                self.lottie(self.replaceItems(params, itemsArray[index]), selectorParams);
            }
        } else {
            var container = params.container || self.selector(selectorParams);
            // qui ci andrebbe replaceProperties
            //this.alert(self.selector(self.extend({}, params, selectorParams)));
            self.addTag(self.createTagParams(params, tagParam, mainParam), {container: container});
            //if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
        }
    } */
    //}
    /* this.iframe = function (params, selectorParams) {
        var mainParam = 'src';
        if (Array.isArray(params)) {
                for (var index in params)
                self.iframe(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.iframe(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (typeof params == 'string') {params = {src: params}};
                params.tag = 'iframe';
                var paramsReplaced = self.replaceProperties(params);
                self.addTag(paramsReplaced, selectorParams);
            }
        }
    } */
    /*  this.textarea = function (params, selectorParams) {
        if (Array.isArray(params)) {
                for (var index in params)
                self.textarea(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.textarea(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                if (!params.tag) params.tag = 'textarea';
                //params.container = params.container || self.selector(selectorParams);
                self.addTag(params, selectorParams);
            }
        }
    } */
    /*
    this.div = function (params, selectorParams) {
        if (params.firebase) {
            self.log('==========');
            self.log('div');
            self.log(params);
            self.log('==========');
        }

        var tagParam = 'div';
        var mainParam = 'text';
        if (Array.isArray(params)) {
                for (var index in params)
                self.div(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.div(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                self.addTag(self.createTagParams(params, tagParam, mainParam), selectorParams);
                if (params.firebase) self.addFirebaseTag(self.extend({}, params, selectorParams));
            }
        }
    }

    this.ul = function (params, selectorParams) {
        if (Array.isArray(params)) {
                for (var index in params)
                self.ul(params[index], selectorParams);
        } else {
            if (params.items) {
                var itemsArray = params.items;
                delete params.items;
                for (var index in itemsArray) {
                    self.ul(self.replaceItems(params, itemsArray[index]), selectorParams);
                }
            } else {
                params.tag = 'ul';
                    
                var paramsReplaced = self.replaceProperties(params);
                self.addTag(paramsReplaced, selectorParams);
                if (params.firebase) self.addFirebaseTag(params);
            }
        }
    }
    */
    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;
                //var itemsArray = 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';
                //params.container = params.container || self.selector(selectorParams);
                self.do(params, selectorParams);
            }
        }
    };

    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();
                });
            }
        }
    };

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

    /* this.element with "selector" and "method": (remove/dispatchEvent) */
    this.html = function (params, selectorParams, args) {
        /* self.log('html');
        self.log('params');
        self.log(params);
        self.log('selectorParams');
        self.log(selectorParams);   */

        // TO DO: update
        // html properties can be only:
        // selector, attr, html, on, for, if
        // can't be:
        // animate (should go inside on: init?) 
        // tags (div etc) because it should go inside html
        // do
        // functions, window functions...
        // text should be alternative to html tag. if inside html, it should give an error
        // TO DO: verificare se args viene portato agli elementi annidati (non andava con svg) 
        if (params) {

            if (Array.isArray(params)) {

                // TO DO: shoud check if params require plugins
                for (var obj of params)
                    self.html(obj, selectorParams, args);
            } else {

                //if (params.container == '#weatherReport') alert()
                //var container = (selectorParams) ? self.selector(selectorParams) : params.container; // unused
                //var selector = self.selector(self.extend({}, params, selectorParams), undefined, true); //), undefined, true);
                /*
                            self.log('selector');
                            self.log(selector); */
                var container = self.selector(params.selector || params.container) || self.selector(selectorParams);
                //var container = self.selector(selectorParams, undefined, true) || self.selector(params, selectorParams, true);
                /* self.log('container');
                self.log(container); */
                /*                 var element = self.query(container);
                                self.log('element');
                                self.log(element);
                    */
                if (container) {
                    var element = self.query(container);

                    // empty or not? probably yes (like jquery) but we need append/prepend method
                    // append only all the items in the array
                    // TO DO: queryAll -> elements
                    if (element) {

                        if (typeof params == 'string') {

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

                            //$(container).append(paramsCompiled); // should be .html not append
                            //element.appendChild(paramsCompiled);
                            if (paramsCompiled) {
                                if (typeof paramsCompiled == 'object') {
                                    // should be a nested tag or actions on,for,if,empty
                                    self.do(paramsCompiled, { selector: container }, args);

                                } else if (typeof paramsCompiled == 'string') {
                                    element.innerHTML += paramsCompiled; // TO DO: now is like append. Is it correct?
                                }
                            }

                        } else {

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

                            if (params.tag) {
                                self.do(params, container, args);
                                //self.nested({key: params.tag, obj: params[par], selector: {selector: container}, args: args});
                            }
                            else if (params.html) {
                                // replace \" to "
                                //var htmlCompiled = self.compile(params.html, args); // is undefined
                                // TO DO: replace all the following "compile" with replaceProperties?
                                /* self.log('params.html');
                                self.log(params.html); */
                                var htmlCompiled = self.replaceProperties(params.html, args);
                                /* self.log('htmlCompiled');
                                self.log(htmlCompiled); */
                                self.html(htmlCompiled, { selector: container });
                            } else if (params.lang) {
                                //alert(self.json.setup.language);
                                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); // textKey instead of lang?
                                    self.dataStorage.set(element, 'params', params);
                                }

                            } else if (params.text) { // string
                                //params.text = self.replaceTags({text:params.text});
                                //params.text = self.text(params.text);  // verifica presenza di tags e traduzioni
                                //$(selector).text(self.compile(params.text, args));
                                element.textContent = self.replaceProperties(params.text, args);
                                //} else if (params.empty || params.do == 'empty') {
                                //    element.innerHTML = '';
                            } else if (params.append) { // string
                                //params.append = self.replaceTags({text:params.append});
                                //$(selector).append(self.compile(params.append, args));
                                //element.appendChild(self.compile(params.append, args));
                                element.innerHTML += self.replaceProperties(params.append, args);

                            } else if (params.prepend) { // string
                                //params.prepend = self.replaceTags({text:params.prepend});
                                //$(selector).prepend(self.compile(params.prepend, args));
                                //element.insertBefore(self.compile(params.prepend, args));
                                element.innerHTML = self.replaceProperties(params.prepend, args) + element.innerHTML;

                            } else if (params.blocks) { // obsolete
                                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);
                                // on, for, if... (can be changed to include also html)
                                self.do(nestedParams, { selector: container }, args);


                                if (element.isConnected) {
                                    // case "html":{selector:... attr: }
                                    // the wrong use "html":{selector, attr, html:...}
                                    // now doesn't work (because is inside the last else)
                                    /* if (params.on) {
                                        alert(container);
                                        self.on(params.on, { selector: container }, args);
                                    } */
                                        
                                    if (params.attr) self.setAttributes(element, params.attr, args);
                                    if (params.style) self.css(params, selectorParams, args); //  to be removed

                                    // TO DO: params.style is deprecated (now in attr or css)
                                }
                                //self.blocks(params, selectorParams, args);
                            }



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

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

    };

    /* this.add = function (params) { // obsoleto, sostituito da html
        var selector = self.selector(params);
        if (params.inverse)
            $(selector).append(params.html);
        else
            $(selector).prepend(params.html);
    } */
    /*    this.attr = function (params, selectorParams, args) {
            var selector = self.selector(params);
            var element = self.query(selector);
            for (var attribute in params.attr)
            elements.forEach(function (element, index) {
                element.setAttribute(attrId, attrValue);
            });
        }
        */
    /* this.empty = function (params) {
        // { [container, class, value, id] }
        var selector = self.selector(params);
        $(selector).empty();
    } */
    /* this.addClass  = function (params) {
        var selector = self.selector(params);
        $(selector).addClass(params);
    }

    this.removeClass  = function (params) {
        self.log('removeClass');
        var selector = self.selector(params);
        $(selector).removeClass(params);
    } */
    /* this.toggleClass  = function (params) {
        var selector = self.selector(params);
        $(selector).toggleClass(params);
    } */
    //"#wifi > ul > li > span[data-field=share]:contains('Private')"
    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, "");
            //self.attribute(elements, attribute, attrs[attribute]);
            //self.attribute(elements, attribute, self.replaceWithPrefix(attrs[attribute], 'result', args)); // TO DO: check
        // before remove and check all the {result} occurrence (result excludes do/then/success etc.)
        // replaceResult replace {result} from Alert (should be obsolete)
        // check in the actual apps if {result} is used
    };

/*     this.setAttributes = function (elements, attrs, args) {
            if (!Array.isArray(elements)) elements = [elements];
            elements.forEach(function (element, index) {
                for (var attribute in attrs) {
                    if (typeof attrs[attribute] == 'boolean' && attrs[attribute]) { // it consider also firebase (empty values are removed)
                        element.setAttribute(attribute, "");
                    } else {
                        //if (attrs[attribute] == undefined) attrs[attribute] = '';
                        element.setAttribute(attribute, self.replaceProperties(attrs[attribute]));
                    }
                }
            });
            self.attribute(elements, attribute, self.replaceResult(attrs[attribute], args));
    }; */

    // TO DO: distinguish setAttribute from getAttribute
    // because we need to set attribute empty
    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) { // it consider also firebase (empty values are removed)
                    element.setAttribute(attrId, "");
                } else {
                    element.setAttribute(attrId, self.replaceProperties(attrValue));
                }
            });
        } else {
            if (!Array.isArray(elements))
                return elements.getAttribute(attrId); // todo: intercept the error "Cannot read properties of undefined (reading 'getAttribute"

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

    this.attr = function (params, selectorParams, args) {
        // deprecated. to be replaced with "css" {selector, style, addClass, removeClass, toggleClass}
        // css is used as an action to apply addClass removeClass toggleClass
        // and to apply style: {...}
        //params = self.replaceProperties(params);
        self.css(self.replaceProperties(params), selectorParams, args);
    };

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

    this.css = function (params, selectorParams, args) {
        // {selector, style, addClass, removeClass, toggleClass}

        /* console.log('selectorParams');
        console.log(selectorParams);
        console.log('css');
        console.log(params); */
        if (Array.isArray(params)) {
            for (var obj of params)
                self.css(obj, selectorParams, args);
            /* } else if (params.selector && Array.isArray(params.selector)) {
                for (let selector of params.selector) {
                    let obj = self.cloneObject(params);
                    obj.selector = selector;
                    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);

            //var selector = params.selector || selectorParams;
            //var elements = document.querySelectorAll(selector);
            //console.log(elements);
            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);
            }
        }
    };

    /* this.class = function (params) { // obsolete -> css
        self.log('self.class');
        self.log(params);
        // { [container, class, value, id]
        // [style, addClass, removeClass] }
        var selector = self.selector(params);
        if (params.style)
            $(selector).css(params.style);
        if (params.add)
            $(selector).addClass(params.add);
        if (params.remove)
            $(selector).removeClass(params.remove);
        if (params.toggle)
            $(selector).toggleClass(params.toggle);
    } */
    /* this.createPage = function (data) {
        // SPOSTATA SOTTO fixo.pages.js
        
        //self.log('createPage');
        //self.log(data);
        if (!$('#'+data.id).length) {
            $(data.container).append('<div id="'+data.id+'" class="fixoPage"></div>');
            if (data.background) {
                $('#'+data.id).css({
                    'background-color': data.background
                });
            }
        }
    } */
    /*     this.iconPng = function(icon) {
            return 'https://'+self.domain+'/app/files/icons/100white/'+icon+'.png';
        } */
    /*     this.imgTag = function (data) {
            // DIPENDENZA CON iconPng
            
            var imgId = data.id || '';
            var imgClass = data.class || '';
            var imgValue = data.value || '';
            var imgStyle = data.style || ''; // || 'width: 84px; margin:6px 10px;'; // ATTENZIONE: stile predefinito da rimuovere
            var onClick = (data.action) ? 'onclick="javascript:self.do(\''+data.action+'\',\''+imgValue+'\')" ' : '';
            return '<img src="'+self.iconPng(data.icon)+'" id="'+imgId+'" class="'+imgClass+'" data-value="'+imgValue+'" style="'+data.style+'" '+onClick+'/>';
        } */
    /*     this.setImg = function (data) {
            // Sembra inutilizzata
            // DIPENDENZA CON iconPng
    
            // data = {icon, style}
            var selector = self.selector(data);
            var imgValue = data.value || '';
            $(selector).attr('src', self.iconPng(data.icon));
            if (data.style) $(selector).css(style);
            if (data.action) {
                $(selector).attr('onclick', 'javascript:self.do(\''+data.action+'\',\''+imgValue+'\')');
            }
        } */
    // --------------
    //  SVG
    // --------------
    /*     this.svgSet = function (params) {
            //self.log('svgSet');
            //self.log(params);
            if (self.json && self.json.icons) {
            //if (self.json.setup && self.json.setup.icons) {
    
                if (params.icon) {
                    var iconObj = {};
                    iconObj.icon = (params.icon.indexOf(':')) ? params.icon.split(/:/)[1] : string;
                    iconObj.set = (params.icon.indexOf(':')) ? params.icon.split(/:/)[0] : undefined;
    
                    var svgIcons = self.json.icons || {}; // {src, prefix, suffix}
                    //var svgIcons = self.json.setup.icons || {}; // {src, prefix, suffix}
                    if (iconObj.set)
                        if (svgIcons[iconObj.set])
                            svgIcons = svgIcons[iconObj.set];
                        else
                            self.log('Can\'t find the icons set "'+iconObj.set+'" in "icons"');
                    iconObj.src = svgIcons.src || ''; // serve?
                    var svgPrefix = svgIcons.prefix || '';
                    var svgSuffix = svgIcons.suffix || '';
                    //if (iconObj.src !== '' && iconObj.icon) iconObj.src = '#' + svgPrefix + iconObj.icon + svgSuffix;
                    if (iconObj.icon) {
                        
                        if (!svgIcons.preload)
                            // siamo vicini
                            iconObj.src = svgIcons.src + '#' + svgPrefix + iconObj.icon + svgSuffix;
                        else
                            iconObj.src = '#' + iconObj.set + '_' + svgPrefix + iconObj.icon + svgSuffix;
                    }
                    iconObj.viewbox = svgIcons.viewbox; //  || '0 0 25 25';
                    return iconObj; // {src, set, name, viewbox}
                } else {
                    self.log('icon "name" parameter undefined');
                }
            } else {
                self.log('Can\'t find "icons" in "setup"');
            }
        }
        
        this.svgChange = function (data) {
            self.log('svgChange');
            self.log(data);
    
            var selector = self.selector(data); //), undefined, true);
            var containerElement = self.query(selector);
    
            var elementSvg = containerElement.querySelectorAll('svg')[0];
            var elementSvgUse = elementSvg.querySelectorAll('use')[0];
            var elementSvgText = elementSvg.querySelectorAll('text')[0];
            var elementSvgCircle = elementSvg.querySelectorAll('circle')[0];
    
            var iconObj = self.svgSet(data);  // {src, set, name, viewbox}
    
            if (iconObj.src)
                elementSvgUse.setAttribute('xlink:href', iconObj.src);
                //$(data.container + ' > svg > use').attr('xlink:href', iconObj.src);
    
            if (data.text !== undefined) {
                elementSvgText.textContent = data.text;
                // $(data.container + ' > svg > text').text(data.text);
    
                var textY = 6.5; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
                if (data.textSize !== undefined) {textY = data.textSize/2;}
    
                if (data.textPosition == 'bottom') { // textPosition should be removed
                    elementSvgUse.setAttribute('transform', 'translate(0, -20) scale(0.80)');
                    //$(data.container + ' > svg > use').attr("transform", "translate(0, -20) scale(0.80)");
                    textY += 26;
                }
    
                elementSvgText.setAttribute('y', textY);
                //$(data.container + ' > svg > text').attr('y',textY);
                elementSvgText.style['font-size'] = data.textSize+'px';
                //$(data.container + ' > svg > text').css('font-size',data.textSize+'px');
    
            } else {
                elementSvgText.textContent = '';
                //$(data.container + ' > svg > text').text('');
    
            }
            if (data.background !== undefined){
    
                elementSvgCircle.setAttribute('fill', data.background);
                elementSvgCircle.setAttribute('fill-opacity', 1);
                elementSvgCircle.style['stroke'] = data.background;
            }
            if (data.border !== undefined) {
                elementSvgCircle.setAttribute('stroke-opacity', 1);
                elementSvgCircle.style['stroke'] = data.border;
            }
    
            if (data.color !== undefined && data.color !== 'colored') {
                elementSvgUse.style['fill'] = data.color;
            }
            if (data.textColor !== undefined){
                elementSvgText.style['fill'] = data.textColor;
            } else if (data.color) {
                elementSvgText.style['fill'] = data.color;
            }
    
        }
    
        this.svgImage = function (data) {
            //$(data.container).hide();
            //$(data.container).css({opacity:0});
    
            var selector = self.selector(data); //), undefined, true);
            var containerElement = self.query(selector);
            //var containerElement = self.query(data.container);
    
            //if (data.value !== undefined) $(data.container).data('value', data.value);
    
            var iconObj = self.svgSet(data); // {src, set, name, viewbox}
            var viewBox = iconObj.viewbox || '-50 -50 100 100';
            var htmlString = '<svg viewBox="'+viewBox+'"><circle cx="0" cy="0" r="46" fill-opacity="0"></circle><use xlink:href=""></use><text x="0" y="0" text-anchor="middle" fill=""></text></svg>';
    
            if (containerElement) {
                containerElement.innerHTML = htmlString;
    
                //$(data.container).append('<svg viewBox="'+viewBox+'"><circle cx="0" cy="0" r="46" fill-opacity="0"></circle><use xlink:href=""></use><text x="0" y="0" text-anchor="middle" fill=""></text></svg>');
    
                var elementSvg = containerElement.querySelectorAll('svg')[0];
                var elementSvgUse = elementSvg.querySelectorAll('use')[0];
                var elementSvgText = elementSvg.querySelectorAll('text')[0];
                var elementSvgCircle = elementSvg.querySelectorAll('circle')[0];
    
                elementSvgUse.setAttribute('xlink:href', iconObj.src);
                //$(data.container + ' > svg > use').attr('xlink:href', iconObj.src);
    
                if (data.text !== undefined) {
    
                    elementSvgText.textContent = data.text;
                    // $(data.container + ' > svg > text').text(data.text);
    
                    var textY = 6.5; // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/text-anchor
                    if (data.textSize !== undefined) {textY = data.textSize/2;}
    
                    if (data.textPosition == 'bottom') { // textPosition should be removed
                        elementSvgUse.setAttribute('transform', 'translate(0, -20) scale(0.80)');
                        //$(data.container + ' > svg > use').attr("transform", "translate(0, -20) scale(0.80)");
                        textY += 26;
                    }
    
                    elementSvgText.setAttribute('y', textY);
                    //$(data.container + ' > svg > text').attr('y',textY);
                    elementSvgText.style['font-size'] = data.textSize+'px';
                    //$(data.container + ' > svg > text').css('font-size',data.textSize+'px');
                }
    
    
                if (data.background !== undefined) {
    
                    elementSvgCircle.setAttribute('fill', data.background);
                    elementSvgCircle.setAttribute('fill-opacity', 1);
                    elementSvgCircle.style['stroke'] = data.background;
                }
                if (data.border !== undefined){
                    elementSvgCircle.setAttribute('stroke-opacity', 1);
                    elementSvgCircle.style['stroke'] = data.border;
    
                    //$(data.container + ' > svg > circle').attr('stroke-opacity', 1);
                    // FORSE DEVE DIVENTARE attr ANZICHE' css
                    //$(data.container + ' > svg > circle').css({'stroke': data.border});
                }
                
                //if ((data.color === undefined) && (data.color !== 'colored')) {data.color = '#fff';}
                if (data.color !== undefined && data.color !== 'colored') {
                    elementSvgUse.style['fill'] = data.color;
                    //$(data.container + ' > svg > use').css({'fill': data.color});
                    //$(data.container + ' > svg > text').css({'fill': data.color});
                }
                if (data.textColor !== undefined){
                    elementSvgText.style['fill'] = data.textColor;
                    //$(data.container + ' > svg > text').css({'fill': data.textColor});
                } else {
                    elementSvgText.style['fill'] = data.color;
                    //$(data.container + ' > svg > text').css({'fill': data.color});
                }
    
    
    
                self.css({style: data.style}, {container: data.container + ' > svg'});
            }
    
        }
        
        this.svg = function (params, selectorParams, args) {
            var paramsReplaced = self.replaceProperties(params, args);
    
            //var container = self.selector(self.extend({}, params, selectorParams));
    
            var container = self.selector(paramsReplaced, undefined, true) || self.selector(selectorParams, undefined, true);
    
            var element = self.query(container);
            var color = paramsReplaced.color || getComputedStyle(element).color;
            //if (element.getElementsByTagName('SVG').length == 0) {
            //if (element.querySelectorAll('svg').length == 0) {
    
                self.svgImage({
                    container: container,
    
                    // special param
                    icon: paramsReplaced.icon,
                    text: paramsReplaced.text,
                    textSize: paramsReplaced.textSize, // verify (obsolete when span will be nested)
                    textPosition: paramsReplaced.textPosition, // verify (obsolete when span will be nested)
                    textColor: paramsReplaced.textColor, // verify (obsolete when span will be nested)
                    background: paramsReplaced.background,
                    border: paramsReplaced.border,
                    color: paramsReplaced.color || color, // $(container).css("color"),
                    style: paramsReplaced.style, // da fare css in svgImage
                    class: paramsReplaced.class
                    //hoverBackground: data.hoverBackground, // verify
                    //hoverBorder: data.hoverBorder, // verify
                    //hoverColor: data.hoverColor, // verify
                });
    
        } */
    // --------------
    //  ITEM
    // --------------
    /*
            this.checkItem = function (data) {
    
            var selectedIconClass = data.selection || 'selectedIcon'; // self.json.setup.classes.selectedIcon;
    
            //self.log('checkItem');
            //self.log(data);
            var checked = 0;
            // var selector = '';
            var selector = self.selector(data);
    
            if (data.style === undefined) {data.style = selectedIconClass;}
            if (data.checked !== undefined) {
                checked = self.replaceProperties(data.checked);
                //checked = self.replaceTags({text:data.checked});
                checked = data.checked;
                if (data.checked) {
                    $(selector).addClass(data.style);
                } else {
                    $(selector).removeClass(data.style);
                }
            } else {
                checked = $(selector).hasClass( data.style );
            }
            return checked;
        } */
    /*
        this.toggleItem = function (data) { // modificato 20180709 da changeCheckItem in toggleItem
            self.log('toggleItem');
            //self.log(data);
            var checked;
            checked = self.checkItem({
                id: data.id,
                class: data.class,
                value: data.value
                //style: data.style
            });
            checked = !checked; // inverte il valore
            self.checkItem({
                id: data.id,
                class: data.class,
                value: data.value,
                checked: checked
                //style: data.style,
            });
            return checked;
        }
        */
    /*     this.selectItem = function (data) {
    
            var selectedIconClass = self.json.setup.classes.selectedIcon;
    
        
            var selector = self.selector({container: data.container, class: data.class});
            if (data.style === undefined) {data.style = selectedIconClass;}
            $(selector).removeClass(data.style);
    
            self.log('selector');
            self.log(selector);
    
    
            if (data.value || data.id) {
                selector = self.selector(data);
                
                $(selector).addClass(data.style);
            }
    
        }
        
        this.unselectItems = function (data) {
            var selectedIconClass = self.json.setup.classes.selectedIcon;
            //if (data.style)
            if (data.style === undefined) {data.style = selectedIconClass;}
            if (data.class.indexOf('.') < 0) {data.class = '.' + data.class}
            $(data.class).removeClass(data.style);
        }
            */
    /*     this.removeImage = function (data) {
            var selector = self.selector(data);
            $(selector).css({'background-image':'url()'});
        }
    
        this.loadImage = function (data) {
            var selector = self.selector(data);
            //if (!data.style) data.style = {};
            //if (data.background) data.style['background-color'] = data.background;
            if (data.image || data.image === '') {
                self.getImage({ // ex fixo.loadImage
                    object: selector,
                    url: data.image
                    //style: data.style,
                    //animation: data.animation,
                });
            } else {
                self.removeImage(data);
            }
        } */
    /*         this.changeItem = function (data, selectorParams, args) {
            self.log('changeItem');
            self.log(data);
            var container = (selectorParams) ? self.selector(selectorParams) : data.container;
            var selector = self.selector(self.extend({}, data, selectorParams));
    
            var iconClass = ''; //self.json.setup.classes.icon || '';
            var badgeClass = 'iconBadge'; //self.json.setup.classes.badge || '';
    
            if (data.svg || data.text || data.background || data.border || data.color) {
                $(selector + ' > svg').css({opacity:1});
                self.svgChange({
                    container: selector,
                    name: data.svg,
                    text: data.text,
                    background: data.background,
                    border: data.border,
                    color: data.color,
                });
            } else {
                $(selector + ' > svg').css({opacity:0});
            }
    
            // IMMAGINE
            if (data.image && data.image !== '') {
                var imgData = data;
                imgData.style = {
                    width: '82px',
                    height: '82px',
                    'margin-bottom': '27px'
                };
                if (data.background) imgData.style['background-color'] = data.background;
                self.loadImage(imgData);
                $(selector + ' > svg').css({opacity:0});
            } else {
                self.removeImage(data);
                $(selector + ' > svg').css({opacity:1});
            }
            
            if (data.badgeIcon || data.badgeText) {
                if (!$('.'+badgeClass).length)
                    $(selector).append('<div class="'+badgeClass+'"></div>');
                self.svgImage({
                    container: selector + ' > .'+badgeClass,
                    icon: data.badgeIcon,
                    text: data.badgeText,
                    background: data.badgeBackground,
                    border: data.badgeBorder,
                    color: data.badgeColor,
                });
            } else {
                $(selector + ' > .'+badgeClass).remove();
            }
    
    
            if (data.titleColor)
                $(selector + ' > span').css({'color':data.titleColor});
            else if (data.color)
                $(selector + ' > span').css({'color':data.color});
            
            if (data.titleSize) $(selector + ' > span').css({'font-size':data.titleSize});
            if (data.title) {
                $(selector+' > span').text(data.title);
                if ($(selector+' > span').hasClass('textFitted')) {
                    self.textFit($(selector+' > span'), {
                        alignHoriz: false,
                        multiLine: false,
                        alignVert: false,
                        minFontSize: 10,
                        maxFontSize: data.titleSize || 12,
                    });
                }
            }
    
            if (data.checked !== undefined) {
                self.checkItem({
                    container: data.container,
                    class: data.class,
                    value: data.value,
                    id: data.id,
                    checked: data.checked
                    //style: data.style,
                });
            }
    
            // action (obsolete)
            if (data.action) {
                var onData = $(selector).data('onData');
                onData.action = data.action;
                $(selector).data('onData', onData); // nuova versione
            }
            // events
            if (data.on) self.on(data.on, {container: selector});
    
        } */
    /*     this.enableIcon = function (params, selectorParams) {
            var container = (selectorParams) ? self.selector(selectorParams) : params.container;
            var selector = self.selector(self.extend({}, params, selectorParams));
    
            $(selector).css({'opacity':'1'});
            $(selector).off().on(self.touch, function(event){
                event.stopPropagation();
                self.onEvent(this, event);
            });
        }
        
        this.disableIcon = function (params, selectorParams) {
            var badgeClass = self.json.setup.classes.badge || '';
    
            var container = (selectorParams) ? self.selector(selectorParams) : params.container;
            var selector = self.selector(self.extend({}, params, selectorParams));
    
            $(selector + ' > .'+badgeClass).remove();
            $(selector).css({'opacity':'0.5'});
            $(selector).off();
        }
            */
    /*     this.getFileExtension = function (path) {
            var dotPosition=path.lastIndexOf(".");
            var stringLength=path.length;
            var extensionFile=path.substring(dotPosition+1,stringLength);
            return extensionFile;
        } */
    // --------------------
    // SLIDER
    // --------------------
    /*         var slides = {};
        
        var sliderLib = 'fixoSlider';
        var sliderWrapperClass = 'fixoSlider';
        //var sliderSlideClass = 'fixoSlidez';
        var sliderSelectedClass = 'slideSelected'; */
    /*
        var sliderLib = 'flickity';
        var sliderWrapperClass = 'fixoSlider';
        var sliderSlideClass = 'fixoSlide';
        var sliderSelectedClass = 'is-selected';
    */
    /*
        var sliderLib = 'sliderpro';
        var sliderWrapperClass = 'sp-slides';
        var sliderSlideClass = 'sp-slide';
        var sliderSelectedClass = 'sp-selected';
    */
    /*
    var sliderLib = 'swiper';
    var sliderWrapperClass = 'swiper-wrapper';
    var sliderSlideClass = 'swiper-slide';
    var sliderSelectedClass = 'swiper-slide-active';
    */
    /*
    this.sliderResume = function (data) {
        var selector = self.selector(data);
        if (slides[selector]) slides[selector].object.resume();
    }
        
    this.sliderFreeze = function (data) {
        var selector = self.selector(data);
        if (slides[selector]) slides[selector].object.freeze();
    }
        
    this.sliderNext = function(data) {
        var selector = self.selector(data);
        if (sliderLib == 'fixoSlider') slides[selector].object.next();
        //if (sliderLib == 'flickity') slides[selector].object.flickity('next');
    }

    this.sliderPrev = function(data) {
        var selector = self.selector(data);
        if (sliderLib == 'fixoSlider') slides[selector].object.prev();
        //if (sliderLib == 'flickity') slides[selector].object.flickity('previous');
    }

    this.autoSliderEvent = function() {
        var selector = self.selector(data);
        if (self.sliderSelected)
            slides[self.sliderSelected].object.autoSliderEvent();
    }

    this.addSlideIcon = function(data) {
        // creata per rendere più leggibile createSlide
        var selector = self.selector(data);
        var isImage = self.getFileExtension(data.icon);
        if (isImage == 'gif' || isImage == 'jpg' || isImage == 'jpeg' || isImage == 'png')
            fixo.loadImage({
                object: selector + ' > .slideIcon',
                url: data.icon,
                style: {height: '150px',
                    width: '150px',
                    marginTop: '25px',
                    marginLeft: '-75px',
                    backgroundSize: 'cover'
                }
            });
        else
        //if (data.icon)
            self.svgImage({
                container: selector + ' > .slideIcon',
                icon: data.icon,
                color: '#fff',
                });
        if (data.iconName)
            $(selector + ' > .slideIconName').html(data.iconName); // da cancellare?
        if (data.iconAction) {
            var value = data.iconValue || '';
            $(selector + ' > .slideIcon').attr('onclick', 'javascript:self.do(\''+data.iconAction+'\', \''+value+'\')');
        }
    }

    this.changeSlideIcon = function(data) {
        var selector = self.selector(data);
        // verificare se si può usare changeItem
        if (data.icon)
            self.svgChange({
                container: selector + ' > .slideIcon',
                name: data.icon,
                color: '#fff',
                });
        if (data.iconName)
            $(selector + ' > .slideIconName').html(data.iconName);
    }

    this.updateSlide = function(data) {
        self.log('updateSlide');
        if (!data.class) data.class = 'fixoSlide';
        var selector = self.selector(data);
        // ICONA
        self.changeSlideIcon(data);
        // BACKGROUND
        if (data.background) $(selector).css({'background-color':data.background});
        // TITOLO E TESTO
        var alertTitleObj = $(selector).children('.slideTitle');
        var alertTextObj = $(selector).children('.slideText');
        var titleAndText = (alertTitleObj.text() !== '' && alertTextObj.text() !== '');
        if (titleAndText) {
            alertTitleObj.css({'margin-top':'-225px'});
        }  else {
            alertTitleObj.css({'margin-top':'-60px'});
        }
        if (data.title !== undefined) {
            alertTitleObj.html(data.title);
            self.textFit(titleObj, {alignHoriz: true, multiLine: false, alignVert: true, minFontSize: 40, maxFontSize: 56});
        }
        if (data.text !== undefined) {
            alertTextObj.html(data.text);
            self.textFit(textObj, {alignHoriz: true, multiLine: false, alignVert: !titleAndText, minFontSize: 17, maxFontSize: 40});
        }
        // BOTTONE
        if (data.button !== undefined) {
            if (data.button) {
                if (data.buttonLabel) {
                    $(selector + ' > .slideButton').html(data.buttonLabel);
                }
                if (data.buttonAction) {
                    $(selector + ' > .slideButton').data('action', data.buttonAction);
                    $(selector + ' > .slideButton').data('value', data.value);
                    $(selector + ' > .slideButton').attr('onclick', 'javascript:self.do(\'' + data.buttonAction + '\',\''+data.value+'\')');
                }
                $(selector + ' > .slideButton').show();
            } else {
                $(selector + ' > .slideButton').hide();
            }
            // aggiungere changeItem dell'icona
        }
    }

    this.slideTextFit = function() {
        //self.log('slideTextFit');

        //if (!data.class) data.class = 'fixoSlide';
        //var selector = self.selector(data);
        var slideSelector = self.sliderSelected + ' > .' + sliderSelectedClass;

        titleObj = $(slideSelector).children('.slideTitle');
        textObj = $(slideSelector).children('.slideText');

        var titleAndText = (titleObj.text() !== '' && textObj.text() !== '');
        if (titleAndText) {
            titleObj.css({'margin-top':'-225px'});
            //textObj.css({'top':(384+125-30)+'px'});
        } else {
            titleObj.css({'margin-top':'-60px'});
        }
        self.textFit(titleObj, {alignHoriz: true, multiLine: false, alignVert: true, minFontSize: 40, maxFontSize: 56});

        self.textFit(textObj, {alignHoriz: true, multiLine: false, alignVert: !titleAndText, minFontSize: 17, maxFontSize: 40});

    }

    this.sliderTo = function(data) {
        // apre la slide data.index
        self.log('sliderTo');
        var selector = self.selector(data);
        if (data.index !== undefined) // comprende il caso = 0
            if (slides[selector] && slides[selector].object)
                slides[selector].object.to(data.index);

            //if (sliderLib == 'swiper')
            //    slides[selector].object.slideToLoop(data.index); // slideToLoop
            //if (sliderLib == 'flickity')
            //    slides[selector].object.flickity('select', data.index);
            //if (sliderLib == 'extraslider')
            //    $(selector+' > .extra-slider').trigger('extra:slider:goto', data.index);

        if (data.value !== undefined) { // comprende il caso = 0
            self.log(self.selector(data));
            if (!data.class) data.class = 'fixoSlide';
            var slideToSelect = self.selector(data);
            var index = $(slideToSelect).index();
            if (index > -1) slides[selector].object.to(index);
            //var slideToSelect = '.' + sliderSlideClass + '[data-value="' + data.prefix + data.value + '"]';
        }

            //if (sliderLib == 'flickity')
            //    slides[selector].object.flickity('selectCell', slideToSelect);
    }
    
    this.sliderExist = function(data) { // si può cancellare?
        var selector = self.selector(data);
        //alert(($(selector + ' > .extra-slider').length > 0));
        return ($(selector + ' > .extra-slider').length > 0)
        // return (slides[selector])
    }
    
    this.createSlide = function(data) {
        self.log('createSlide');
        //if (!data.prefix) data.prefix = "";
        if (!data.class) data.class = "fixoSlide";
        //var containerSelector = self.selector(data);
        var slideContent = '<div class="' + data.class + '" data-value="'+ data.value + '"></div>';

        if (data.inverse) {
            $(data.container).prepend(slideContent);
        } else {
            $(data.container).append(slideContent);
        }
        var selector = self.selector(data);
        var infoButtons = data.infoButtons || {};

        // STORE DATA
        $(selector).data('container', data.container);
        if (data.value) $(selector).data('value', data.value);
        //if (data.id) $(selector).data('id', data.id);
        //if (data.prefix) $(selector).data('prefix', data.prefix);

        var textHtml =  (data.text) ? data.text.replace(/\r?\n/g, '<br/>') : '';

        // PRICE
        //if(data.price && data.price !== '') textHtml += '<br/>€ '+data.price;

        // BACKGROUND
        if (data.background) $(selector).css({'background-color':data.background});

        // IMAGE
        if (data.image && data.image !== '') {
            if (data.title !== '' || textHtml !== '' )
                {
                    //$(selector).css({'background-image':'linear-gradient(to right,rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('+data.image+')'});
                    //} else {
                    //$(selector).css({'background-image':'url('+data.image+')'});
                    $(selector).append('<div class="fixoImgOverlay"></div>')
                }
            
            if (sliderLib == 'fixoSlider') {
                $(selector).css({'background-image':'url('+data.image+')'});
                // DA GESTIRE INTERNAMENTE A fixo.slider.js
                // DOVRA' CARICARE SOLO SLIDE ATTUALE, PREC. E SUCC

                //fixo.loadImage({
                //    object: selector,
                //    url: data.image,
                //    animation: 'fadeIn',
                //});

            }

            //if (sliderLib == 'swiper') {
            //    $(selector).addClass('swiper-lazy');
            //    $(selector).attr('data-background', data.image);
            //}
            //if (sliderLib == 'flickity') {
            //    $(selector).attr('data-flickity-bg-lazyload', data.image);
            //}

        } else {

            $(selector).css({'background-image':'none'});
            // SERVICE ICON
            //$(selector).append('<div class="slideIcon"></div><div class="slideIconName" style="display:none"></div></div>');
            //self.addSlideIcon(data);

            // if (data.icon)
            //     self.svgImage({
            //         container: selector + ' > .slideIcon',
            //         icon: data.icon,
            //         color: '#fff',
            //         });
            // if (data.iconName)
            //     $(selector + ' > .slideIconName').html(data.iconName);

            // if (data.icon)
            //     self.svgImage({
            //         container: selector + ' > .slideIcon',
            //         icon: data.icon,
            //         color: '#fff',
            //         });
            // if (data.iconName)
            //     $(selector + ' > .slideIconName').html(data.iconName);
        }
        
        // ICONA
        $(selector).append('<div class="slideIcon"></div><div class="slideIconName" style="display:none"></div></div>');
        self.addSlideIcon(data);

        // TITLE & TEXT
        $(selector).append('<div class="slideTitle"></div><div class="slideText"></div><div class="slideInfo"></div>');

        var alertTitleObj = $(selector + ' > .slideTitle');
        var alertTextObj = $(selector + ' > .slideText');
        var alertInfoObj = $(selector + ' > .slideInfo');

        // BOTTONI INFO

        var btnInfo = '';
        for (var i in infoButtons) {
            if (infoButtons[i].icon !== '')
                btnInfo += self.imgTag({
                    icon: infoButtons[i].icon,
                    action: infoButtons[i].action,
                    class: infoButtons[i].class,
                    value: infoButtons[i].value,
                    //style: 'width: 84px; margin:6px 10px;'
                });
            //alertInfoObj.html(btnInfo);
        }
        alertInfoObj.html(btnInfo);

        // TESTO
        
        if (data.title && data.title !== '') {
            alertTitleObj.html(data.title);
        } else {
            alertTitleObj.html(data.iconName);
        }
        alertTextObj.html(textHtml);
        
        //alertInfoObj.html()
        

        // if (data.link && data.link !== '') { // serviva a costruire il div con il qrcode, sostituito dal link per aprire lo swal alert
        //     var link = data.link;
        
        //     self.log('DATALINK: '+link);

        //     if (link.indexOf('musement.com') || link.indexOf('booking.com')) {
        //         link = link.replace(/^https?:\/\//,''); //rimuovo https dal link
        //         self.log('link: '+link);
        //         link = 'https://c83.travelpayouts.com/click?shmarker=219111&promo_id=2015&source_type=customlink&type=click&custom_url=' + link;
        //     }
            
        //     self.log('DATALINK1: '+link);
            
        //     $(selector).append('<div class="slideQrCode"></div>');
        //     self.qrCode({
        //         container: selector,
        //         class: 'slideQrCode',
        //         text: link,
        //         size: 100
        //     });
        //     // attributo sul Codice QR per la visualizzazione dell'alert
        //     $('.slideQrCode > img').attr('onclick','javascript:self.do(\'app.hot.alertQRCode\')');
        // } else {

            
            $(selector).append('<div class="slideButton" style="display:none"></div>');
            if (data.button) {
                if (data.buttonLabel) {
                    $(selector + ' > .slideButton').html(data.buttonLabel);
                }
                if (data.buttonAction) {
                    $(selector + ' > .slideButton').data('action', data.buttonAction);
                    $(selector + ' > .slideButton').data('value', data.value);
                    $(selector + ' > .slideButton').attr('onclick', 'javascript:self.do(\'' + data.buttonAction + '\',\''+data.value+'\')');
                }
                $(selector + ' > .slideButton').show();

                // $(selector + ' > .slideButton').off().on(self.touch, function(){
                //     var action = $(this).data('action');
                //     var key = $(this).data('value');
                //     self.do(action, value);
                // });

            } else {
                $(selector + ' > .slideButton').hide();
            }
        //} chiude l'IF del data.link
        var titleAndText = (alertTitleObj.text() !== '' && alertTextObj.text() !== '');

        // POSIZIONE TITOLO
        var titleTop = -70 -170 * (titleAndText);
        alertTitleObj.css({'margin-top': titleTop + 'px'});
        self.textFit(alertTitleObj, {alignHoriz: true, multiLine: false, alignVert: true, minFontSize: 30, maxFontSize: 56});
        
        // ALTEZZA TESTO
        var textHeight = 340 - 100 * (infoButtons.length > 0);
        alertTextObj.css({'height': textHeight + 'px'});
        
        
        //self.textFit(alertTitleObj, {alignHoriz: true, multiLine: false, alignVert: true, minFontSize: 30, maxFontSize: 56});

        //self.textFit(alertTextObj, {alignHoriz: true, multiLine: false, alignVert: !titleAndText, minFontSize: 15, maxFontSize: 45});
        //self.log('END createSLIDE');


    }
    
    this.createSlider = function (data) {
        self.log('createSlider');
        //if (!data.prefix) data.prefix = "";
        var selector = self.selector(data);
        var startIndex = data.index || 0;
        //self.log('pre DATAINDEX: '+data.index);
        
        // RIMUOVE ISTANZA PRECEDENTE

        if (slides[selector] && slides[selector].object) {
            // Memorizza posizione
            var slideSelector = selector + ' > .' + sliderSelectedClass;
            //self.log('slideSelector di self.createSlider: '+slideSelector);
            startIndex = data.index || $(slideSelector).index();
            // Rimuove slider precedente
            //self.log('startIndex: di '+data.index+' .index() '+$(slideSelector).index());
            $(selector).empty();
            delete slides[selector].object;
        }

        slides[selector] = {};


        // FRECCE
        // if (data.buttons) {
        //     if (sliderLib == 'swiper') {
        //         $(selector).append('<div class="swiper-button-next swiper-button-white"></div>');
        //         $(selector).append('<div class="swiper-button-prev swiper-button-white"></div>');
        //     }
        // }
        
        for (var key in data.slides) {
            var slide = data.slides[key];
            slide.inverse = data.inverse;
            //slide.container = selector; //+' > .' + sliderWrapperClass;
            slide = self.extend({
                container: selector,
                class: 'fixoSlide', //sliderSlideClass,
                value: data.value, //data.prefix + data.value
            }, slide);
            self.createSlide(slide);
            
        }

        // CREAZIONE OGGETTO SLIDER
        
        if (sliderLib == 'fixoSlider') {

            var params = {
                container: selector, //+' > .' + sliderWrapperClass,
                play: data.play,
                interval: data.interval,    // ms
                onChange: data.onChange,
                onChangeStart: self.slideTextFit,
                    index: startIndex         // prima slide
                // direction: 1,            // from right to left
                // opts.swipeDuration: 0.6, // sec
                // opts.swipeTreshold: 0.2, // Swipe from +/-20%
            };

            slides[selector].object = $.fn.slide(params);
        }
        //self.log('END createSlider');
    }
*/
    /*
        this.getSelectedSlide = function (data) {
            self.log('getSelectedSlide');
            //self.log(data);
            var selector = self.selector(data);
            if (sliderLib == 'fixoSlider') {
                var slideSelector = selector + ' > .' + sliderSelectedClass;
                
                self.log($(slideSelector).data('value'));
                return $(slideSelector).data('value');
            }
    
        }
    
        var sliderSelect = function (currentItem, currentIndex) { //si può cancellare?
            var id = $(currentItem).parent().parent().parent().attr('id');
            self.log('slideId:'+id);
            self.log('currentItem:'+currentItem);
            self.log('currentIndex:'+currentIndex);
            slides[id].currentItem = currentItem;
            slides[id].currentIndex = currentIndex;
        }
    
        var sliderPause = function (currentItem, currentIndex) { // si può cancellare?
            $(currentItem).parent().parent().trigger('extra:slider:pause');
            var id = $(currentItem).parent().parent().parent().attr('id');
            self.log('slideId:'+id);
            self.log('currentItem:'+currentItem);
            self.log('currentIndex:'+currentIndex);
            slides[id].currentItem = currentItem;
            slides[id].currentIndex = currentIndex;
        }
    
        var sliderPlay = function (currentItem, currentIndex) { // si può cancellare?
            $(currentItem).parent().parent().trigger('extra:slider:pause');
            $(currentItem).parent().parent().trigger('extra:slider:resume');
            var id = $(currentItem).parent().parent().parent().attr('id');
            self.log('slideId:'+id);
            self.log('currentItem:'+currentItem);
            self.log('currentIndex:'+currentIndex);
            slides[id].currentItem = currentItem;
            slides[id].currentIndex = currentIndex;
        }
    
        var sliderResume = function (currentItem, currentIndex) { //si può cancellare?
            var id = $(currentItem).parent().parent().parent().attr('id');
            if (slides[id].active) {
                sliderPlay(currentItem, currentIndex);
            } else {
                sliderPause(currentItem, currentIndex);
            }
        }
    
        var sliderClick = function (currentItem, currentIndex) { // si può cancellare ?
            var id = $(currentItem).parent().parent().parent().attr('id');
            if (slides[id].active) {
                slides[id].active = false;
            } else {
                slides[id].active = true;
            }
        } */
    /*         this.addItem = function (data, selectorParams) { // obsolete
    
            var container = (selectorParams) ? self.selector(selectorParams) : data.container;
            var selector = self.selector(self.extend({}, data, selectorParams));
    
            // app default
            // aggiungere anche dimensione di default
    
            var iconClass = self.json.setup.classes.icon || '';
            var badgeClass = self.json.setup.classes.badge || '';
    
            var style = (data.style) ? data.style : {};
    
            // retro-compatibility
            if (data.size) {
                if (data.size == 'small')
                    style.width = '50px';
                else if (data.size == 'medium')
                    style.width = '80px';
                else if (data.size == 'big')
                    style.width = '120px';
                else if (data.size)
                    style.width = data.size;
            }
    
            style = self.extend({}, style, { // retrocompatibilità / obsoleto
                color: data.color,
                background: data.background,
                border: data.border,
                display: data.display,
                margin: data.margin,
                padding: data.padding,
                width: data.width,
                height: data.height
            });
    
    
            // Create
            if (data.id === undefined) {data.id = '';}
            if (data.value === undefined) {data.value = data.id};
            if (data.class === undefined) {data.class = iconClass} else if (data.class !== iconClass) {data.class += ' ' + iconClass};
    
            var itemDiv = '<div id="'+data.id+'" class="'+data.class+'" data-value="'+data.value+'" data-selector="'+selector+'"></div>';
            
            if (data.prepend) {
                $(data.container).prepend(itemDiv);
            } else {
                $(data.container).append(itemDiv);
            }
    
            self.css({style: {
                width: style.width,
                display: style.display,
                padding: style.padding,
                margin: style.margin
            }}, {container: selector});
    
            // SVG
    
            if (data.svg || data.text) {
                self.svgImage({
                    container: selector,
                    icon: data.svg,
                    text: data.text,
                    textSize: data.textSize, // verify (obsolete when span will be nested)
                    textPosition: data.textPosition, // verify (obsolete when span will be nested)
                    textColor: data.textColor, // verify (obsolete when span will be nested)
                    background: style.background,
                    border: style.border,
                    color: style.color
                    //hoverBackground: data.hoverBackground, // verify
                    //hoverBorder: data.hoverBorder, // verify
                    //hoverColor: data.hoverColor, // verify
                });
            } else {
                
            }
    
            // image
    
            if (data.image) {
                var imgData = data;
                imgData.style = {
                    width:'82px',
                    height:'82px',
                    'margin-bottom':'27px'
                };
                if (style.background) imgData.style['background-color'] = style.background;
                self.loadImage(imgData);
            }
            
            // badge
            
            if (data.badgeIcon || data.badgeText) {
                $(selector).append('<div class="'+badgeClass+'"></div>');
                self.svgImage({
                    container: selector + ' > .'+badgeClass,
                    icon: data.badgeIcon,
                    text: data.badgeText,
                    background: data.badgeBackground,
                    border: data.badgeBorder,
                    color: data.badgeColor,
                });
            }
    
    
            if (data.title) {
                //data.title = self.text(data.title);
                data.titleColor = data.titleColor || style.color;
                if (data.titleColor == undefined) {data.titleColor = ''}
                if (data.titleSize == undefined) {data.titleSize = ''}
                //if (data.titleSizeMin == undefined) {data.titleSize = '7px'}
    
                $(selector).append('<span></span>');
                $(selector + ' > span').css({
                    color: data.titleColor,
                    'font-size': data.titleSize
                });
                //if (typeof data.title == 'object') alert(data.title.text);
                self.text(data.title, {container: selector + ' > span'});
                //var title = self.text(data.title);
                //$(selector + ' > span').text(title);
    
                //self.text(data.title);
    
                // itemSelectedBorderWhite GESTITO IN INDEX.CSS
    
                // questo stile dovrebbe essere fisso e con posizione assoluta
                if (data.image)
                    $(selector + ' > span').css({
                        'margin-top':'89px',
                        position: 'absolute'
                    });
    
    
                if (data.textFit !== false) {
                    self.textFit($(selector+' > span'), {
                        alignHoriz: false,
                        multiLine: false,
                        alignVert: false,
                        minFontSize: 10,
                        maxFontSize: data.titleSize,
                    });
                }
            }
    
            // action (obsolete)
            if (data.action || data.functions) {
                var.functions[selector] = data.functions || data.action; // obsoleto
                $(selector).data('onData', data); // nuova versione
                $(selector).off().on(self.touch, function(event){
                    event.stopPropagation();
                    self.onEvent(this, event);
                });
            }
            
            // matchMedia
            if (data.matchMedia) {
                for (var mediaEvent in data.matchMedia) {
                    self.matchMedia(data.matchMedia[mediaEvent], mediaEvent);
                }
            }
    
            // events
            if (data.on) self.on(data.on, {container: selector});
    
            if (data.checked !== undefined) {
                self.checkItem({
                    container: data.container,
                    class: data.class,
                    value: data.value,
                    id: data.id,
                    checked: data.checked
                    //style: data.style,
                });
            }
        } */
    // --------------
    //  TABS
    // --------------
    /*         this.addTab = function (data, selectorParams) {
            //self.log('addTab');
            //self.log(data);
    
            var containerParams = self.cloneObject(selectorParams);
    
            data.container  = containerParams.container;
            var selector = self.selector(data);
    
            var tabClass = ''; //self.json.setup.classes.button || '';
            var badgeClass = 'iconBadge'; //self.json.setup.classes.badge || '';
    
            var style = (data.style) ? data.style : {};
    
            // span height
            //style["line-height"] = (style.height) ? style.height : '42px';
    
            style = self.extend({}, style, {
                color: data.color,
                background: data.background,
                border: data.border,
                display: data.display,
                margin: data.margin,
                padding: data.padding,
                width: data.width,
                height: data.height
            });
    
            if (data.id === undefined) {data.id = '';}
            if (data.value === undefined) {data.value = data.id};
            if (data.class === undefined) {data.class = tabClass} else if (data.class !== tabClass) {data.class += ' ' + tabClass};
    
            var tabDiv = '<div id="'+data.id+'" class="'+data.class+'" data-value="'+data.value+'" data-selector="'+selector+'"></div>';
    
            $(data.container).append(tabDiv);
            
            self.css({style: style}, {container: selector});
    
            //if (data.key) {$(selector).data('key', data.key);}
    
            //var iconColor = style.color || $(selector).css("color");
    
            // ICON
            if (data.icon) {
                self.addItem({
                    class: 'icon',
                    value: data.value,
                    svg: data.icon,
                    image: data.image,
                    style: {
                        color: style.color || $(selector).css("color"),
                        width: style.height
                    }
                }, {container: selector});
            }
    
            // TEXT
            if (data.text !== undefined) {
                //alert(JSON.stringify(data.text));
    
                $(selector).append('<span></span>');
    
                self.text(data.text, {container: selector + ' > span'});
    
    
            } else {
                //alert(JSON.stringify(data.text));
            }
    
            if (data.icon) $(selector + ' > span').css({'padding-left': '0px'});
    
            // SPAN
            if(style.color) $(selector + ' > span').css({'color':style.color});
    
            // BADGE
            if (data.badgeIcon || data.badgeText) {
                if (!$('.'+badgeClass).length)
                    $(selector).append('<div class="'+badgeClass+'"></div>');
                self.svgImage({
                    container: selector + ' > .'+badgeClass,
                    icon: data.badgeIcon,
                    text: data.badgeText,
                    background: data.badgeBackground,
                    border: data.badgeBorder,
                    color: data.badgeColor,
                });
            } else {
                $(selector + ' > .'+badgeClass).remove();
            }
    
            // data
            if (data.action || data.functions){
                var.functions[selector] = data.functions || data.action; // obsoleto
                $(selector).data('onData', data); // nuova versione
                $(selector).off().on(self.touch, function(event){
                    event.stopPropagation();
                    self.onEvent(this, event);
                    //$(this).data('value')
                    //var selector = $(this).data('selector');
                    //var action = var.functions[selector];
                    //self.do($(this).data('onData').action, $(this).data('value'));
                });
            }
    
            // matchMedia
            if (data.matchMedia) {
                for (var mediaEvent in data.matchMedia) {
                    self.matchMedia(data.matchMedia[mediaEvent], mediaEvent);
                }
            }
    
            // events
            if (data.on) self.on(data.on, {container: selector});
    
            if (data.selected) self.selectTab(data);
            
        } */
    /*         this.changeTab = function (data, selectorParams) {
    
            var container = (selectorParams) ? self.selector(selectorParams) : data.container;
            var selector = self.selector(self.extend({}, data, selectorParams));
    
            if (data.icon && data.value) {
                self.changeItem({
                    container: selector,
                    class: 'icon',
                    value: data.value,
                    svg: data.icon,
                    color: data.color,
                    background: data.background
                });
            }
    
            if(data.background) $(selector).css({'background':data.background});
            if(data.color) $(selector + ' > span').css({'color':data.color});
    
            if (data.text) {
                //$(selector + ' > span').text(self.text(data.text));
                self.text(data.text, {container: selector + ' > span'});
            }
    
            if (data.action){
                $(selector).off().on(self.touch, function(){
                    data.action(data.value)
                });
            }
            // events
            if (data.on) self.on(data.on, {container: selector});
    
            if (data.selected) self.selectTab(data);
        } */
    /*         this.unselectTabs = function (data) {
    
            var selectionClass = data.selection || 'selectedButton';
            //var selectionClass = self.json.setup.classes.selectedButton;
            if (data.style) selectionClass = data.style;
            $(data.class).removeClass(selectionClass);
        }
    
        this.toggleTab = function (data) {
            data.toggle = true;
            return self.selectTab(data);
        }
    
        this.selectTab = function (data) {
            var selectionClass = data.selection || 'selectedButton';
            //var selectionClass = self.json.setup.classes.selectedButton;
            if (data.style) selectionClass = data.style;
            var selector = self.selector(data);
    
            if (data.deselectClass)
                if (data.class) $('.'+data.class).removeClass(selectionClass);
                
            if (data.checked !== undefined) {
                if (data.checked == false) $(selector).removeClass(selectionClass);
                else $(selector).addClass(selectionClass);
            } else if (data.toggle !== undefined) {
                // da toggleTab
                if ($(selector).hasClass(selectionClass)) {
                    $(selector).removeClass(selectionClass);
                    return false;
                } else {
                    //if (data.class) $('.'+data.class).removeClass(selectionClass);
                    $(selector).addClass(selectionClass);
                    return true;
                }
            } else {
                //if (data.class) $('.'+data.class).removeClass(selectionClass);
                $(selector).addClass(selectionClass);
            }
        } */
    // ---------------------------
    // FUNZIONI DATEPICKER
    // ---------------------------
    /*
        // basato su: http://www.daterangepicker.com/#options
        
        this.createDatePicker = function (data) {
            self.log('createDatePicker');
            var calendarVisible = false;
            if (!data.field) data.field = data.container + ' > .daterange';
            
            if (!$(data.field).length) {
                calendarVisible = true;  // Crea e nasconde campo testo e mostra calendario
                $(data.container).html('<input type="text" class="form-control daterange" style="display:none"/>');
            }
            
            var weekD = self.weekdaysShort(1, capitalize);
            weekD = weekD.unshift(weekD[6]);
            var monthsOfYear = ["January","February","March","April","May","June","July","August","September","October","November","December"];
            if (data.language) {
                monthsOfYear = moment.localeData(data.language).months();
                for (var index in monthsOfYear) {
                    monthsOfYear[index] =
                        monthsOfYear[index].substr(0,1).toUpperCase()
                        + monthsOfYear[index].substr(1);
                }
            }
            //var datarange = $('#daterange');
            
            $(data.field).daterangepicker({
                singleDatePicker: data.single,
                parentEl: data.container,
                startDate: data.start,
                endDate: data.end,
                autoApply: true,
                autoUpdateInput: false,
                locale: {
                    format: 'DD/MM/YYYY',
                    daysOfWeek: weekD,
                    monthNames: monthsOfYear,
                    firstDay: 1 // monday
                }
            });
            
            
            $(data.field).on('cancel.daterangepicker', function(ev, picker) {
                data.onCancel();
            });
    
            $(data.field).on('apply.daterangepicker', function(ev, picker) {
                data.onChange({
                    start: picker.startDate.format('YYYYMMDD'),
                    end: picker.endDate.format('YYYYMMDD')
                });
                if (picker.startDate && picker.endDate && picker.startDate !== picker.endDate) {
                    $(data.field).val(picker.startDate.format('DD/MM/YYYY') + '-' + picker.endDate.format('DD/MM/YYYY'));
                } else {
                    $(data.field).val(picker.startDate.format('DD/MM/YYYY'));
                }
            });
    
            self.unselectDatePicker({container: data.container});
    
    
        }
    
        var disableDates = [];
        this.updateDatePicker = function(data) {
            var datePicker = $(data.container + ' > .daterange').data('daterangepicker');
            if (data.disableDates) {disableDates = data.disableDates;}
    //             alert(datePicker+'-'+data.container);
            datePicker.isInvalidDate = function(date) {
                return (disableDates.indexOf(date.format('YYYYMMDD')) >= 0)
            }
            datePicker.updateView();
            if (data.start && data.end) {
                self.setDatePicker({
                    container: data.container,
                    start: data.start,
                    end: data.end,
                });
            } else {
                self.unselectDatePicker({container: data.container});
            }
        }
    
        this.unselectDatePicker = function(data) {
            $(data.container).find('.start-date').removeClass('start-date active');
            $(data.container).find('.end-date').removeClass('end-date active');
            $(data.container).find('.in-range').removeClass('in-range');
    
        }
            
        this.setDatePicker = function (data) {
            var datePicker = $(data.container + ' > .daterange').data('daterangepicker');
            datePicker.setStartDate(data.start);
            datePicker.setEndDate(data.end);
            datePicker.updateView();
        }
    
        this.getDatePicker = function (data) {
            var start = $(data.container + ' > .daterange').data('daterangepicker').startDate(data.start);
            var end = $(data.container + ' > .daterange').data('daterangepicker').endDate(data.end);
            //alert('ok: '+data.container);
            return {start: start, end: end}
        }
    
        // ---------------------------
        // FUNZIONI TIMEPICKER
        // ---------------------------
    
        this.createTimePicker = function (data) {
            $(data.container).html('<div class="datetimepicker" data-value="'+data.value+'"></div>');
    
            $(data.container + ' > .datetimepicker').datetimepicker({
                icons: {
                    up: "fixoNavUp",
                    down: "fixoNavDown",
                    previous: "fixoNavBackward",
                    next: "fixoNavForward"
                },
                inline: true,
                sideBySide: true,
                //locale: 'it'
                locale: {
                    format: 'DD/MM/YYYY',
                    daysOfWeek: weekD,
                    monthNames: monthsOfYear,
                    firstDay: 1 // monday
                }
            });
    
            //self.log('now: '+Date.now()+' moment: '+moment());
    
            self.setTimePicker({
                container: data.container,
                time: data.time
            });
    
    
            $(data.container + ' > .datetimepicker').off().on('dp.change', function(event) {
                var datePicked = moment(event.date).unix();
                //alert(datePicked);
                data.onChange(datePicked);
            });
    
        }
    
        this.setTimePicker = function (data) {
            //self.log('>>> setTimePicker --> data container: '+data.container+' data time: '+data.time+' moment: '+moment.unix(data.time).calendar());
            $(data.container + ' > .datetimepicker').data('DateTimePicker').date(moment.unix(data.time));//.format('LLL'));
        }
    
        this.getTimePicker = function (data) {
            //alert('ok: '+data.container);
            $(data.container + ' > .datetimepicker').data('DateTimePicker').date();
        }
    */
    // ---------------------------
    // TIME SLIDER
    // ---------------------------
    /*
        this.createTimeSlider = function(data){
            var container = data.container;
            var id = data.id;
    
            // c'è un'anomalia nella libreria bootstrap-slider
            // crea il contenitore dello slider con lo stesso id del campo input
            // va verificata la possibilità di configurazione della funzione .slider
            
            $('#'+data.container).append('<input id="'+id+'" data-slider-id="'+id+'" type="text" class="span2" value="" data-slider-min="0" data-slider-max="48" data-slider-step="1" data-slider-value="[0,48]" data-slider-tooltip="hide"/>');
            $('#'+id).slider();
            $('#'+id).off().on("slide slideStop", function(event) {
    
                self.log(event);
                self.changeItem({
                    id:'stepbackwardSlider'+id,
                    text: self.numberToHour({value: event.value[0], max: 48})
                });
                self.changeItem({
                    id:'stepforwardSlider'+id,
                    text: self.numberToHour({value: event.value[1], max: 48})
                });
                
                if (data.onChange) {
                    data.onChange({
                        start: self.numberToHour({value: event.value[0], max: 48}),
                        end: self.numberToHour({value: event.value[1], max: 48})
                    })
                }
    
            });
    
            // ICONS Slider
            self.addItem({
                container:'#'+id+' > .min-slider-handle',
                class: 'sliderIconLeft',
                id: 'stepbackwardSlider'+id,
                background: '#fff',
                color: 'black',
                border: 1,
                text: '00:00'
            });
            self.addItem({
                container:'#'+id+' > .max-slider-handle',
                class: 'sliderIconRight',
                id: 'stepforwardSlider'+id,
                background: '#fff',
                color: 'black',
                border: 1,
                text: '24:00'
            });
            //$('#'+sliderId+' .slider-selection').css({'background': '#fff'});
    
        }
    
        this.setTimeSlider = function(data){
            //Change value on slider left/right icon
            self.changeItem({
                    id:'stepbackwardSlider'+data.id,
                    text: data.start
                });
            self.changeItem({
                id:'stepforwardSlider'+data.id,
                text: data.end
            });
            //Convert value in 0-48 format
            var start = self.hourToNumber({value: data.start, max: 48});
            var end = self.hourToNumber({value: data.end, max: 48});
            // Set value on slider
            $('input[id="'+data.id+'"]').slider('setValue',[start,end]);
    
        }
    
        this.getTimeSlider = function(data){
            var slideStart = $('input[id="'+data.id+'"]').slider('getValue')[0] || 0;
            var slideEnd = $('input[id="'+data.id+'"]').slider('getValue')[1] || 48;
            var start = self.numberToHour({value: slideStart, max: 48});
            var end = self.numberToHour({value: slideEnd, max: 48});
            if (start == '24:00') {start = '00:00'}
            var timetable = {
                start: start || '00:00',
                end: end || '24:00'
            }
            return timetable;
        }
    */
    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;
    };

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

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

    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);
        // var obj = self.compile(data);
        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;
        //qrcode.clear(); // clear the code.
        //qrcode.makeCode(text); // make another code
    };

    /*     this.qrcode = function (data) {
            self.log('qrcode');
            var selector = self.selector(data);
    
            var options = {
                // render method: 'canvas', 'image' or 'div'
                render: 'image',
    
                // version range somewhere in 1 .. 40
                minVersion: 1,
                maxVersion: 40,
    
                // error correction level: 'L', 'M', 'Q' or 'H'
                ecLevel: 'L',
    
                // offset in pixel if drawn onto existing canvas
                left: 0,
                top: 0,
    
                // size in pixel
                size: data.size || 200,
    
                // code color or image element
                fill: data.color || '#000',
    
                // background color or image element, null for transparent background
                background: data.background || null,
    
                // content
                text: self.replaceProperties(data.text) || 'no text',
    
                // corner radius relative to module width: 0.0 .. 0.5
                radius: 0,
    
                // quiet zone in modules
                quiet: 0,
    
                // modes
                // 0: normal
                // 1: label strip
                // 2: label box
                // 3: image strip
                // 4: image box
                mode: 0,
    
                mSize: 0.1,
                mPosX: 0.5,
                mPosY: 0.5,
    
                label: data.label || 'no label',
                fontname: data.fontname || 'sans',
                fontcolor: data.fontcolor || '#000',
    
                image: null
            }
            $(selector).empty().qrcode(options);
        }   */
    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;
    };


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

    // -----------
    // DRAG
    // -----------
    /*
        var draggingData;
    
        this.sortable = function (data) {
            var rootEl = $(data.container)[ 0 ];
            var dragEl;
            
            draggingData = data;
            var selector = self.selector(data);
            $(selector).attr('draggable', 'true');
            $(data.exclude).attr('draggable', 'false');
            
            // Function responsible for sorting
            function onDragOver(event) {
                event.preventDefault();
                event.dataTransfer.dropEffect = 'move';
    
                var dragTarget = event.target; // target.nextSibling ||
                if ($(dragTarget).hasClass(draggingData.class))
                    $( dragEl ).insertAfter( $(dragTarget) );
                if ($(dragTarget).parent().hasClass(draggingData.class))
                    $( dragEl ).insertAfter( $(dragTarget).parent() );
            }
    
            // End of sorting
            function onDragEnd(event){
                event.preventDefault();
    
                $(dragEl).css({'opacity':'100%'});
                rootEl.removeEventListener('dragover', onDragOver, false);
                rootEl.removeEventListener('dragend', onDragEnd, false);
    
                // Notification about the end of sorting
                draggingData.target = dragEl;
                draggingData.onDragEnd(draggingData);
            }
    
            // Sorting starts
            rootEl.addEventListener('dragstart', function (event){
                dragEl = event.target; // Remembering an element that will be moved
                
                $(dragEl).css({'opacity':'50%'});
    
                // Limiting the movement type
                event.dataTransfer.effectAllowed = 'move';
                event.dataTransfer.setData('Text', dragEl.textContent);
    
    
                // Subscribing to the events at dnd
                rootEl.addEventListener('dragover', onDragOver, false);
                rootEl.addEventListener('dragend', onDragEnd, false);
    
    
                setTimeout(function () {
                    // If this action is performed without setTimeout, then
                    // the moved object will be of this class.
                    $(dragEl).css({'opacity':'100%'});
                }, 0)
            }, false);
        }
    
            */
    /*
        this.setWeekDays = function(data){
    
            if (!data.days) data.days = {};
    
            for (var i=1; i<7; i++){
                self.selectTab({
                    container: data.container,
                    class: 'fixoTabsWeekDays',
                    value: String(i),
                    checked: data.days[String(i)] || false
                });
            }
        }
        
        this.createWeekDays = function(data){
            //var selector = self.selector(data);
            var weekDays = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
            self.log('data.language '+data.language);
            if (data.language) weekDays=moment.localeData(data.language).weekdaysShort();
            for (var i=0; i<7; i++){
                var weekDay = weekDays[i];
                self.addTab({
                    container: data.container,
                    class: data.class,
                    value: String(i),
                    id: data.container + i,
                    background: 'black',
                    color: 'white',
                    border: '1px solid rgba(0, 0, 0, 1)',
                    text: weekDay,
                    action: function(value){
                        //alert($(this).attr(id));
                        // il toggleTab deselezionava sempre gli altri giorni
                        // risolto usando il selectTab col parametro checked
                        // va sistemato toggleTab
                        if (!data.days) data.days = {};
                        var checked = !Boolean(data.days[value]);
                        self.selectTab({
                            container: data.container,
                            class: 'fixoTabsWeekDays',
                            value: value,
                            checked: checked
                        });
                        data.days[value] = checked;
                    }
                });
            }
        }
    */
    //--------------------------
    // DA FIXO.JS
    //--------------------------
    /*---------------------
        UTILS
    ---------------------*/
    var sortField;

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

    this.sort = function (data) {
        // fixo.sort({object:{}, add: {}, field:'time', reverse:true})
        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 [];
        }
    };

    this.objectToArray = function (data) {
        // data: {object:{}, add: {}}
        var arr = [];
        var obj = data.object;
        if (obj !== undefined) {
            for (var key in obj) {
                obj[key].id = key; // aggiunge la chiave dell'oggetto come proprietà
                if (data.add) obj[key] = Object.assign(obj[key], data.add);
                arr.push(obj[key]);
            }
        }
        return arr;
    };



    /*
        this.itemsFound = function(items, field, value) {
                return $.grep(items, function(e){ return e.name == value; });
        }
    
    */
    /*     this.itemFound = function(items, field, value) {
            var itemFound = false;
            for (var i=0; i<items.length; i++) {
                if (items[i][field] == value) {
                        itemFound = true;
                }
            }
            return itemFound;
        }  */
    /*     this.itemExist = function(data) {
            if (data.value) {
                return ($(data.class + '[data-value='+data.value+']').length > 0);
            } else {
                return ($(data.class).length > 0);
            }
        } */
    this.equal = function (obj1, obj2) {
        if (obj1 && obj2) {
            return (JSON.stringify(obj1).replace('"', '') == JSON.stringify(obj2).replace('"', ''));
        } else {
            return (obj1 == obj2);
        }
    };

    /*     this.arraysEquals = function(arr1,arr2) {
            return ($(arr1).not(arr2).length === 0 && $(arr2).not(arr1).length === 0);
        } */
    var escapeRegExp = function (str) {
        return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
    };
    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) { //  && replace
                    //if (typeof str !== 'string') self.log(str);
                    result = String(str).replace(new RegExp(escapeRegExp(find), 'g'), replace);
                    //if (typeof str == 'string') return result; else return JSON.parse(result);
                    if (typeof str == 'object' && result) return JSON.parse(result);
                    else
                        return result;
                } else {
                    /* self.log('str:'+str);
                    self.log('find:'+find);
                    self.log('replace:'+replace); */
                    return str;
                }
            }
        } else {
            return str;
        }
    };

    this.capitalize = function (str) {
        return str.replace(/\b\w/g, l => l.toUpperCase());
    };


    /*---------------------
    URL UTILS
---------------------*/
    this.getPageName = function (url) {
        var index = url.lastIndexOf("/") + 1;
        var filenameWithExtension = url.substr(index);
        var filename = filenameWithExtension.split(".")[0]; // <-- added this line
        return filename; // <-- added this line
    };

    this.goToMainUrl = function () {
        window.location = self.mainUrl;
    };

    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, " "));
    };

    var paramsToObject = function (params) {
        params = params.split('#')[0];
        return JSON.parse('{"' + params.replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}');
    };

    var objectToParams = function (object) {
        var str = '';
        for (var key in object) str += '&' + key + '=' + object[key];
        if (str) str = str.substr(1);
        return encodeURI(str);
    };

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

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

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

    /*
        var encodeNum = function(num) { // 0..1296 es:customer
            var hex = c.toString(36);
            return c.toString(36);
        }
    
        var decodeNum = function(str) {
            return str.parseInt(36); // .length == 1 ? "0" + hex : hex
        }
    */
    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);
        }
    };
    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;
    };

    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)];
    };

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

    this.degToRGB = function (deg) {
        return packRGB(HSLToRGB([deg / 360, 1, 0.5]));
    };

    this.stripHtml = function (html) {
        var tmp = document.createElement("DIV");
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || "";
    };

    //--------------------------
    // TASK MANAGER
    //--------------------------
    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);
        }
    };

    var varlog = function (str) {
        self.log(str);
        self.log(window[str]); // 'var' -> var
    };

    this.logJson = function (jsonPar) { self.log(JSON.stringify(jsonPar)); };

    this.getTimestamp = function () {
        // we need also an API to get the time
        // like .getTime()?
        return Math.floor(Date.now());
    };

    this.cloneObject = function (obj) {
        var newObj = JSON.stringify(obj);
        return self.parse(newObj);
    };


    /*------------------------
        TEXT
    ------------------------*/
    /* var caseUp = function (string) {
        if (!string) string = '';
        return string.toUpperCase();
    }
    
    var caseDown = function (string) {
        if (!string) string = '';
        return string.toLowerCase();
    }
    
    var capitalize = function(string) {
        if (!string) string = '';
        return self.capitalize(string);
    }
            
    var text = function (key, caseFunction) {
        var t = self.json.text[key] || '';
        if (caseFunction) return caseFunction(t); else return t;
    } */
    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;
    };

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

    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));} */
    };

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

    /*    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');
            }
        }  */
    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 = {};

    this.timer = function (params, args) {
        // can go in "browser.json / functions (JS Browser BOM)
        // https://www.w3schools.com/js/js_window.asp
        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);
            }
        }
    };

    this.setInterval = function (params, args) {
        // could include the param "fromNow": true to execute the function
        /* self.log('setInterval');
        self.log(params); */
        if (params.name && params.duration) {
            if (self.intervals[params.name])
                clearInterval(self.intervals[params.name]);

            //self.do(params.do);
            self.do(params.do);
            self.intervals[params.name] = setInterval(function () {
                //self.do(params.do);
                self.do(params.do);
                return params.name;
            }, Number(params.duration)); // self.timeToMs
        }
        else
            self.log('"setInterval" function requires "name" and "duration" parameters');
    };

    this.clearInterval = function (params, args) {
        /* self.log('params');
        self.log(params);
        self.log('self.intervals[params.name]');
        self.log(self.intervals[params.name]); */
        if (self.intervals[params.name]) {
            clearInterval(self.intervals[params.name]);
            delete self.intervals[params.name];
        }
        // else
        //    self.log('"clearInterval" name not found: ' + params.name);
    };

    this.timeouts = {};

    this.setTimeout = function (params, args) {
        // could include the param "fromNow": true to execute the function
        /* self.log('setTimeout');
        self.log(params); */
        if (params.name && params.duration) {
            if (self.timeouts[params.name])
                clearInterval(self.timeouts[params.name]);

            //self.do(params.do);
            //self.do(params.do);
            self.timeouts[params.name] = setTimeout(function () {
                //self.do(params.do);
                self.do(params.do);
                return params.name;
            }, Number(params.duration)); // self.timeToMs
        }
        else
            self.log('"setTimeout" function requires "name" and "duration" parameters');
    };

    this.clearTimeout = function (params, args) {
        /* self.log('params');
        self.log(params);
        self.log('self.timeouts[params.name]');
        self.log(self.timeouts[params.name]); */
        if (self.timeouts[params.name]) {
            clearTimeout(self.timeouts[params.name]);
            delete self.timeouts[params.name];
        }
        //else
        //    self.log('"clearInterval" name not found: ' + params.name);
    };




    this.on = function (params, selectorParams) {
        //let selector = params.selector || params.container;

        var container = (selectorParams) ? self.selector(selectorParams) : params.selector; // 1.0.9 instead of params.container










        /* self.log('on');
        self.log(params);
        self.log('container');
        self.log(container); */
        //var selector = self.selector(self.extend({}, params, selectorParams));
        //$(container).data('onData', {events: params, container: container});
        var element;

        if (container) {
            element = self.query(container); // shoud be foreach elements...
            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);
            }
        } /*  else {
            self.log('"on" function with selector undefined');
            self.log('params');
            self.log(params);
            self.log('selectorParams');
            self.log(selectorParams);
        } */







        for (var eventId in params) {
            //var eventAction = params[eventId];
            //$(selector).data('onData').action = eventAction;
            /* if (eventId == 'thunkable') {
                self.thunkable(params[eventId]);
            } else  */
            if (eventId == 'init') {
                //self.log(params.init);
                //self.do(params.init, undefined, selectorParams);
                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); // potrebbe essere esplicitato sulla app



                    //var action = self.cloneObject(params.app.on.hashchange);
                    //self.replaceAll("{result}", window.location.hash);
                    //self.do(self.onHashChange);
                    self.do(self.onHashChange);
                    self.uiUpdate();
                });
            } else if (eventId == 'scroll') {
                self.onScroll.push(params.scroll);
                window.addEventListener('scroll', function () {
                    //self.do(self.onScroll);
                    self.do(self.onScroll);
                });
            } else if (eventId == 'resize') {
                self.onResize.push(params.resize);
                window.addEventListener('resize', function () {
                    //self.do(self.onResize);
                    self.do(self.onResize);
                });
            } else if (eventId == 'in') { // like inViewport but only one time
                if (container) {
                    const observer = new IntersectionObserver((entries, observer) => {
                        entries.forEach(function (entry) {
                            /* self.log('entry');
                            self.log(entry);
                            self.log('entry.target');
                            self.log(entry.target); */
                            /* self.log(entry);
                            self.log(entry.intersectionRatio);
                            self.log(entry.isIntersecting);
                            self.log(entry.isVisible); */
                            // isIntersecting, isVisible
                            //var element = self.query(entry.target);
                            //self.log(entry.intersectionRatio);
                            if (entry.intersectionRatio == 1) { // .isIntersecting?
                                var el = entry.target;
                                if (el.getAttribute('in') !== 'true') {
                                    el.setAttribute('in', 'true'); // change with dataStorage
                                    self.onEvent(entry.target, { type: 'in' });
                                }
                            }
                        });
                    }, {
                        rootMargin: '0px',
                        threshold: [0, 1] // if 0.5 is the half-height of the elmement
                    });
                    //const element = self.query(container);
                    observer.observe(self.query(container));
                }
            } else if (eventId == 'inViewport') {
                if (container) {
                    const observer = new IntersectionObserver((entries, observer) => {
                        entries.forEach(function (entry) {
                            /* self.log('entry');
                            self.log(entry);
                            self.log('entry.target');
                            self.log(entry.target); */
                            /*
                            self.log(entry.intersectionRatio);
                            self.log(entry.isIntersecting);
                            self.log(entry.isVisible); */
                            // isIntersecting, isVisible
                            //var element = self.query(entry.target);
                            //self.log(entry.intersectionRatio);
                            if (entry.intersectionRatio > 0) {
                                self.onEvent(entry.target, { type: 'inViewport' });
                            }
                        });
                    }, {
                        rootMargin: '0px',
                        threshold: [0, 1] // if 0.5 is the half-height of the elmement
                    });
                    //const element = self.query(container);
                    observer.observe(self.query(container));
                }
            } else if (eventId == 'outViewport') {
                const observer = new IntersectionObserver((entries, observer) => {
                    entries.forEach(function (entry) {
                        /* self.log(entry);
                        self.log(entry.isIntersecting);
                        self.log(entry.isVisible); */
                        // isIntersecting, isVisible
                        //var element = self.query(entry.target);
                        //self.log(entry.intersectionRatio);
                        if (entry.intersectionRatio == 0) {
                            self.onEvent(entry.target, { type: 'outViewport' });
                            /* var el = entry.target;
                            if (el.getAttribute('in') == 'true') {
                                el.setAttribute('in', 'false');
                                self.onEvent(entry.target, {type: 'out'});
                            } */
                        }
                    });
                }, {
                    rootMargin: '0px',
                    threshold: [0, 1] // if 0.5 is the half-height of the elmement
                });
                const element = self.query(container);
                observer.observe(element);


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

                /* if (container == '.nav-link[data-value="docs"]') {
                    alert(JSON.stringify(params));
                    //element = element[0];
                    $(element).hide();
                } */
                //element.removeEventListener(eventId);
                // $(container).off(eventId);
                if (element) {

                    // TO DO: verify
                    //if (element.getAttribute('listener') !== 'true') { // only one mouse event
                    //self.log('addEventListener: ' + eventId); 
                    element.addEventListener(eventId, function (event) {
                        //$(container).off(eventId).on(eventId, function (event) {
                        /* self.log('event');
                        self.log(event); */
                        element.setAttribute('listener', 'true');
                        //event.preventDefault();
                        //event.stopPropagation();
                        self.onEvent(this, event);
                    });
                    //} 
                    if (eventId == 'mousedown' || eventId == 'mouseup' || eventId == 'click')
                        element.style.cursor = 'pointer';
                }

                /* $(container).off(eventId).on(eventId, function (event) {
                //$(container).off(eventId).on(eventId, function (event) {
                    self.log('event');
                    self.log(event);
                    event.preventDefault();
                    event.stopPropagation();
                    self.onEvent(this, event);
                }); */
                //$(container).css({cursor: 'pointer'});
            }
        }

    };

    /* this.addAction = function (params, selectorParams) {
        var container = (selectorParams) ? self.selector(selectorParams) : params.container;
        var selector = self.selector(self.extend({}, params, selectorParams));

        $(selector).data('onData', params);
        //$(selector).data('onData', {action: params.action, params: params.args});
        $(selector).off().on(this.touch, function (event) {
            event.stopPropagation();
            self.onEvent(this, event);
        });
        $(selector).css({cursor: 'pointer'});
    } */
    /*     this.ace = function (params) {
            params.container = 'blockCode';
            var editor = ace.edit("blockCode");
            var langTools = ace.require('ace/ext/language_tools');
            editor.setTheme("ace/theme/monokai"); // ambiance
            editor.getSession().setMode("ace/mode/json");
            //editor.renderer.setOption('showLineNumbers', false);
            editor.container.style.background="rgba(255,255,255,0)";
    
            var staticWordCompleter = {
                getCompletions: function(editor, session, pos, prefix, callback) {
                    var wordList = ["app", "setup", "pages"];
                    callback(null, wordList.map(function(word) {
                        return {
                            caption: word,
                            value: word,
                            meta: "static"
                        };
                    }));
            
                }
            }
            editor.completers = [staticWordCompleter];
    
            // https://github.com/ajaxorg/ace/wiki/Configuring-Ace
            editor.setOptions({
                fontSize: "11pt",
                selectionStyle: "text",
                highlightActiveLine: true,
                wrap: true,
                showLineNumbers: false,
                showGutter: false,
                fixedWidthGutter: false,
                readOnly: true,
                //enableBasicAutocompletion: true,
                //enableSnippets: true,
                //enableLiveAutocompletion: true,
            });
            editor.setValue(var.code, -1);
        } */
    /*     this.edit = function (item) {
            //var.code = JSON.stringify(item, null, '\t');
    
            self.do( {
                "alert": {
                    "background": "rgba(255,255,255,0.9)",
                    "width": "93%",
                    "showCancelButton": true,
                    "cancelButtonText": 'Cancel',
                    "confirmButtonText": 'Save',
                    "html": '<div id="blockCode" align="left" style="background: transparent; margin: 0; width: 93%; height:86vh"></div>'
                }
            });
            //self.code(JSON.stringify(item, null, '\t'),);
            self.code(item, {container: '#blockCode'});
    
            //setTimeout(function () {
                
                //alert(var.blockCode);
                // https://codepen.io/jjsjjja/pen/VoGYRB
            //}, 100);
        } */
    this.money = function (value) {
        /* if (value && value.indexOf(',') == -1 && value.indexOf('.') == -1) {
            value = value + '.00';
        } */
        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;
    };


    this.offcanvas = function (params) {
        self.log('offcanvas');
        self.log('params');
        self.log(params);
        // https://getbootstrap.com/docs/5.0/components/offcanvas/
        // to add: toggle, getInstance, getOrCreateInstance
        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);
        // do: show (default) / hide (see boostrap Offcanvas docs)
        if (element) {
            if (params.do == 'hide') {
                self.css({
                    removeClass: 'show'
                }, params);
                //new bootstrap.Offcanvas(element).hide(); // doesn't work
            } else {
                new bootstrap.Offcanvas(element).show();
            }
        } else {
            self.log('"offcanvas" method can\'t find the selected element');
        }
    };

    /**
     * Converts a DOM element to its CSS selector string representation
     * @param {HTMLElement} element - The DOM element to convert
     * @returns {string} The CSS selector string that uniquely identifies the element
     */
    this.elementToSelector = function (element) {
        // 1. Use ID if available
        if (element.id) {
            return '#' + element.id;
        }

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

        // 3. Use class if unique
        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;
                    }
                }
            }
        }

        // 4. Use tag if unique
        const tagName = element.tagName.toLowerCase();
        const sameTag = document.querySelectorAll(tagName);
        if (sameTag.length === 1) {
            return tagName;
        }

        // 5. Fallback to tag + nth-child path
        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(' > ');
    };


    this.onEvent = function (element, event) {
        //self.log('element');
        //self.log(element);
        /* self.log('event');
        self.log(event); */
        /* if (event) {
            event.preventDefault(); // verify
            event.stopPropagation();
        } */
        /* var item = {};
        if (self.dataStorage.has(element, 'onData'))
            item = self.dataStorage.get(element, 'onData'); */
        var item = self.dataStorage.get(element, 'onData');
        var code = self.dataStorage.get(element, 'code') || '';
        //var item = $(element).data('onData');
        //var code = $(element).data('code');
        //var value = String($(element).data('value'));
        //var selector = $(element).data('selector');
        //var value = String(self.dataStorage.get(element, 'value'));
        var value = element.getAttribute('data-value');
        //var selector = element.getAttribute('data-selector');
        var selector = self.elementToSelector(element); // 1.0.12








        /* self.log('value');
        self.log(value);
        self.log('args');
        self.log(args); */
        var eventType = (event.type && item.on) ? item.on[event.type] : undefined;

        /* self.log('onEvent');
        self.log('item');
        self.log(item);
        self.log('selector');
        self.log(selector);
        self.log('event.type');
        self.log(event.type); */
        /*
        if (event) {
            args = {
                value: value,
                event: eventType
            }
        }
        */
        /*  self.log('event.type');
            self.log(event.type);
            self.log('event');
            self.log(event);  */
        var action, specialKey;

        if (item.on) {

            if (event.button == '2' && event.type == 'mousedown') { // right click / long touch

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

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

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

                //const specialKeys = ['shiftKey', 'altKey', 'cmdKey'];
                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) {
            //console.dir(action);
            //action = self.replaceProperty(action, '{on:target}', targetId);
            if (event.target) {
                //self.log(event);
                var actionString;
                var targetId = event.target.id;
                var targetClass = event.target.className;
                var targetValue = event.target.dataset.value;
                var targetSelector = event.target.dataset.selector; // deprecated



                /* self.log('event');
                self.log(event); */
                //if (targetValue) alert(targetValue);
                targetSelector = self.replaceAll(targetSelector, '"', '\''); // TO DO: if JSON5?
                action = self.stringify(action);
                if (typeof targetId !== 'string') targetId = '';
                if (typeof targetClass !== 'string') targetClass = '';

                /* actionString = self.replaceAll(action, '{event.target.id}', targetId);
                actionString = self.replaceAll(actionString, '{event.altKey}', event.altKey);
                actionString = self.replaceAll(actionString, '{event.target.className}', targetClass);
                actionString = self.replaceAll(actionString, '{event.target.data-value}', targetValue); // obsolete
                actionString = self.replaceAll(actionString, '{event.target.dataset.value}', targetValue);
                actionString = self.replaceAll(actionString, '{event.target.dataset.selector}', targetSelector); */
                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); // obsolete
                actionString = self.replaceAll(actionString, '{on:target.dataset.value}', targetValue);
                actionString = self.replaceAll(actionString, '{on:target.dataset.selector}', targetSelector); // deprecated
                actionString = self.replaceAll(actionString, '{on:which}', event.which);
                actionString = self.replaceAll(actionString, '{on:clientY}', event.clientY);
                actionString = self.replaceAll(actionString, '{on:clientX}', event.clientX);

                // custom
                actionString = self.replaceAll(actionString, '{on:value}', targetValue);
                actionString = self.replaceAll(actionString, '{on:dragging}', (event.which > 0));
                //actionString = self.replaceProperty(actionString, 'on', event);
                if (this.isJson(actionString))
                    action = self.parse(actionString);

                else
                    action = actionString;
            }

            // TODO: let decide to the user the parameter to pass
            var args = item.args; // || item.params || item.value || value;




            //args.event = event;
            //if (args)
            //    action = self.replaceResult(action, args);
            let specialKeyLog = (specialKey) ? ' + ' + specialKey : '';
            //self.log('event ' + event.type + specialKeyLog);
            //self.log('action');
            //self.log(action);
            /* self.log('args');
            self.log(args);
            self.log('{container: selector}');
            self.log({container: selector}); */
            //self.do(action, args, {selector: selector}); // TO DO: only if mouse over or
            self.do(action, { selector: selector }, args);
        } // (action)

    };

    this.pluginsFunctionAvailable = []; // should use pluginsLoaded

    self.pluginsLoader = function (packs, callback, params) {
        /* self.log('pluginsLoader');
        self.log('packs');
        self.log(packs);
        self.log('callback');
        self.log(callback);
        self.log('params');
        self.log(params);  */
        //self.pluginsFunctionAvailable.push(name); // posizione giusta
        var filesToLoad = [];

        for (pack of packs) {
            /* self.log('pack');
            self.log(pack); */
            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]; // TO DO: obsolete, remove
                    version = pack.version;
                }

                /* if (params) {
                    if (!Array.isArray(params[0])) params[0] = [params[0]];
                    if (!self.pluginsCallbacks[pack.name]) self.pluginsCallbacks[pack.name] = [];
                    self.pluginsCallbacks[pack.name].concat(params[0]);
                } */
                self.pluginsLoaded[pack.name] = version; // should contain the queue of actions to do after the plugin loads




                // TO DO: move in Promise.all(promises).then(function() { 
                // it needs to feed a queue of actions to do after the plugin loads
                if (!Array.isArray(pluginsFiles)) // multi-file plugin
                    pluginsFiles = [pluginsFiles];

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

        }

        //self.log(filesToLoad);
        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 }));
                //promises.push(loadPlugin(url));
            });

            Promise.all(promises).then(function (files) {
                // all filesToLoad are loaded (all the Promise in loadPlugin are solved)
                /* self.log('all plugin files are loaded');
                self.log(files);
                self.log(params); */
                callback(...params); // change with ...


                //callback(params[0], params[1], params[2]); // change with ...
            }).catch(function (script) {
                self.log('failed to load error');
                self.log(item);
                self.log(script);
            });
        }
    };


    this.pluginsRequired = function (params) {
        /* self.log('pluginsRequired');
        self.log(params); */
        // analize function object or array of objects
        if (self.json.resources) {
            const pluginsFunctions = self.json.resources.pluginsFunctions;
            var pluginsRequired = [];
            if (typeof params == 'string') {
                /* self.log('params');
                self.log(params); */
                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];
                /* self.log('objArray');
                self.log(objArray); */
                for (var obj of objArray) {
                    /* self.log('obj');
                    self.log(obj); */
                    for (var key in obj) {
                        var mainFunction = (key.indexOf('.') >= 0) ? key.substr(0, key.indexOf('.')) : key;
                        //self.log('mainFunction');
                        //self.log(mainFunction);
                        if (pluginsFunctions[mainFunction]) {
                            if (!self.pluginsLoaded[pluginsFunctions[mainFunction].name])
                                pluginsRequired.push(pluginsFunctions[mainFunction]);
                        } /*  else if (key == 'var') {
                            const functionName = self.functionName(obj.var.value);
                            if (functionName) {
                                if (!self.pluginsLoaded[pluginsFunctions[functionName].name])
                                    pluginsRequired.push(pluginsFunctions[functionName]);
                            }
                        } */






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

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

    /*
    this.runParts = function (actionObj, container, argumentsPar, handleOtherActions) {
        console.log('runParts');
        console.log(actionObj);
        if (Array.isArray(actionObj)) {
                for (var action in actionObj)
                    self.runParts(action);
        } else {
            var otherMethods = false;
            var otherActions = {};
            for (var partKey in actionObj) {

                if (partKey.startsWith('var ') || partKey.startsWith('data ') || partKey.startsWith('set '))
                    self.setData({path: partKey, value: actionObj[partKey]});
                else {
                    var methodOrPart = false;
                    var method = self.element({path: partKey, root: self.methods});
                    if (method) { // search in jsonic.methods
                        methodOrPart = true;
                        if (typeof method == 'function') {
                            //self.docElement(actionObj[partKey]);
                            self.log('method: ' + partKey);
                            method(actionObj[partKey], container, argumentsPar);
                        }
                    } else if (self.json.parts) {  // search in self.json.parts
                        var part = self.element({path: 'parts.'+self.replaceAll(partKey,':','.')});
                        if (part) {
                            part = self.replaceProperty(part, 'arg', actionObj[partKey]);
                            if (actionObj[partKey].setup)
                                part = self.replaceProperty(part, 'setup', actionObj[partKey].setup);
                            console.log('part');
                            console.log(part);
                            self.run(part, container);
                            methodOrPart = true; // if (!actionFound) { will go in the else (!part)
                        }
                    }
                    if (!methodOrPart) otherActions[partKey] = actionObj[partKey];
                    else otherMethods = true;
                }
            }

            //if (handleOtherActions) handleOtherActions(otherActions, container, argumentsPar);
            return otherMethods;
        }
    }
    */
    /* this.jsonicMethods = function (actions, container, argumentsPar) {

        // 'icons' should be removed from the json files

        var withContainer;
        var functionFound = false;

        for (var actionKey in actions) {

            //if (nodes.functions.indexOf(actionKey) >=  0) {

            switch (actionKey) {

                case 'for':
                case 'editor':
                case 'ace':
                case 'code':
                case 'qrcode':
                case 'lottie':
                case 'animate':
                case 'html':
                case 'hide':
                case 'show':
                case 'toggle':
                case 'in':
                case 'out':
                case 'if':
                case 'ajax':
                case 'attr':
                case 'run':
                case 'delay':
                case 'css': withContainer = true; break;
                default: withContainer = false;
            }
            var functionObj = actions[actionKey];
            if (functionObj) {
                functionFound = true;
                if ((functionObj.if == undefined) || self.if(functionObj.if, container, argumentsPar)) {
                    return self.doFunction({
                        name: nodes.functions[actionKey],
                        function: functionObj,
                        args: argumentsPar,
                        selector: container,
                        withContainer: withContainer
                    });
                } else {
                    if (functionObj.if !== undefined) {
                        self.log(self.js(functionObj.if, argumentsPar));
                    }
                }
            } else if (nodes.params.indexOf(actionKey) < 0)
                return self.method(actions[actionKey], argumentsPar);

        }

                
    } */
    /*    this.do = function (actionPar, argumentsPar, selectorParams) { // ex this.action
    
            if (actionPar !== undefined && actionPar !== null) { //  !== undefined
    
                // check plugins required
                let pluginsRequired = self.pluginsRequired(actionPar);
                if (pluginsRequired.length > 0) {
                    let params = [actionPar, argumentsPar, selectorParams];
                    self.pluginsLoader(pluginsRequired, self.do, params);
                
                } else {
    
                    if (Array.isArray(actionPar)) {
    
                        if (actionPar.length > 0) {
                            var actionArray = [];
                            for (var index in actionPar)
                                if (typeof actionPar[index] == 'object')
                                    actionArray[index] = self.cloneObject(actionPar[index]);
                                else
                                    actionArray[index] = actionPar[index];
                            //actionArray = actionArray.concat(actionPar);
                            //actionArray = self.cloneObject(actionPar);
                            var actionFunction = actionArray.shift();
                            self.do(actionFunction, argumentsPar, selectorParams);
                            if (actionArray.length > 0)
                                self.do(actionArray, argumentsPar, selectorParams);
                        }
                    } else {
    
                        if (typeof actionPar == 'function') {
    
                            return actionPar(argumentsPar);
    
                        } else if (typeof actionPar == 'string') {
    
                            var actionPart = self.element({path: 'parts.' + actionPar}); // it includes replaceProperties
                            self.log('actionPar stringa');
                            self.log(actionPar);
                            if (actionPart)
                                return self.do(actionPart, argumentsPar);
                            //else if (json.actions[actionPar]) // obsolete
                            //    return self.do(json.actions[actionPar], argumentsPar);
                            else
                                return actionPar; //return self.js(actionPar); // TO DO: finisce qui anche una stringa di un tag p ("p": "string")
    
    
                        } else if (typeof actionPar == "object") {
    
                            var actionObj = actionPar;
                            var container = actionObj.selector || actionObj.container || selectorParams;
    
                            // events
                            if (actionObj.on) {self.on(actionObj.on, container);}
    
    
                            if (self.notEmptyObject(actionObj)) {
    
    
                                var actionFound = self.runParts(actionObj, container, argumentsPar, self.jsonicMethods);
    
    
                                if (!actionFound) {
    
                                    var functionFound = false;
    
                                    for (var index in nodes.functions) {
                                        //if (nodes.params.indexOf(actionKey) < 0)
    
                                        //var action = Object.keys(actionObj)[0];
                                        var withContainer = (nodes.functionsWithContainer.indexOf(nodes.functions[index]) >=0 );
                                        var functionObj = actionObj[nodes.functions[index]];
                                        if (functionObj) {
                                            functionFound = true;
                                            if ((functionObj.if == undefined) || self.if(functionObj.if, container, argumentsPar)) {
                                            //if ((functionObj.if == undefined) || Boolean(self.js(functionObj.if, argumentsPar))) {
                                                return self.doFunction({
                                                    name: nodes.functions[index],
                                                    function: functionObj,
                                                    args: argumentsPar,
                                                    selector: selectorParams,
                                                    withContainer: withContainer
                                                });
                                            } else {
                                                if (functionObj.if !== undefined) {
    
                                                    self.log(self.js(functionObj.if, argumentsPar));
                                                }
                                            }
                                        }
                                    }
    
                                    if (!functionFound) {
                                        self.log('actionPar');
                                        self.log(actionPar);
                                        self.log('!functionFound');
                                        self.log(actionObj);
                                        return self.method(actionObj, argumentsPar);
                                    }
                                    
                                }
                            }
                            
                        } else {
                            //self.log('Function not found');
                            //self.log(actionPar);
                            //self.log(argumentsPar);
                            return actionPar;
                            // functions from this.do
                            //actionPar(argumentsPar);
                        }
                    }
                }
            } else {
                // TO DO: check
                //self.log('function with actionPar undefined');
            }
    
        }
    */
    /*    this.jsonicMethod2 = function (actionObj, selectorParams, argumentsPar) {
    
            var functionFound = false;
    
            for (var action in actionObj) {
                var functionObj = actionObj[action];
                if (functionObj) {
                    var withContainer = (nodes.functionsWithContainer.indexOf(action) >=0 );
                    functionFound = true;
                    if ((functionObj.if == undefined) || Boolean(self.js(functionObj.if, argumentsPar))) {
                        return self.doFunction({
                            name: action,
                            function: functionObj,
                            args: argumentsPar,
                            selector: selectorParams,
                            withContainer: withContainer
                        });
                    }
                }
    
                if (!functionFound) {
                    self.log('!functionFound');
                    self.log('actionObj');
                    self.log(actionObj);
                    return self.method(actionObj, argumentsPar);
                }
            }
    
        
        } */
    this.for = function (params, selectorParams, args) {
        /* self.log('for');
        self.log(params); */
        let container = self.selector(params.selector || params.container) || self.selector(selectorParams);

        // TO DO: doesn't go as an automous function (needs to be inside a div)
        /* self.log('for');
        self.log('container');
        self.log(container); */
        //if (params.html) { // new version
        let itemsName = params.id || params.name;
        //self.log('itemsArray');
        //self.log(itemsArray);
        /* if (params.reverse) {
            var itemsReversed = {};
            for (var key in Object(itemsArray).keys().reverse()) {
                itemsReversed[key] = itemsArray[key];
            }
            itemsArray = itemsReversed;
        } */
        if (params.of) {
            //var itemsArray = self.cloneObject(params.of); // value is the previous array id
            //if (typeof itemsArray == 'string')
            //itemsArray = self.compile(itemsArray, args); // compile includes functions and create bugs
            //self.log('params.of');
            //self.log(params.of);
            var itemsArray = self.replaceProperties(params.of, args);


            /* self.log('itemsArray');
                self.log(itemsArray);  */
            /* if (typeof itemsArray == 'string') {
                itemsArray = self.js(itemsArray); // TO DO: check
            } */
            if (itemsArray) {

                /* if (typeof itemsArray == 'string') {
                    if (itemsArray.startsWith('js:')) itemsArray = self.js(itemsArray.substr(3));
                    //else itemsArray = self.replaceProperties(itemsArray, args);
                } */
                /*  else {
                    itemsArray = self.actionResult(itemsArray, args);
                    itemsArray = self.replaceProperties(itemsArray, args);
                } */
                /* self.log('itemsArray replaced');
                self.log(itemsArray); */
                /* self.log('for'); */
                /* self.log(itemsName); */
                /* self.log(itemsArray); */
                if (params.html) { // 1.0.6 html in for is deprecated
                    if (!params.do) params.do = [];
                    else if (!Array.isArray(params.do)) params.do = [params.do];
                    params.do.push({ html: params.html });
                }


                //delete params; // needed to avoid infinite loop
                if (typeof itemsArray == 'object') {

                    let indexNum = 0;

                    for (var index in itemsArray) {

                        var item = itemsArray[index];

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

                        if (typeof item.output == 'object') { // TO DO: obsolete

                            /* self.log('item.for.output');
                            self.log(item.for.output); */
                            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.compile(output, args); 
                                    output = self.replaceProperties(output, args);
                                }

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



                                //item = self.replaceProperty(item, key, output);
                            }
                        }

                        if (params.do.length > 0) {

                            //alert(JSON.stringify(params.do));
                            //self.do(self.replaceProperty(params.do, itemsName, item), undefined, {selector: container});
                            //let objWithIndex = self.replaceStringInObject(params.do, '{' + params.index + '}', index);
                            let objWithIndex = params.do;

                            if (params.var) { // TO DO: obsolete (remove when used)
                                self.element({ path: params.var, value: item, root: self.json.var });
                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, 'index:' + params.var, index);
                                //objWithIndex = self.replaceStringInObject(objWithIndex, '{index:'+params.var+'}', index);
                            }

                            if (itemsName) {
                                //objWithIndex = self.replaceStringInObject(objWithIndex, '{index:'+itemsName+'}', index);
                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, 'index:' + itemsName, index);
                                objWithIndex = self.replacePropertyWithPrefix(objWithIndex, itemsName, item);
                                //objWithIndex = self.replaceProperty(objWithIndex, 'value:'+itemsName, item);
                                //objWithIndex = self.replaceProperty(objWithIndex, itemsName, item); // obsolete
                            }

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

                        /* if (params.html) { // should be inside a do // commented from 1.0.6
                            let indexName = params.index || 'index';
                            let objWithIndex = self.replaceStringInObject(params.html, '{'+indexName+':'+itemsName+'}', index);
                            objWithIndex = self.replaceProperty(objWithIndex, itemsName, item);
                            self.do(objWithIndex, {selector: container});
                        } */
                        indexNum += 1;

                    }
                }
            }

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

            /* var step = Number(params.step) || 1;
            var from = Number(params.from) || 0;
            var to = Number(params.to); */
            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));
            /* var step = Number(self.replaceProperties(params.step)) || 1;
            var from = Number(self.replaceProperties(params.from)) || 0;
            var to = Number(self.replaceProperties(params.to)); */
            /* self.log('from');
            self.log(from);
            self.log('to');
            self.log(to);
            self.log('step');
            self.log(step); */
            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 });

                    // TO DO: find "if (params.html) { // 1.0.6" and modify also the following code 
                    var item = self.replaceItems(params, String(index), itemsName); // obsolete: replaced with {index:itemsName}










                    //var item = self.replaceProperty(params, itemsName, String(index));
                    /* self.log('itemsName');
                    self.log(itemsName);
                    self.log('index');
                    self.log(index);
                    self.log('item');
                    self.log(item); */
                    if (item.output && typeof item.output == 'object') { //} !== undefined) {

                        /* self.log('item.for.output');
                        self.log(item.for.output); */
                        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.compile(output, args); 
                                output = self.replaceProperties(output, args);
                            }

                            item = self.replaceItems(item, output, key);
                            //item = self.replaceProperty(item, key, output);
                        }
                    }

                    // item = self.replaceItems(item, itemValue, itemsName); old version
                    //delete item.for; // needed to avoid infinite loop
                    if (item.do) {
                        params.do = self.replacePropertyWithPrefix(item.do, 'index:' + itemsName, index);
                        //params.do = self.replaceStringInObject(item.do, '{index:'+itemsName+'}', index);
                        self.do(item.do, selectorParams, args); // return?

                        //self.do(item.do, args, selectorParams); // return?
                    }
                    if (item.html) {
                        //params.html = self.replaceStringInObject(item.html, '{index:'+itemsName+'}', index);
                        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'); // link to see the documentation
            self.log(params);
        }
        //}
        // }
    };

    this.htmlTag = function (params, selectorParams, tagParam, mainParam) {
        /* self.log('htmlTag');
        self.log(tagParam); */
        if (Array.isArray(params)) {
            for (var index in params)
                self.htmlTag(params[index], selectorParams, tagParam, mainParam);
        } else {

            //----------------------------------
            // attributes of the tag in the key
            // example: "div id=btn btn"
            // (classes are without identifier)
            //---------------------------------- 
            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 = {};

                    // attributes in key: attribute=value
                    let tagArr = tagClass.split(' ');
                    var attrClass = '';
                    for (let tagProp of tagArr) {
                        let tagAttr = new RegExp("([_a-zA-Z]+[_a-zA-Z0-9-]*)=(.+)"); // [_a-zA-Z0-9-/]
                        if (tagAttr.test(tagProp)) {
                            //console.log('attribute found '+tagProp);
                            let tagPropArr = tagProp.split('=');
                            params.attr[tagPropArr[0]] = tagPropArr[1];
                        } else {
                            attrClass += ' ' + tagProp;
                        }
                    }

                    // classes
                    if (params.attr.class)
                        params.attr.class = params.attr.class + attrClass;
                    else {
                        params.attr.class = attrClass;
                    }
                    //alert(tagParam + '/'+ tagClass+ '#');
                }
            }

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

            self.do(htmlTagObj, selectorParams);

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

        }
    };

    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'; // TO DO: -> html (to test with actual apps)
                    break;
            }
        }
        var paramsObj = {};
        if (typeof params == 'string') paramsObj[mainParam] = params; else paramsObj = params;
        paramsObj.tag = tagParam;
        return paramsObj;
    };



    this.onResize = [];
    this.onHashChange = [];
    this.onScroll = [];
    //let swup;
    this.notEmptyObject = function (obj) {
        return (obj // null and undefined check
            && Object.keys(obj).length > 0);
        // && Object.getPrototypeOf(obj) === Object.prototype
        // https://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object
    };

    this.run = function (params, selectorParams, args) {
        /* self.log('run');
        self.log(params); */
        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;

            /*  self.log('codeToRun');
                self.log(codeToRun); */
            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];
                }

                /*  self.log('codeToRun.on');
                    self.log(codeToRun.on);
    
                    self.log('selector');
                    self.log(selector); */
                // actions
                //if (codeToRun.container) {self.empty(codeToRun.container);} // reset
                // .in SIGNIFICA CONTENITORE (select)
                if (codeToRun.on) { self.on(codeToRun.on, selector); }
                if (codeToRun.do) { self.do(codeToRun.do, selector, args); }

                if (self.notEmptyObject(actions)) {
                    /* self.log('notEmptyObject actions');
                    self.log(actions);
                    self.log('selector');
                    self.log(selector); */
                    return self.do(actions, selector, args);
                    //return self.do(actions, args, codeToRun.in);
                }
            }
        }

        //self.extendJsonFromElement(codeToRun); // style, actions, texts, data, var. no setup, on, do, blocks, container
    };
    /*
        this.execute = function (params, selectorParams, args) { // obsolete
    
            // should create a new jsonicApp instance and assign it to jsonic[id]
            // jsonicApp should load the plugins required and not are already loaded
            // jsonicApp should import the json not already imported
            // jsonicApp should load the icons not already loaded
    
            // jsonicApp should init the database if not already initialized
            // otherwise should copy the db object from window.jsonic?
            
            // to have app relative var and data these should be called as var:clock.... / var:calendar....
    
            // or each app should have an ID (without id the ID is "app")
    
            // to have exclusive setup, var, data/items, pages and to reset var on the second execution
            // for example: window.jsonic['container'] = new jsonicObject(options);
            self.log('execute');
    
            if (params) {
    
                if (params.do == 'init') {
    
                    self.log('init');
                    window.removeEventListener('hashchange');
                    window.removeEventListener('resize');
                    window.removeEventListener('beforeinstallprompt');
    
                    // 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;
                    });
    
                    self.json.setup.page.hash = window.location.hash.substr(1);
    
                    // we consider json.setup.modules instead of self.modules
                    // to be sure to follow the right order of execution
    
                    var modules = []; // we need it to keep the order of the modules given in the setup
                    for (key in json.setup.modules) {
                        var mod = self.replaceProperties(json.setup.modules[key]);
                        modules = modules.concat(self.cloneObject(mod));
                    }
                    
                    //modules.push({name: 'ide', url: 'https://jsonic.io/app/modules/ide.json'}); // TO DO if admin
                    // or can execute global before all the rest
                    // modules can contain {libs and apps}
                    // ide module can be loaded only if required
                    
                    for (var mod of modules) {
                        //self.log(mod.name);
                        var params = self.modules[mod.name];
                        //self.log(params);
    
                        if (params.html) {
    
                            var container = selectorParams || 'body';
    
                            //if (params.app.html) {
    
                                //var documentTitle = self.json.setup.title;
                                //if (documentTitle)
                                //    document.title = documentTitle;
                                //if (document.querySelector('meta[name="description"]'))
                                //    document.querySelector('meta[name="description"]').setAttribute("content", documentDescription);
    
                                self.html(params.html, {container: container});
    
                                // window events
    
                            //}
                        }
    
                        if (params.on) { // -> this.on
    
                            // hashchange
    
                            if (params.on.hashchange) {
                                self.onHashChange.push(params.on.hashchange);
                                window.addEventListener('hashchange', function() {
                                    self.json.setup.page.hash = window.location.hash.substr(1); // potrebbe essere esplicitato sulla app
                                    //var action = self.cloneObject(params.app.on.hashchange);
                                    //self.replaceAll("{result}", window.location.hash);
                                    self.do(self.onHashChange);
                                    self.uiUpdate();
                                });
                            }
    
                            // resize
    
                            window.addEventListener('resize', self.resizeEvent);
                            if (params.on.resize) {
                                self.onResize.push(params.on.resize);
                                window.addEventListener('resize', function () {
                                    self.do(self.onResize);
                                });
                            }
    
    
                            // receiveMessage
    
                            if (params.on.thunkable && params.on.thunkable.receiveMessage)
                                window.receiveMessage(function (message) {
                                    //document.querySelector('#message').value = message;
                                    jsonic.functions(params.on.thunkable.receiveMessage, message, undefined);
                                });
    
    
                            self.on(params.on, {container: container}); // init included
                        }
    
                        //else
                        //self.log('The node "app" requires "container" and "html" properties');
                    }
            
    
                } else {
    
                    //if (params.setup) args = self.extend(args, params.setup); // or params.args?
                    if (params.blocks) self.extendJson(json, {blocks: params.blocks});
                    //if (params.library) self.extendJson(json, {library: params.library}); // obsolete
                    if (params.media) self.extendJson(json, {media: params.media}); // obsolete
                    if (params.functions) self.extendJson(json, {functions: params.functions}); // obsolete
                    if (params.actions) self.extendJson(json, {actions: params.actions});
                    if (params.css) self.extendJson(json, {css: params.css});
                    //if (params.styles) self.extendJson(json, {styles: params.styles});
                    //if (params.iconset) self.extendJson(json, {iconset: params.iconset});
                    if (params.data) self.extendJson(json, {data: params.data});
                    if (params.items) self.extendJson(json, {items: params.items});
                    if (params.var) self.extendJson(json, {var: params.var});
    
                    if (params.html) {
                        var container = selectorParams || 'body'; // is an object
                        if (typeof container == 'string')
                            container = {container: container};
    
    
                        //if (self.inputValue({id: 'module'}) == 'base') {
                                // icons will be obsolete
    
                        //} else {
                            //var setup = extendParams.setup || params.setup;
                            //if (container || html.container) {
    
    
                                //if (params.app.html) {
                                    var code = self.cloneObject(params.html);
                                    //self.log('code');
                                    //self.log(code);
                                    //self.log(JSON.stringify(code));
    
                                    self.html(code, container);
                                //}
                            //} else {
                            //    self.log('execute method requires container parameter');
                            //}
    
                            if (params.on && params.on.init) {
                                self.do(params.on.init, undefined, container);
                            }
                                // args can be params.setup
                        //}
                    } else if (params.module) {
                        var moduleId = self.replaceProperties(params.module);
                        return self.do(self.modules[moduleId]);
                    }
    
    
                }
    
            }
        } */
    /*     this.createApp = function (data) {
            self.log('createApp');
            //self.log(self.modules);
                
            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;
            });
    
            self.json.setup.page.hash = window.location.hash.substr(1);
    
            for (var file in self.modules) {
                var params = self.modules[file];
    
                //if (params.on) self.extendJson(json, {media: params.media});
                // if (params.icons) self.extendJson(json, {icons: params.icons});
                //if (params.functions) self.extendJson(json, {functions: params.functions});
                //if (params.styles) self.extendJson(json, {styles: params.styles});
                //if (params.items) self.extendJson(json, {items: params.items});
                //if (params.var) self.extendJson(json, {var: params.var});
                //if (params.library) self.extendJson(json, {library: params.library});
                
                if (params.app && params.app.container) {
    
                    //var documentTitle = self.json.setup.title;
                    //if (documentTitle)
                    //    document.title = documentTitle;
                    //if (document.querySelector('meta[name="description"]'))
                    //    document.querySelector('meta[name="description"]').setAttribute("content", documentDescription);
    
                    //alert(document.querySelector(params.app.container).isConnected);
                    self.html(params.app.html, {container: params.app.container});
    
                    // window events
    
                    if (params.app.on) { // -> this.on
    
                        // hashchange
    
                        if (params.app.on.hashchange) {
                            self.onHashChange.push(params.app.on.hashchange);
                            window.addEventListener('hashchange', function() {
                                self.json.setup.page.hash = window.location.hash.substr(1); // potrebbe essere esplicitato sulla app
                                //var action = self.cloneObject(params.app.on.hashchange);
                                //self.replaceAll("{result}", window.location.hash);
                                self.do(self.onHashChange);
                                self.uiUpdate();
                            });
                        }
    
                        // resize
    
                        window.addEventListener('resize', self.resizeEvent);
                        if (params.app.on.resize) {
                            self.onResize.push(params.app.on.resize);
                            window.addEventListener('resize', function () {
                                self.do(self.onResize);
                            });
                        }
    
                        // receiveMessage
    
                        if (params.app.on.thunkable && params.app.on.thunkable.receiveMessage)
                            window.receiveMessage(function (message) {
                                //document.querySelector('#message').value = message;
                                jsonic.functions(params.app.on.thunkable.receiveMessage, message, undefined);
                            });
    
                        // init
                        self.log('params.app.on');
                        self.log(params.app.on);
                        self.log('params.app.container');
                        self.log(params.app.container);
    
                        self.on(params.app.on, {container: params.app.container});
                    }
    
                }
                //else
                //self.log('The node "app" requires the "container" property');
            }
    
            
            if (self.pluginsLoaded['swup'] && self.json.setup.swup) {
                let swup = new Swup(self.json.setup.swup);
            }
            
    
            self.do(data);
        } */
    /* this.menuToggle = function () {
        self.log('menuToggle');
        //$('.offcanvas-collapse').toggleClass('open');
        $('#header').toggleClass('headerMenuOpen');
        var.menuOpen = (!var.menuOpen);
        self.log(var.menuOpen);
    }

    this.menuOpen = function () {
        self.log('menuOpen');
        $('#menu').addClass('menuOpen');
        var.menuOpen = true;
        self.log(var.menuOpen);
    }

    this.menuClose = function () {
        self.log('menuClose');
        $('#menu').removeClass('menuOpen');
        var.menuOpen = false;
        self.log(var.menuOpen);
    } */
    /*         this.createMenu = function (data) {
            self.log('createMenu');
            // self.userRole
            var menuId = (typeof data.id == 'function') ? data.id() : data.id;
            var container = data.container || '#header > ul';
            $(container).empty();
    
            for (var index in self.json.menu[menuId]) {
                var selector = container + ' > li:eq(' + index + ')'; // > span';
                var item = self.json.menu[menuId][index];
                // action
                //if (item.page && !item.action) item.action = {page: item.page};
                if (!var.functions[container])
                    var.functions[container] = {};
                var.functions[container][index] = item;
    
                $(container).append('<li id="item'+index+'"></li>');
                item.color = item.color || 'inherit';
                item.background = item.background || 'transparent';
                item.border = item.border || 'transparent';
                
                self.tab(item, {container: selector});
    
            }
            self.do(data);
        } */
    /* this.changeMenu = function (menuId) {
    }
    */
    /*  this.form = function (params, selectorParams) {
            // replaceProperties può essere passato all'inizio di ogni costruttore
            //params.container = params.container || self.selector(selectorParams);
            //var selector = self.selector(params);
            var container = params.container || self.selector(selectorParams);
            //var elementContainer = self.query(container);
            //var selector = self.selector(self.extend({}, params, selectorParams));
            //var element = self.query(selector);
    
            //params.tag = 'form';
            //if (params.id === undefined) {params.id = '';}
            //if (params.value === undefined) {params.value = ''};
            //if (params.class === undefined) {params.class = ''};
    
            //$(container).append('<'+params.tag+' id="'+params.id+'" class="'+params.class+'" data-value="'+params.value+'" data-selector="'+selector+'"></'+params.tag+'>');
            for (var itemKey in params.items) {
                var item = params.items[itemKey];
    
                self.html({
                    "form": {
                        "attr": {
                            "id": params.id,
                            "class": params.class,
                            "data-value": params.data-value || params.value
                        },
                        "div": [
                            {
                                "attr": {
                                    "class": "form-group",
                                    "data-value": itemKey
                                },
                                "html": [
                                    {
                                        "label": {
                                            "attr": {
                                                "for": itemKey,
                                                "text": item.title
                                            }
                                        }
                                    },
                                    {
                                        "input": {
                                            "attr": {
                                                "id": itemKey,
                                                "class": "form-control",
                                                "data-value": "formInput" + itemKey,
                                                "value": item.inputValue,
                                                "type": item.type,
                                                "text": item.title,
                                                "aria-describedby": "note"+itemKey,
                                                "placeholder": "",
                                                "disabled": disabled
                                            }
                                        }
                                    },
                                    {
                                        "small": {
                                            "attr": {
                                                "class": "form-text text-muted",
                                                "data-value": "formNote"+itemKey,
                                                "text": item.note
                                            }
                                        }
                                    },
                                ]
                            }
                        ]
                    }
                }, {
                    container: container
                });
    
            }
    
        } */
    /*    this.addList = function (params, selectorParams) {
            var container = (selectorParams) ? self.selector(selectorParams) : params.container;
            var selector = self.selector(self.extend({}, params, selectorParams));
    
            if (params.id === undefined) {params.id = '';}
            if (params.value === undefined) {params.value = ''};
            if (params.class === undefined) {params.class = ''};
            $(container).append('<ul id="'+params.id+'" class="'+params.class+' list-group" data-value="'+params.value+'"></ul>');
    
            self.list(params, {container: container});
        
        } */
    /*
            this.addBlock = function (params, selectorParams) {
    
            if (params.icons) self.icons(params.icons, selectorParams);
            if (params.tab) self.tab(params.tab, selectorParams);
            if (params.html) self.html(params.html, selectorParams);
            if (params.div) self.div(params.div, selectorParams);
            if (params.ul) self.ul(params.ul, selectorParams);
            if (params.li) self.li(params.li, selectorParams);
            if (params.span) self.span(params.span, selectorParams);
            if (params.img) self.img(params.img, selectorParams);
            if (params.form) self.form(params.form, selectorParams);
            if (params.list) self.addList(params.list, selectorParams);
            //if (params.img) alert(JSON.stringify(params.img.src));
        }
    */
    /*     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 blocksObj = self.get('self.json.library.'+params);
                    if (blocksObj)
                        self.blocks(blocksObj.blocks, selectorParams);
                } else {
                    if (params.library) {
                        self.library(params.library, selectorParams, args); // {container: container}
                    } else {
                        self.addTag(params, selectorParams, args); // {container: container}
                    }
                }
            }
        }
        */
    this.inputValue = function (params) {
        var selector = self.selector(params.selector || params.container);
        var element = document.querySelector(selector); // self.query(p2);
        return element.value;
    };


    /*     this.layout = function (params, selectorParams, args) {
            if (Array.isArray(params)) {
                for (var index in params)
                    self.layout(params[index], selectorParams, args);
            } else {
                if (typeof params == 'string') {
                    var libraryObj = self.get('self.json.layouts.'+params); // todo: get?
                    //var libraryHtml = libraryObj.html || libraryObj.blocks;
                    if (libraryObj)
                        self.layout(libraryObj, selectorParams, args); // TODO warning potential infinite loop (if string)
                    else
                        self.log('"theme" not found: ' + params);
                } else {
                    var libraryHtml = params.html;
                    // can a library component be an object?
                    if (params.setup) args = self.extend(args, params.setup); // or params.args?
                    if (params.functions) self.extendJson(json, {functions: params.functions});
                    if (params.css) self.extendJson(json, {css: params.css});
                    //if (params.styles) self.extendJson(json, {styles: params.styles});
                    if (params.items) self.extendJson(json, {items: params.items});
                    if (params.var) self.extendJson(json, {var: params.var});
                    self.html(libraryHtml, selectorParams, args); // {container: container}
                    //self.(params, selectorParams, args);
                }
            }
        } */
    this.part = this.block = function (params, selectorParams, args) {

        let nested;
        let part;
        let partArguments;

        if (typeof params.do == 'string' && self.json.parts) { // && par.match(/[.>]+/)) 

            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 }); // TO DO: . -> space?



            /* if (par.indexOf(':')>0 && !part) {
                self.firebaseGetPart({localPath:localPath, container: params.selector, arg:params.obj});
            } */
        } else if (typeof params.do == 'object')
            part = params.do;

        if (part) {
            //if (!params.arguments) params.arguments = {};
            // TO DO: Check this. seems a workaround for fantacards/ui:qrcode
            if (params.arguments)
                partArguments = self.replaceProperties(params.arguments);

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

            nested = self.replacePropertyWithPrefix(part, 'setup', partArguments); // {setup} -> {arguments}
            nested = self.replacePropertyWithPrefix(nested, 'arguments', partArguments); // to tix

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

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

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



                //self.log('blockName');
                //self.log(blockName);
                var libraryObj = self.element({ path: 'self.json.blocks.' + blockName }); // todo: get?





                //self.log('libraryObj');
                //self.log(libraryObj);
                //var libraryHtml = libraryObj.html || libraryObj.blocks;
                if (libraryObj)
                    //self.html(libraryObj, selectorParams, args);
                    self.run(libraryObj, selectorParams, args);


                //self.blocks(libraryObj, selectorParams, args); // TODO warning potential infinite loop (if string)
                else
                    self.log('library component not found: ' + params);
            } else {
                self.run(params, selectorParams, args);
                //self.run(params, selectorParams, args);
                //var libraryHtml = params.html || params.app.html;
                /* // can a library component be an object?
                if (params.setup) args = self.extend(args, params.setup); // or params.args?
                if (params.parts) self.extendJson(json, {parts: params.parts});
                if (params.actions) self.extendJson(json, {actions: params.actions});
                if (params.css) self.extendJson(json, {css: params.css});
                if (params.data) self.extendJson(json, {data: params.data});
                if (params.var) self.extendJson(json, {var: params.var});
                if (params.texts) self.extendJson(json, {texts: params.texts});

                self.html(params.html, selectorParams, args); // {container: container}
                //self.do(params, selectorParams, args); */
            }
        }
    };

    this.uiUpdate = function (option) {
        window.dispatchEvent(new Event('resize')); // workaround known ACE bug

        //self.updateCodeEditors(); // workaround known ACE bug
    };

    this.resizeEvent = function (event) {
        //self.log('resizeEvent');
        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 });
                    //self.do(self.resizeActions[element].action, {selector: selector});
                } else {
                    var value = self.resizeActions[selector][property];
                    var result = self.replaceProperties(value) || '';
                    //$(selector).css(property, result);
                    element.style[property] = result;
                }
            }
        }
    };



    /*  var pages = {};
        var pagesSlider;
        
        this.createPages = function (data) {
            self.log('createPages');
    
            //backLink = (self.json.setup && self.json.setup.home && self.json.setup.home[var.mode]) ? self.json.setup.home[var.mode] : '';
            //  class="container-fluid px-0" style="margin-top:'+self.json.setup.main.top+'"
    
            var pageContainer = '#pages'; // 'main > div'
    
            for (var pageKey in self.json.pages) {
                params = self.json.pages[pageKey];
                var pageRoles = (params.roles) ? String(params.roles).split(',') : undefined;
    
                // se il ruolo dell'utente permette la vista della pagina e se non è stata già creata
                if (!params.roles || (pageRoles.indexOf(self.userRole()) >= 0 && document.querySelectorAll('#'+pageKey).length == 0)) { // $('#'+pageKey).length == '#'+pageKey doesn't exist
                    
                    //if (params.preload) {
    
                        params.tag = 'div';
                        //params.id = pageKey;
                        params.class = 'page';
                        params.value = pageKey;
    
                        // the previous definition should be in layout.page
    
                        self.addTag(params, {container: pageContainer});
                    //}
    
    
                } else {
                    self.log('' + self.userRole() + ' users can\'t access to the page '+pageKey);
                }
            }
            
            self.do(data);
        }  */
    this.pageFullScreen = function (onOff) {
        self.log('pageFullScreen');
        if (onOff)
            self.addClass(document.querySelector('body'), 'fullscreen');

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

    //var backLink = 'stage';
    /* this.pageBack = function () {
        self.gotoPage(backLink);
        //window.location.href = '?p='+backLink;
        //$('body').removeClass('nav-open'); // chiude menu
    } */
    this.link = function (link, args) {
        /* function link() {
            [native code]
        } */
        //alert(link);
        link = self.replaceProperties(link, args);
        window.location.href = link;
    };

    this.reload = function () {
        location.reload();
    };

    this.gotoHomePage = function () {
        self.gotoPage(self.homePage());
    };

    this.homePage = function () {
        //alert('homePage');
        if (self.json.pages) {
            if (Object.keys(self.json.pages).length > 0) {
                //var homeKey = Object.keys(self.json.pages).find(key => self.json.pages[key] === 'home');
                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');
        }
    };

    /*  this.activePage = function () { // to check if obsolete
            return var.page;
        } */
    /*         this.slideIndex = function (id) {
            return $('#'+id).index()+1;
        } */
    /*         this.slideTo = function (params) {
            if (pagesSlider) pagesSlider.goTo(self.slideIndex(params.id));
            $('#pages > .seq-canvas > .seq-in').addClass(params.transition);
        } */
    this.updateCodeEditors = function () {
        // workaround known ACE bug:
        // setValue doesn't work if editor is hidden
        // http://jsfiddle.net/e4r7byLn/
        var elements = document.querySelectorAll('.ace_editor');
        if (elements) {
            elements.forEach(function (element, index) {
                var editor = ace.edit(element);
                editor.renderer.updateFull(true);
                //AceColorPicker.load(ace, editor);
                //AceColorPicker.load(ace, editor); // works after some ms
            });
        }
    };

    this.gotoPage = function (pageId, fromHash) {
        self.log('page'); // changePage
        self.log('pageId');
        self.log(pageId);
        self.log('fromHash');
        self.log(fromHash);
        //self.log("var.page");
        //self.log(var.page);
        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) {
                            //var.pagePath = fromHash; // obsolete
                            self.json.setup.page.hash = fromHash;
                        } else {
                            //delete var.pagePath;
                            delete self.json.setup.page.hash;
                        }

                        //var.page = pageId; // activePage
                        //var newHash = pageId;
                        //if (!fromHash) window.location.hash = newHash;
                        var fullScreen = self.json.pages[pageId].fullscreen || false;
                        //self.pageFullScreen(fullScreen);
                        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.out({
                                    "class": "page",
                                    "data-value": self.json.setup.page.id,
                                    "transition": "fadeOutLeft"
                                }); */
                                self.json.setup.page.id = pageId;
                                self.json.setup.page.title = page.title;
                                self.json.setup.page.roles = page.roles;

                                /*                                 // TO DO: don't change document title and description
                                
                                                                // window title
                                                                var documentTitle = self.json.setup.title;
                                                                if (page.title)
                                                                    documentTitle += ' ' + self.text(page.title);
                                                                var documentDescription = self.json.setup.description || '';
                                                                if (documentTitle)
                                                                    document.title = documentTitle;
                                                                if (document.querySelector('meta[name="description"]'))
                                                                    document.querySelector('meta[name="description"]').setAttribute("content", documentDescription); */
                                //$('title').text(windowTitle);
                                // change page
                                self.hide({
                                    "class": "page"
                                });
                                self.in({
                                    "class": "page",
                                    "data-value": pageId,
                                    "transition": "fadeIn"
                                });

                                /* $('.page').hide();
                                $('.page[data-value='+pageId+']').fadeIn(); */
                                //self.resizeEvent();
                                self.uiUpdate(); // resize event trigger









                                /* // page back icon
                                    if (page.back) {
                                    backLink = page.back;
                                    $('#headerLeftIcons').show((page.back));
                                } else {
                                    $('#headerLeftIcons').hide();
                                } */
                            }

                            if (page.update) { // obsolete
                                self.do(page.update); //, {container: '.page[data-value='+pageId+']'}); 

                                //self.do(page.update); //, {container: '.page[data-value='+pageId+']'}); 
                            }

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

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

                            //$('body').removeClass('nav-open'); // chiude menu
                            //$('.offcanvas-collapse').removeClass('open');   
                        } else {
                            if (self.userRole() == 'guest') {
                                /* self.do({
                                    "login": {
                                        "success": {
                                        "reload": true
                                        }
                                    }
                                }); */
                            } else {
                                self.alert('Non hai i privilegi per accedere a questa pagina');
                            }

                        }
                    } else {
                        // pagina non trovata
                    }
                } 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;

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

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

    this.alert = function (params, args) {
        /* self.log('alert');
        self.log('params');
        self.log(params);
        self.log('args');
        self.log(args); */
        var paramsReplaced = self.cloneObject(params);

        //paramsReplaced = self.replaceProperties(paramsReplaced, args);
        //self.log('paramsReplaced');
        //self.log(paramsReplaced);
        if (paramsReplaced) {
            if (paramsReplaced.do && paramsReplaced.do !== 'fire') {
                return Swal[paramsReplaced.do](params);
                // more info on https://sweetalert2.github.io/#methods
            } else {
                if (typeof paramsReplaced == 'string') paramsReplaced = self.text({ string: paramsReplaced, args: args }); // verify



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

                //self.log('paramsReplaced2');
                //self.log(paramsReplaced);
                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 });
                //self.log('paramsReplaced string replaced 2');
                if (paramsReplaced.inputValue) paramsReplaced.inputValue = self.text({ string: paramsReplaced.inputValue, args: args });

                //if (paramsReplaced.html) paramsReplaced.html = self.text({string: paramsReplaced.html, args: args}); // verify
                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.showConfirmButton) paramsReplaced.showConfirmButton = self.text({string: paramsReplaced.showConfirmButton, 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.inputOptions) paramsReplaced.inputOptions = self.text({string: paramsReplaced.inputOptions, args: args});
                if (paramsReplaced.footer) {
                    paramsReplaced.footer = self.text({ string: paramsReplaced.footer, args: args });
                }

                //if (paramsReplaced.upload) alert('paramsReplaced.upload: '+paramsReplaced.upload);
                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> ';
                    // <img src="" alt="" style="background:#333" width="100" height="100" id="hidden-img" />
                    //if (paramsReplaced.onUpload) var.onUpload = paramsReplaced.onUpload;
                    //var.uploadingTarget = paramsReplaced.uploadingTarget;
                }

                //self.log('paramsReplaced3');
                //self.log(paramsReplaced);
                alertObj.confirm = paramsReplaced.confirm; // deprecated
                alertObj.cancel = paramsReplaced.cancel; // deprecated
                alertObj.deny = paramsReplaced.deny; // deprecated
                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);

                // animate
                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) {
                    //swalParams.icon = self.replaceProperties(swalParams.icon, args);
                    if (typeof swalParams.icon !== 'string') {
                        customIcon = self.cloneObject(swalParams.icon);
                        delete swalParams.icon;
                    }
                    /* var swalIcons = []; //"warning", "error", "success", "info", "question"];
                    if (swalIcons.indexOf(params.icon)<0) {
                        customIcon = params.icon;
                        
                        //if (!params.customClass) params.customClass = {};
                        //params.customClass.icon = "customIcon";
                    } */
                }



                //if (swalParams.showConfirmButton) delete swalParams.showConfirmButton;
                // swalParams.update = function() {
                //     self.log('swalParams.update');
                //     self.log(var.updateSwal);
                // }
                // onRender is obsolete
                /* swalParams.onRender = function () {
                    startUploadListeners({
                        container: '.drop-zone',
                        onUpload: params.onUpload,
                        path: params.path
                        //uploadingTarget: var.uploadingTarget
                        //onError:
                    });
                }
*/
                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 () {
                        //Swal.showLoading(); // rimuove i bottoni
                        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);
                    };
                }

                //self.log('swalParams');
                //self.log(swalParams);
                Swal.fire(swalParams).then((result) => {
                    //self.log('alert result');
                    //self.log(result);
                    // TO DO: if (!result) result = {} to avoid errors  
                    var alertResult = result.value;

                    if (alertObj.on) {
                        if (alertObj.on.value && result.value) {
                            //if (alertObj.id) alertObj.values[alertObj.id] = 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);
                                //alertObj.on.value = self.replaceProperty(alertObj.on.value, 'alert', result);
                                self.do(alertObj.on.value, undefined, result.value); // TO DO: remove result.value once removed all {result}

                                //self.do(alertObj.on.value, result.value); // TO DO: remove result.value once removed all {result}
                            }, 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); // TO DO: remove result.value once removed all {result}

                            //self.do(alertObj.on.isConfirmed, result.value); // TO DO: remove result.value once removed all {result}
                        }
                        if (alertObj.on.isDenied && result.isDenied) {
                            alertObj.on.isDenied = self.replacePropertyWithPrefix(alertObj.on.isDenied, 'alert', result);
                            self.do(alertObj.on.isDenied, undefined, result.value); // TO DO: remove result.value once removed all {result}

                            //self.do(alertObj.on.isDenied, result.value); // TO DO: remove result.value once removed all {result}
                        }
                        if (alertObj.on.isDismissed && result.isDismissed) {
                            alertObj.on.isDismissed = self.replacePropertyWithPrefix(alertObj.on.isDismissed, 'alert', result);
                            self.do(alertObj.on.isDismissed, undefined, result.dismiss); // TO DO: remove result.value once removed all {result}

                            //self.do(alertObj.on.isDismissed, result.dismiss); // TO DO: remove result.value once removed all {result}
                        }

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


                    } else {
                        /* if (result.isConfirmed) {
                            self.do(alertObj.confirm, result.value);
                        } else if (result.isDenied) {
                            self.do(alertObj.deny, result.value);
                        } else if (result.dismiss === Swal.DismissReason.cancel) {
                            self.do(alertObj.cancel, result.value);
                        } else if (result.dismiss === Swal.DismissReason.timer) {
                            self.log('Alert closed by the timer');
                        } */
                    }

                    /*

                    Reason	Description	Related configuration
                    Swal.DismissReason.backdrop	The user clicked the backdrop.	allowOutsideClick
                    Swal.DismissReason.cancel	The user clicked the cancel button.	showCancelButton
                    Swal.DismissReason.close	The user clicked the close button.	showCloseButton
                    Swal.DismissReason.esc	The user clicked the Esc key.	allowEscapeKey
                    Swal.DismissReason.timer	The timer ran out, and the alert closed automatically.	timer

                    */
                });

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

                //self.log('customIcon');
                //self.log(customIcon);
                if (customIcon) {
                    /* self.icons({
                        "class": "icon",
                        "svg": customIcon
                    }, {container: '.swal2-icon'}); */
                    self.do(customIcon, { selector: '.swal2-icon' });
                    //self.addTag(customIcon, {container: '.swal2-icon'}); // check
                    self.css({
                        selector: '.swal2-icon',
                        style: {
                            'border': '0px solid transparent'
                        }
                    });
                    self.show({
                        selector: '.swal2-icon'
                    });
                    /* $('.swal2-icon').css({
                        'border': '0px solid transparent'
                    });
                    $('.swal2-icon').show(); */
                }

                if (swalParams.on && swalParams.on.init) // to be repeated in each function
                    self.do(swalParams.on.init);
                //self.do(swalParams.on.init);
            }
        }
    };

    /*   var startUploadListeners = function (params) { // da FIXO
            self.log('startUploadListeners');
            var.upload = params;
            var obj = $(params.container);
            self.log(params);
            obj.on('dragenter', function (e) {
                e.stopPropagation();
                e.preventDefault();
                $(this).css('border', '2px solid #0B85A1');
            });
            obj.on('dragover', function (e) {
                    e.stopPropagation();
                    e.preventDefault();
            });
            
            $(document).on('dragenter', function (e) {
                e.stopPropagation();
                e.preventDefault();
            });
            $(document).on('dragover', function (e) {
                e.stopPropagation();
                e.preventDefault();
                obj.css('border', '2px dotted #0B85A1');
            });
            $(document).on('drop', function (e) {
                e.stopPropagation();
                e.preventDefault();
            });
            // automatically submit the form on file select
    
            obj.on('drop', function (e) {
                    $(this).css('border', '2px dotted #0B85A1');
                    e.preventDefault();
                    var files = e.originalEvent.dataTransfer.files;
                
                    //We need to send dropped files to firebase
                    handleFileUpload(files);
            });
            $(params.container+'-file').on('change', function (e) {
                var files = $(params.container+'-file')[0].files;
                self.log(params.container+'-file');
                self.log(files)
                handleFileUpload(files);
            });
    
        } */
    /*     this.uploadFileNameAndType = function(params){
            self.log('uploadFileNameAndType');
            self.log(params);
            
            var fileArr = params.fileName.split('.');
            var.upload.fileName = params;
            var.upload.fileType = fileArr[1].toLowerCase();
            if (!$('.swal2-act').length) $('.swal2-modal').append('<div class="swal2-act"></div>');
            if (var.upload.fileType == 'ply') {
                $('.swal2-act').css({'display':'none'});
            } else {
                $('.swal2-act').css({'display':'block'});
                $('.swal2-act').append('<button onclick=\"app.onUploadConfirm()\" type=\"button\" class=\"swal2-confirm swal2-styled\" aria-label=\"\" style=\"display: inline-block; border-left-color: rgb(48, 133, 214); border-right-color: rgb(48, 133, 214);\">'+self.text('btnImport')+'</button>');
            }
        } */
    /*     var handleFileUpload = function(files) {
            self.log('handleFileUpload');
            self.log(files);
            self.log('var.upload');
            self.log(var.upload);
            //var auth0Id = fixo.getAuth0Id();
            // Swal.close(); // chiusura popup
            for (var i = 0; i < files.length; i++) {
                var fd = new FormData();
                fd.append('file', files[i]);
                if (!files[i].name.endsWith('png')) var.upload.fileTimestamp = self.getTimestamp();
    
                self.fireBaseUpload({
                    'file': files[i], //base64toBlob(files),
                    'timestamp': var.upload.fileTimestamp,
                    'path': var.user.uid+'/' || '' //auth0Id+'/files/images' //path_to_where_you_to_store_the_file
                }, function (data) {
                    self.log('data');
                    self.log(data);
                    if (!data.error) {
                        //if (data.progress && data.progress == 0) $('.swal2-actions').append('<span class="prog" style="font-size:48px;font-weight:bolder"></span>');
                        if (data.progress >= 0 && data.progress <= 100) {
                            //progress update to view here
                            // self.log('Uploading file');
                            // self.log(var.uploadingTarget);
                            $('#msgFooter').html('<span>'+data.progress+'%</span>')
                        }
                        if (data.progress == 100) {
                            //$(var.uploadingTarget).removeClass('pulseFade');
                            // Swal.close();
    
                            if (data.fileExt !== 'png'){
                                self.log('diverso da png');
                                $('.drop-zone-file').attr('accept','.png');
                                var.upload.data = data;
                            }
    
                            if (var.upload.onUpload) self.do(var.upload.onUpload, data);
                        }
                    } else {
                        //$(var.uploadingTarget).removeClass('pulseFade');
                        self.log(data.error + ' Firebase image upload error');
                    }
                });
            }
        }; */
    /*     var uploadDone = function (data) {
            self.log('uploadDone');
            // download URL here "data.downloadURL"
            var auth0Id = fixo.getAuth0Id();
            var storageRef = firebase.storage().ref();
            var imageRef = storageRef.child(auth0Id+'/files/images/'+data.fileName);
            // Get the download URL
            imageRef.getDownloadURL().then(function(url) {
                self.do(var.onUpload, url);
                // Insert url into an <img> tag to "download"
            }).catch(function(error) {
                self.log(error.code);
                // A full list of error codes is available at
                // https://firebase.google.com/docs/storage/web/handle-errors
    
                switch (error.code) {
                case 'storage/object_not_found':
                    // File doesn't exist
                    break;
    
                case 'storage/unauthorized':
                    // User doesn't have permission to access the object
                    break;
    
                case 'storage/canceled':
                    // User canceled the upload
                    break;
    
                case 'storage/unknown':
                    // Unknown error occurred, inspect the server response
                    break;
                }
            });
        } */
    this.findKey = function (obj, key) {
        return obj.filter(function (item) {
            return Boolean(item[key]);
        });
    };

    /*     this.fireBaseUpload = function(parameters, callBackData) {
            self.log('fireBaseUpload');
            self.log('parameters');
            self.log(parameters);
            
            // Get a reference to the storage service, which is used to create references in your storage bucket
            var storageRef = firebase.storage().ref();
            
            
            var file = parameters.file;
            var path = parameters.path;
            var timestamp = parameters.timestamp;
            //var path = var.user.uid; // non riesco a passare la path come parametro da olo.app.json così come l'errore funzione onUpload
    
            
            self.log('file');
            self.log(file);
            self.log('timestamp');
            self.log(timestamp);
            self.log('path');
            self.log(path);
    
            //Riga originale pre-modifica nome file
    // 		    var fullPath = path + '/' + file.name;
    
            // expected parameters to start storage upload
            var metaData = {'contentType': file.type};
            var fileSize = formatBytes(file.size); // get clean file size
            var fileType = file.type;
            var fileName = file.name;
            var fileExt = fileName.split('.')[1];
            
            self.log(metaData);
            self.log(fileSize);
            self.log(fileType);
            self.log(fileName);
            self.log(fileExt);
            var modFileName = timestamp+'.'+fileExt;
    
            //var fullFilePath = storageRef.child(path+timestamp+'-'+fileName);
            var fullFilePath = storageRef.child(path+modFileName);
            self.log('fullFilePath');
            self.log(fullFilePath);
    
            var nm = fileName.split('.');
            var name = nm[0];
    
            self.log('nm');
            self.log(nm);
            self.log('name');
            self.log(name);
            
    
            
            // Modifica file name generato
            // var name = generateRandomString(16); //(location function below)
            // var n = name+'.'+file.type.replace('image/','');
            // var fullPath = path + '/' + n;
            
            
            //Riga originale pre-modifica nome file
    // 		    var n = file.name;
            
            
            // generate random string to identify each upload instance
            //name = generateRandomString(12); //(location function below)
            
    //		    var uploadFile = storageRef.child(parameters.path).put(file, metaData);
            var uploadFile = fullFilePath.put(file, metaData);
        
            // first instance identifier
            //callBackData({id: name, fileSize: fileSize, fileType: fileType, fileName: n}); 20200724
            callBackData({id: timestamp.toString(), fileSize: fileSize, fileType: fileType, fileName: modFileName, fileExt: fileExt}); // fileName: fileName //id: name
        
            uploadFile.on('state_changed', function (snapshot) {
                var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
                progress = Math.floor(progress);
                self.log('progress');
                self.log(progress);
                callBackData({
                        progress: progress,
                        element: timestamp.toString(),
                        //element: name,
                        fileSize: fileSize,
                        fileType: fileType,
                        fileName: modFileName,
                        fileExt: fileExt});
                        //fileName: fileName
            }, function (error) {
                callBackData({error: error});
            }, function () {
                var downloadURL = uploadFile.snapshot.downloadURL;
                callBackData({
                        downloadURL: downloadURL,
                        element: timestamp.toString(),
                        //element: name,
                        fileSize: fileSize,
                        fileType: fileType,
                        fileName: modFileName,
                        fileExt: fileExt});
                        //fileName: fileName
            });
        } */
    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];
    };

    this.userRole = function () {
        switch (Boolean(self.user('uid'))) {
            case true:
                return 'user'; // user logged in
                break;
            case false:
                return 'guest';
                break;
        }
    };

    this.userIn = function () {
        //var user = firebase.auth().currentUser;
        //if (user.emailVerified) {
        return Boolean(self.user('uid'));
    };

    this.userOut = function () {
        return (!self.user('uid'));
    };

    this.user = function (param) {
        /*         self.log('user');
                self.log(auth); */
        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;
    };

    this.firstName = function (nameParam) {
        var firstNameStr = (nameParam) ? nameParam.split(' ')[0] : '';
        return firstNameStr;
    };

    this.userUid = function () {
        if (auth) return auth.currentUser.uid;
        else return undefined;
    };

    this.userVerified = function () {
        if (auth) return auth.currentUser.emailVerified;
        else return undefined;
    };

    /*         this.value = function (valueParams) {
            if (valueParams.string)
                return valueParams.string
            else if (valueParams.function)
                //return self.doFunctionByName(valueParams.function, window, '');
        }
    */
    this.thunkableMessage = function (message, callback) {
        if (callback) callback(message);
    };

    this.onMessage = {
        alert: "test"
    };

    this.processMessage = function (message) {
        self.alert('processMessage');
        self.do(self.onMessage, message);
        //self.do(self.onMessage, undefined, message);
    };

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

    };

    /*     var ThunkableWebviewerExtension = (function () {
            const postMessageToWebview = (message) => {
                if (window.ReactNativeWebView) {
                    window.ReactNativeWebView.postMessage(message);
                } else {
                    window.parent.postMessage(message, '*');
                }
            };
    
            const getReceiveMessageCallback = (fxn, hasReturnValue) => (event) => {
                if (typeof fxn === 'function') {
                    if (event.data) {
                        let dataObject;
                        try {
                            dataObject = JSON.parse(event.data);
                        } catch (e) {
                            // message is not valid json
                        }
                        if (dataObject && dataObject.type === 'ThunkablePostMessage' && hasReturnValue) {
                            fxn(dataObject.message, (returnValue) => {
                                const returnMessageObject = { type: 'ThunkablePostMessageReturnValue', uuid: dataObject.uuid, returnValue };
                                postMessageToWebview(JSON.stringify(returnMessageObject));
                            });
                        } else if (!hasReturnValue && (!dataObject || dataObject.type !== 'ThunkablePostMessage')) {
                            fxn(event.data);
                        }
                    }
                }
            };
    
            return {
                postMessage: postMessageToWebview,
                receiveMessage: function (fxn) {
                    const callbackFunction = getReceiveMessageCallback(fxn, false);
                    document.addEventListener('message', callbackFunction, false);
                    window.addEventListener('message', callbackFunction, false);
                },
                receiveMessageWithReturnValue: function (fxn) {
                    const callbackFunction = getReceiveMessageCallback(fxn, true);
                    document.addEventListener('message', callbackFunction, false);
                    window.addEventListener('message', callbackFunction, false);
                },
            };
        })(); */
    this.extendFunctions = function (functions) {
        // change it. don't call self.function
        for (var name in functions) {
            if (functions[name].js) {
                try {
                    // create or override function
                    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);
                    // error.column is wrong
                    /* let col = error.column -17;
                    console.log(
                        functions[name].js.substring(0,col)
                        + '%c' + functions[name].js.substring(col, col+1)
                        + '%c' + functions[name].js.substring(col+1), 'color:orange; font-weight:bold', 'color:#ddd')
                    console.log('%c' + error.message, 'color:orange;'); */
                    // since the script is in one row, error.columns is the exact position of the error
                }
            } else {
                self.log(name + ' function requires js property');
            }
        }
    };


    this.function = function (obj, args) {
        /* console.log('function');
        console.log(params);
        console.log('args');
        console.log(args); */
        // TO DO: add "name" parameter to make the js function permanently available in self.methods[name]
        // https://stackoverflow.com/questions/49125059/how-to-pass-parameters-to-an-eval-based-function-injavascript
        //app[func].apply( this, args ); // or app[func]( ...args ); // ES6 (2015)
        // https://stackoverflow.com/questions/1316371/converting-an-array-to-a-function-arguments-list
        //args = self.replaceProperties(args); // needed?
        //try {
        if (obj.name && obj.js) {
            // create or replace function
            self.functions[obj.name] = new Function(obj.js);
        } else if (obj.name && !obj.js) {
            if (self.functions[obj.name]) {
                // execute function
                if (!obj.arguments || !Array.isArray(obj.arguments)) obj.arguments = [obj.arguments];
                try {
                    return self.functions[obj.name](...obj.arguments);
                    //return self.functions[obj.name].apply(null, 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;');
            }
            //return self.functions[obj.name](...obj.arguments); // also args?
        } else if (obj.js) {
            // execute js code as a function
            if (obj.arguments && Array.isArray(obj.arguments)) {
                return new Function(obj.js).call(null, obj.arguments);
                //return new Function( obj.js ).apply(this, obj.arguments );
            } else {
                return new Function(obj.js).call(null, obj.arguments);
            }
        }
        /* } catch (error) {
            self.log('function ERROR');
            self.log(error);
            self.log(obj);
            return false
        } */
    };


    this.eval = this.js = function (code, args) {
        /* self.log('js');
        self.log(code);
        self.log(args); */
        /* if (Array.isArray(code)) {
            let codeCombined = '';
            for (var codePart of code) codeCombined += self.replaceProperties(codePart, args);
            try {
                return eval(code);
            } catch (error) {
                self.log('JS error');
                self.log(error);
                return code;
            } */
        /*  if (typeof code == 'object') {
    
            } else { */

        code = self.replaceProperties(code, args);
        /* self.log('args');
        self.log(args); */
        try {
            //return new Function( code ).call(null, args);
            return eval(code);
        } catch (error) {
            /* self.log('javascript catch on "'+ codeString + '"');
            self.log('error');
            self.log(error);
            self.log('codeString');
            self.log(codeString);
            self.log('args');
            self.log(args);
            return false */
            return code;
        }
        //}
    };

    /*    this.compile = function (obj, args) {
            // functions e addTag potrebbero essere eseguiti da una stessa funzione
            // in reatà addTag è la funzione che andrebbe lanciata quando la chiave
            // dell'azione è tra i tag html
    
            if (obj !== undefined) {
                var result;
                if (typeof obj == 'object')
                    result = self.actionResult(obj, args);
                result = self.replaceProperties(obj, args);
                if (result == 'undefined')
                    return undefined;
                else if (typeof result == 'object') // Array is also an object type
                    return self.do(result, args);
                else if (typeof result == 'function')
                    return result(args);
                else {
                    return result;
                }
            } else {
                return undefined;
            }
        } */
    this.if = function (params, selectorParams, args) {

        // add array
        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)); // remove undefined values
                        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)))); // remove undefined values
                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) {
                //let arr = self.replaceProperties(params.in, args, true);
                //condition = (arr.indexOf(self.replaceProperties(params.value)) >= 0);
                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);
                //var selector = self.selector(params.selector || params.container, selectorParams);
                if (selector) {
                    if (params.exist) {
                        //var element = self.query(params.exist);
                        //condition = Boolean(self.query(selector));
                        condition = Boolean(self.exist(selector));
                        conditionString += ' ' + selector + ' exist ' + params.exist;
                    }
                    if (params.hasClass) { // to be reviewed (self.hasClass need 1 parameter)
                        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);
                        /* self.log('element');
                        self.log(element); */
                        condition = Boolean(self.isDisplay(element, params.isDisplay));
                        conditionString += ' ' + selector + ' isDisplay ' + params.isDisplay;
                    }
                } else {
                    self.log('the "if" function requires "is" or "not" parameter');
                }
            }

            /* conditionString += ': ' + condition;
            self.log(condition, 'grey'); */
            if (condition)
                if (params.then) {
                    // self.console dovrebbe diventare così
                    if (typeof params.then == 'object') {
                        return self.do(params.then);
                        //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);
                    //return self.do(params.else);
                } if (typeof params.else == 'string') {
                    return self.replaceProperties(params.else);
                } else {
                    return params.else;
                }
            }
        } else {
            //self.log(Boolean(params) && params !== 'false');
            return Boolean(self.js(params, args)); // 0.9.7

            //return Boolean(eval(self.replaceProperties(params, args))) // need try
        }
    };

    this.switch = function (params, args) {
        //self.log('switch');
        //self.log(params);
        var value;
        var answer;
        if (params.expression)
            value = String(self.js(params.expression, args));

        else
            self.log('"switch" function requires "expression" parameter');
        //self.log(value);
        if (params.cases) {
            //self.log('params.cases[value]');
            //self.log(params.cases[value]);
            if (value !== undefined && params.cases[value]) {
                if (typeof params.cases[value] == 'object')
                    answer = self.do(params.cases[value], undefined, args);


                //answer = self.do(params.cases[value], 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);


                //answer = self.do(params.cases['default'], args);
                else
                    answer = params.cases['default'];
            }
        } else {
            self.log('"switch" function requires "cases" parameter');
            self.log('answer');
            self.log(answer);
        }
        return answer;
    };


    /*
        this.varAction = this.set = function (params, args) {
            // {name, value, operator}
    
            var value;
            var operator = params.operator;
    
            //self.log('operator');
            //self.log(operator);
    
            if (operator && args) operator = self.replaceProperties(operator, args);
    
    
            //self.log('operator');
            //self.log(operator);
    
            if (params.value !== undefined) { // set
                value = self.js(params.value, args);
                if (params.var) {
                    self.docElement('self.json.var.'+params.var, value);
                } else if (params.name) {
                    var actualValue = self.docElement('self.json.var.'+params.name);
                    if (actualValue == undefined && typeof value == 'string') actualValue = '';
                    if (actualValue == undefined && typeof value == 'number') actualValue = 0;
                    if (actualValue == undefined && typeof value == 'object') actualValue = {};
                    if (operator) {
    
                        if (operator == '+=') actualValue += value;
                        if (operator == '-=') actualValue -= value;
                        if (operator == '/=') actualValue /= value;
                        if (operator == '*=') actualValue *= value;
                        value = actualValue;
                                            }
                    self.docElement('self.json.var.'+params.name, value);
                    //self.log('self.json.var[params.name]');
                    //self.log(self.json.var[params.name]);
                } else {
                    self.log('set command requires "var" parameter');
                }
            } else { // get
                if (params.var) {
                    return self.docElement('self.json.var.'+params.var);
                } else if (params.name) {
                    return self.docElement('self.json.var.'+params.name);
                } else {
                    self.log('set command requires "value" parameter');
                }
            }
            //self.log('value');
            //self.log(value);
            self.log('var ' + params.name + ' = ' + value);
    
        } */
    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);
                //self.do(params.do, args, container)
            }, duration);
        }
    };

    this.firebasePasswordReset = function (paramEmail, paramSuccess, paramError) {
        //firebase.auth().languageCode = 'it';
        firebase.auth().useDeviceLanguage();
        auth.sendPasswordResetEmail(paramEmail).then(function () {
            // Email sent.
            if (paramSuccess) self.do(paramSuccess);
            //if (paramSuccess) self.do(paramSuccess);
        }).catch(function (error) {
            // An error happened.
            if (paramError) self.do(paramError, undefined, error.message);
            //if (paramError) self.do(paramError, error.message);
        });
    };

    this.firebaseUpdateProfile = function (params, paramSuccess, paramError) {
        self.log('firebaseUpdateProfile');
        self.log(params);
        auth.currentUser.updateProfile(params).then(function () {
            // Update successful.
            self.log('User Profile Updated Successfully');
            if (paramSuccess) self.do(paramSuccess);
            //if (paramSuccess) self.do(paramSuccess);
        }).catch(function (error) {
            // An error happened.
            if (paramError) self.do(paramError, undefined, error.message);
            //if (paramError) self.do(paramError, error.message);
        });
    };

    this.firebaseRegister = function (params, success, error) {
        if (params.email && params.password) {
            // log the user in
            auth.createUserWithEmailAndPassword(params.email, params.password).then(cred => {
                self.log(cred.user);
                //var.auth.cred = cred;
                var user = firebase.auth().currentUser;
                /* if (user.emailVerified) {
                } else {
                    self.sendEmailVerification(params);
                } */
                //if (params.success) self.do(params.success);
                if (params.success) self.do(params.success);
            }).catch(error => {
                // Handle Errors here.
                //if (params.error) self.do(params.error, error.message);
                if (params.error) self.do(params.error, undefined, error.message);
            });
        }
    };

    this.firebaseLogin = function (params) {
        self.log('firebaseLogin');
        self.log(params);

        if (auth) {
            auth.signInWithEmailAndPassword(String(params.email), String(params.password)).then(cred => {
                //var.auth.cred = cred;
                var user = firebase.auth().currentUser;
                //firebase.auth().languageCode = 'it';
                if (user.emailVerified) {
                    if (params.success) self.do(self.replaceResult(params, undefined, cred));
                    //if (params.success) self.do(self.replaceResult(params, 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));
                //if (params.error) self.do(self.replaceResult(params, error.message));
            });
        } else {
            // firebase auth non inizializzato
        }
    };

    this.firebaseLogout = function (params) {
        self.log('firebaseLogout');
        firebase.auth().signOut().then(function () {
            self.log('firebaseLogout SUCCESS');
            self.log(params.success);
            if (params.success) {
                //app.stopMeeting();
                localStorage.clear();
                //self.do(params.success);
                self.do(params.success);
            }
        }).catch(function (error) {
            self.log('firebaseLogout ERROR');
            self.log(error);
            //if (params.error) self.do(params.error);
            if (params.error) self.do(params.error);
        });
    };

    // this.firebaseUserVerify = function (params) {
    //     self.log('firebaseUserVerify');
    //     self.log(params);
    //     self.log(params.oobCode);
    //     if (params.oobCode) self.params.codeMail = params.oobCode
    //     self.log(self.params.codeMail);            
    //     //self.do(data);
    //     // inviare mail di verifica            
    // }
    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({
                //toast: true,
                icon: "success",
                title: "verifyingemail", // all\'indirizzo "+params.email+"
                html: "msgverifyingemail",
                //confirmButtonText: "Ok",
                //showCancelButton: false,
                showConfirmButton: false,
                confirm: successAction
            });

        });
    };

    /*         //self.log('function');
            params = self.replaceProperties(params, args); // -> compile
    
            if (typeof params == 'object') {
                // jsonic function
                //return self.do(params, args);
                return self.do(params, args);
            } else if (typeof params == 'function') {
                // javascript function
                return params(args);
            } else if (typeof params == 'string') {
                if (json.actions[params]) {
                    var functionCompiled = self.replaceResult(json.actions[params], args); // replace {result}
                    //functionCompiled = self.replaceProperties(functionCompiled, args);
                    // jsonic predefined function
                    //return self.do(functionCompiled);
                    return self.do(functionCompiled);
                    //return self.actionResult(json.actions[params], args);
                } else {
                    // javascript function name
                    var functionName = self.docElement(params);
                    if (functionName)
                        if (typeof functionName == 'function') {
                            // javascript function
                            return functionName(args)
                        } else {
                            // javascript object
                            return functionName;
                        }
                    else
                        self.log('function undefined: '+ params);
                }
            } */
    this.path = function (pathParams, args, separatorParam) {
        var pathString = '';
        var separator = separatorParam || '/';

        /*  self.log('this.path');
            self.log(pathParams);
            self.log(args); */
        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;
    };


    this.executeFunctionByName = function (functionName, context /*, arguments */) {
        //self.log('executeFunctionByName');
        //self.log(functionName);
        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 {
                //self.log('Error: executeFunctionByName');
                //if (functionName !== undefined) {self.log('functionName:'+functionName);}
            }
        } else {
            self.log('executeFunctionByName');
            self.log('functionName undefined');
        }

    };

    this.functions = {};

    this.methods = {
        // main 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); },

        // TO DO: check
        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); },

        // DA VERIFICARE
        //dayjs: function (params, args) {self.dayjs(params, args)},
        //moment: function (params, args) {self.moment(params, args)},
        //data: function (params, args) {self.data(params, args)},
        //var: function (params, args) {self.var(params, args)},
        //javascript: function (params, args) {self.javascript(params, args)},
        //color: function (params, args) {self.color(params, args)},
        //select: function (params, args) {self.select(params, args)}, 
        // LOGIC & DATA     
        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); },

        // TO DO: check is needs to be renamed to avoid confusion with self.json.data
        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); // backward compatibility TO DO: remove

                    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 });
                    }
                }
            }
        },

        // DOM
        text: function (params, container, args) { self.text(params, container, args); }, // TO DO: -> html
        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 });
            // NOTE: obj = self.element non prende l'oggetto effettivo. quindi element deve contenere tutte le funzioni
            //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);
                /* self.log('value');
                self.log(value); */
                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);
                }
            }
        },

        // should go in utils.json
        calendar: function (params, container, args) { self.calendar(params, args); }, // to check
        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 there remain elements to shuffle.
            while (currentIndex != 0) {
                // Pick a remaining element.
                randomIndex = Math.floor(Math.random() * currentIndex);
                currentIndex--;
                // And swap it with the current element.
                [array[currentIndex], array[randomIndex]] = [
                    array[randomIndex], array[currentIndex]
                ];
            }
            return array;
        },

        // should go in ui.json (or ux) module
        alert: function (params, container, args) { self.alert(params, args); }, // change in swal2
        offcanvas: function (params, container, args) { self.offcanvas(params, args); }, // bootstrap / obsolete

        //in: function (params, container, args) {self.in(params, container, args)},  // bootstrap TO DO: replace in code with ui:in
        out: function (params, container, args) { self.out(params, container, args); }, // bootstrap TO DO: replace in code with ui:out
        qrcode: function (params, container, args) { self.qrcode(params, container, args); }, // obsolete TO DO: replace in code with ui:qrcode
        hide: function (params, container, args) { self.hide(params, container, args); }, // TO DO: replace in code with ui:hide
        show: function (params, container, args) { self.show(params, container, args); }, // TO DO: replace in code with ui:show
        toggle: function (params, container, args) { self.toggle(params, container, args); }, // TO DO: replace in code with ui:toggle
        sortablejs: function (params, container, args) { self.sortablejs(params, args); }, // TO DO: replace in code with ui:sortable
        lottie: function (params, container, args) { self.lottie(params, container, args); }, // TO DO: replace in code with ui:lottie
        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); },
        //editor: function (params, container, args) {self.editor(params, container, args)}, // quill
        // window.dispatchEvent(new Event('resize')); // workaround known ACE bug
        uiUpdate: function (params, container, args) { self.uiUpdate(params, args); },
        swiper: {
            data: {},
            init: function (params, selector, args) {
                //swiperConfig.on = {};
                setTimeout(function () {
                    // waits the plugin... TO DO: add this function to a promise
                    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);
            }
        },

        // should go in browser.json module
        //export: function (params, container, args) {self.export(params, container, args)}, -> browser
        //console: function (params, container, args) {self.console(params, args)},  // obsolete, changed in log
        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); }, // obsolete
        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); },

        // should go in firebase.json module
        database: function (params, container, args) { self.database(params, args); }, // obsolete (check) 
        firebase: function (params, container, args) { self.firebase(params, args); },

        firebaseEvent: {
            action: function (newObj) {
                /* self.logdbRef('firebaseEvent.action');
                self.log('newObj');
                self.log(newObj); */
                /* var path = newObj.path;
                delete newObj.path;
                var pathString = 'self.json.var.db.' + self.replaceAll(path, '/', '.');
                self.log('pathString:'+pathString);
                var dbObj = self.docElement(pathString);
                dbObj = newObj; // update local db */
                var path = newObj.path;
                delete newObj.path;
                //alert(path);
                document.querySelectorAll('[data-firebase="' + path + '"]').forEach(function (element, index) {
                    //$('[data-firebase="'+path+'"]').each(function (index) {
                    // var element = this;
                    var pathString = 'self.json.var.db.' + self.replaceAll(element.getAttribute('data-firebase'), '/', '.'); // data('firebase')
                    var dbObj = self.docElement(pathString);

                    var firebaseValue = newObj[element.getAttribute('data-value')]; // TODO: not works for nested nodes

                    //var template = element.getAttribute('data-template');
                    var template = self.dataStorage.get(element, 'data-template');

                    //if ($(element).is('ul')) {
                    if (element.tagName == 'UL') { // se $(this) è un ul

                        //var dbObj = self.docElement(pathString);
                        // se il percorso ha più nodi e il dato non è presente nel db locale,
                        // va creato un oggetto vuoto per ogni nodo 
                        //var context = element.getAttribute('data-firebase');
                        //self.var.db[context] = newObj; // update local db
                        var liTemplate = template.li || template.template; // template.li retro-compatibility







                        /* self.log('liTemplate');
                        self.log(liTemplate);
                        self.log('newObj');
                        self.log(newObj); */
                        //$(container).empty();
                        element.innerHTML = '';

                        var items = newObj;
                        for (itemKey in items) {
                            var item = items[itemKey];
                            item.key = itemKey;

                            /* if (params.li.class == "deviceItem d-flex flex-wrap align-items-center list-group-item list-group-item-action d-flex align-items-center justify-content-between") {
                            alert(JSON.stringify(params.li));
                        } */
                            //if (dbPath == 'lessons')
                            //prompt(JSON.stringify(liTemplate));
                            // TO DO: replace with replaceProperty
                            var dbParams = self.replaceItems(liTemplate, item, 'item') || '';

                            // var dbParams = self.replaceProperties(liTemplate, undefined, item) || '';
                            // -> TO DO container = self.elementToSelector(element);
                            var container = element.getAttribute('data-selector');

                            self.li(dbParams, { selector: container });
                        }

                        //} else if ( $(element).is('span') || $(element).is('p')) {
                    } else if (element.tagName == 'SPAN' || element.tagName == 'P') {

                        //$(element).text(firebaseValue);
                        element.textContent = firebaseValue;

                    } else if (element.tagName == 'SVG') {
                        // TODO without JQUERY 
                        //$(element).children('text').text(firebaseValue);
                    } else { // se $(this) non è un ul

                        //var dbObj = self.docElement(pathString);
                        // se il percorso ha più nodi e il dato non è presente nel db locale,
                        // va creato un oggetto vuoto per ogni nodo 
                        //var context = element.getAttribute('data-firebase');
                        //self.var.db[context] = newObj; // update local db
                        if (template.blocks || template.html) {
                            var blocksTemplate = template.html || template.blocks;

                            //$(container).empty();
                            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.log('container');
                                self.log(container);
                                self.log('dbParams');
                                self.log(dbParams); */
                                self.html(dbParams, { selector: container });
                            }
                        }

                        if (template.init) self.do(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) {
                        // No user logged in
                        self.log('authStateChanged: GUEST\n');
                        // non bisognerebbe chiedere sempre di fare login 
                    } else {
                        // User logged in
                        self.log('authStateChanged: USER\n');
                        self.log('name: ' + self.user('displayName') + '\nemail: ' + user.email + '\nuid:' + user.uid);
                        //self.log('email verificata');
                        //self.log(self.userVerified()); 
                    }
                    /* self.log('params');
                    self.log(params); */
                    //self.do(params.onAuthStateChanged);
                    self.do(params.onAuthStateChanged);

                    //if (!firebaseInitialized) {
                    // firebaseInitialized = true;
                    //self.do(data);
                    //}
                });
            },

            sendEmailVerification: function (params, container, args) {
                self.sendEmailVerification(params);
            },

            login: function (params, container, args) {
                self.log('params.auth');
                self.log(params.auth); // QUESTO E' {var:auth} MA SEMBRA NON RIMPIAZZARLO
                self.log(args); // QUI MI RIPORTA LA PWD, ma perché ultimo 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.replaceTags({text: params.auth, args: args});
                //var fbAuth = self.replaceResult(params.auth, args);
                var fbAuth = self.replaceProperties(params.auth, args);
                fbAuth.success = params.success;
                fbAuth.error = params.error;
                //alert(JSON.stringify(fbAuth))
                self.firebaseRegister(fbAuth, params.success, params.error);
            },

            updateProfile: function (params, container, args) {
                //params.profile = self.replaceTags({text: params.profile, args: args});
                //params.profile = self.replaceResult(params.profile, args);
                params.profile = self.replaceProperties(params.profile, args);
                self.firebaseUpdateProfile(params.profile, params.success, params.error);
            },

            passwordReset: function (params, container, args) {
                //params.email = self.replaceTags({text: params.email, args: args});
                //params.email = self.replaceResult(params.email, args);
                params.email = self.replaceProperties(params.email, args);
                self.firebasePasswordReset(params.email, params.success, params.error);
            },

            logout: function (params, container, args) {
                self.firebaseLogout(params);
            }
        },

        // should go in mobile.json module
        thunkable: function (params, container, args) { self.thunkable(params, args); },

        // should go in 3d.json module
        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] = {}; // use self.element


                    //let events = ['load', 'preload'];
                    //for (var event in events) {
                    var event = 'load';
                    if (params[event]) {
                        //self.element({root: 'self.methods.modelViewer.events', path: container+'.'+event, value: 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]);
                            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;

                }
            }
        }
    };


    this.googleSignInPopup = function () {
        self.log('googleSignInPopup');
        var provider = new firebase.auth.GoogleAuthProvider();

        firebase.auth().useDeviceLanguage(); // lingua del dispositivo

        firebase.auth()
            .signInWithPopup(provider)
            .then((result) => {
                // @type {firebase.auth.OAuthCredential} 
                var credential = result.credential;
                // This gives you a Google Access Token. You can use it to access the Google API.
                var token = credential.accessToken;
                // The signed-in user info.
                var user = result.user;
                window.location.reload();
                // ...
            }).catch((error) => {
                // Handle Errors here.
                var errorCode = error.code;
                var errorMessage = error.message;
                // The email of the user's account used.
                var email = error.email;
                // The firebase.auth.AuthCredential type that was used.
                var credential = error.credential;
            });
    };


    /* this.googleSignInPopup = function () {
        // [START auth_google_signin_popup]
        self.log('googleSignInPopup');
        var provider = new firebase.auth.GoogleAuthProvider();
        //provider.addScope('https://www.googleapis.com/auth/contacts.readonly'); //check security
        //provider.addScope('https://www.googleapis.com/auth/calendar');
        //var languageApp = jsonic.var.language || 'en';
        //firebase.auth().languageCode = 'it';//languageApp;
        //self.log(provider);
        //return;
        firebase.auth()
            .signInWithPopup(provider)
            .then((result) => {
                // @type {firebase.auth.OAuthCredential}
                var credential = result.credential;
                // This gives you a Google Access Token. You can use it to access the Google API.
                var token = credential.accessToken;
                // The signed-in user info.
                var user = result.user;
                //######### result ########//
                self.log("credential");
                self.log(credential);
                self.log("token");
                self.log(token);
                self.log("user");
                self.log(user);
                window.location.reload();
                // ...
            }).catch((error) => {
                // Handle Errors here.
                var errorCode = error.code;
                self.log('errorCode');
                self.log(errorCode);
                var errorMessage = error.message;
                self.log('errorMessage');
                self.log(errorMessage);
                // The email of the user's account used.
                var email = error.email;
                self.log('email');
                self.log(email);
                // The firebase.auth.AuthCredential type that was used.
                var credential = error.credential;
                self.log('credential');
                self.log(credential);
                // ...
            });
        // [END auth_google_signin_popup]
    } */
    /*     this.auth = function (params, args) {
            self.log('auth');
            self.log(params);
            if (params.do == 'init') {
                auth = firebase.auth();
                auth.onAuthStateChanged(user => {
                    self.json.var.user = user;
                    if (!user) {
                        // No user logged in
                        self.log('authStateChanged: GUEST\n');
                        // non bisognerebbe chiedere sempre di fare login
                    } else {
                        // User logged in
                        self.log('authStateChanged: USER\n');
                        self.log('name: '+self.user('displayName') + '\nemail: '+user.email + '\nuid:'+ user.uid);
                        //self.log('email verificata');
                        //self.log(self.userVerified());
                    }
    
                    //self.log('params.onAuthStateChanged');
                    //self.log(params.onAuthStateChanged);
    
                    self.do(params.onAuthStateChanged);
    
                    //if (!firebaseInitialized) {
                    // firebaseInitialized = true;
                    //self.do(data);
                    //}
                });
            } else if (params.do == 'sendEmailVerification') {
    
                self.sendEmailVerification(params);
    
            } else if (params.do == 'login') {
    
                self.log('params.auth');
                self.log(params.auth);
                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);
    
            } else if (params.do == 'register') {
    
                //var fbAuth = self.replaceTags({text: params.auth, args: args});
                //var fbAuth = self.replaceResult(params.auth, args);
                var fbAuth = self.replaceProperties(params.auth, args);
    
                fbAuth.success = params.success;
                fbAuth.error = params.error;
                //alert(JSON.stringify(fbAuth))
                self.firebaseRegister(fbAuth, params.success, params.error);
                
            } else if (params.do == 'updateProfile') {
    
                //params.profile = self.replaceTags({text: params.profile, args: args});
                //params.profile = self.replaceResult(params.profile, args);
                params.profile = self.replaceProperties(params.profile, args);
                self.firebaseUpdateProfile(params.profile, params.success, params.error);
    
            } else if (params.do == 'passwordReset') {
    
                //params.email = self.replaceTags({text: params.email, args: args});
                //params.email = self.replaceResult(params.email, args);
                params.email = self.replaceProperties(params.email, args);
                self.firebasePasswordReset(params.email, params.success, params.error);
    
            } else if (params.do == 'logout') {
    
                self.firebaseLogout(params);
            }
                // else
                //self.database(params, args);
                
        }
        */
    /*     this.db = function (params, args) {
            if (typeof params == 'string')
                return self.db(params, args);
            else
                self.firebase(params, args);
        } */
    this.console = function (params, args) {

        //value = self.replaceProperties(params);
        //console.log('JSONIC: '+value, 'color:pink');
        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));
        }
    };

    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.actionResult(params.value, 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); // TO DO: return?
                }
            }
        }
    };

    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));
            //var varValue = self.docElement('self.json.var.db.'+name);
            return self.var('db.' + paramsReplaced, args); // to do: change with self.element({path: 'db.'+paramsReplaced}})









            /*
            // questa versione non funzionava. vuol dire che this.var ha un errore nel caso di oggetto
            return self.var({
                name: 'db.'+paramsReplaced
            }, args);

            */
        } /*  else {
            return self.actionResult(params, args);
        } */


    };

    this.firebase = function (params, args) {
        if (params.initializeApp) {
            var firebaseConfig = self.replaceProperties(params.initializeApp);
            firebase.initializeApp(firebaseConfig);
        } else {
            self.database(params, args);
        }
    };

    this.database = function (params, args) {
        //console.log('database');

        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); // local db

                } else {

                    if (params) {
                        if (params.path) {
                            params.path = self.path(params.path, args);
                        }
                        /*  console.log('params.path');
                            console.log(params.path); */
                        /* if (params.value)
                            params.value = params.value; */ //self.value({string: params.value});
                        if (params.value) {

                            /* self.log("params.value pre replace");
                            self.log(params.value); */
                            //params.value = self.replaceTags({text: params.value, args: args});
                            //params.value = self.replaceResult(params.value, args);
                            //params.value = self.replaceProperties(params.value, args);
                            //params.value = self.actionResult(params.value, args);
                            params.value = self.replaceProperties(params.value, args);

                            self.log("params.value post replace");
                            self.log(params.value);
                        }


                        /* self.log('params to FIREBASE');
                        self.log(params); */
                        // params.value = "${var.newDevice}"
                        //var newDevice = `${params.value}`;
                        //var newDevice = `${var.newDevice}`;
                        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);
                                        //if (params.on && params.on.success) self.do(params.on.success, result);
                                    } else {
                                        self.log('firebase get no result.data ERROR');
                                        //if (params.on && params.on.error) self.do(params.on.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, result);
                                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                                    // alert errore di connessione. alert riprova
                                }
                            });
                        } 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);
                                    //if (params.on && params.on.success) self.do(params.on.success, result);
                                } else {
                                    self.log('firebase set ERROR');
                                    if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                                    //if (params.on && params.on.error) self.do(params.on.error, result);
                                    // alert errore di connessione. alert riprova
                                }
                            });
                        } 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, result);
                                    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);
                                    //if (params.on && params.on.success) self.do(params.on.success, result);
                                    // alert errore di connessione. alert riprova
                                }
                            });
                        } else if (params.do == 'addListener') {
                            self.firebaseAddListener(params);
                        } else if (params.do == 'removeListener') {
                            self.firebaseRemoveListener(params);
                        } else if (params.do == 'update') {
                            if (params.value) { // 'set' is the default action with 'value' param
                                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);
                                        //if (params.on && params.on.success) self.do(params.on.success, result);
                                    } else {
                                        self.log('firebase update ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                                        //if (params.on && params.on.error) self.do(params.on.error, result);
                                        // alert errore di connessione. alert riprova
                                    }
                                });
                            } else {
                                self.log('data update requires value param');
                            }
                        } else if (params.do == 'push') {
                            if (params.value) { // 'set' is the default action with 'value' param
                                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);
                                        //if (params.on && params.on.success) self.do(params.on.success,result);
                                    } else {
                                        self.log('firebase push ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                                        //if (params.on && params.on.error) self.do(params.on.error, result);
                                        // alert errore di connessione. alert riprova
                                    }
                                });
                            } else {
                                self.log('data push requires value param');
                            }
                        } else {
                            if (params.value) { // 'set' is the default action with 'value' param
                                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);
                                        //if (params.on && params.on.success) self.do(params.on.success,result);
                                    } else {
                                        self.log('firebase set ERROR');
                                        if (params.on && params.on.error) self.do(params.on.error, undefined, result);
                                        //if (params.on && params.on.error) self.do(params.on.error, result);
                                        // alert errore di connessione. alert riprova
                                    }
                                });
                            }
                        }


                    }
                }
            } else {
                self.log('"database" function needs to be initialised');
            }
        }
    };

    this.ajax = function (params, selectorParams, args) {
        /* self.log('ajax');
        // TO DO: selectorParams
        self.log('ajax');
        self.log('params');
        self.log(params);
        self.log('args');
        self.log(args);  */
        if (params.url) {
            //var url = self.compile(params.url, args);
            var url = self.replaceProperties(params.url, args);
            var data;
            if (params.data)
                data = JSON.stringify(self.replaceProperties(params.data, args));
            //data = JSON.stringify(self.compile(params.data, args));
            /* self.log('data');
            self.log(data); */
            var type = params.type || 'POST';
            var request = new XMLHttpRequest();
            request.open(type, url, true);
            //request.setRequestHeader("Accept", "application/json");
            //request.setRequestHeader('Content-Type', 'application/json');
            /* request.onreadystatechange = function () {
                if (r.readyState != 4 || r.status != 200) return;
                alert("Success: " + r.responseText);
            } */
            request.onload = function () {
                self.log('onload');
                self.log(this);
                if (this.status >= 200 && this.status < 400) {
                    // Success!
                    var result = {};

                    //try {
                    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 */
    this.openFullscreen = function (id) {
        var element = document.getElementById(id);

        if (screenfull.isEnabled) {
            screenfull.request(element);
        }

    };

    /*    this.addListener = function(params) {
            var selector = self.selector(params);
            if (selector)
                $(selector).on(params.event, self.do(params.action));
                //event.stopPropagation();
        } */
    /* Close fullscreen */
    this.closeFullscreen = function () {

        //document.exitFullscreen();
        screenfull.off('change'); // callback
    };

    /*         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++;
                    }
                }
            }
        } */
    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);
        }
    };

    /*         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)'
            });
        }
    */
    //------------------------
    /* 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);
                        }
                    }
                }
            }
        }
    } */
    /*     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);
            }
        }
    
        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);
            }
        }
    
        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);
        }
    
        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;
                }
            }
        }
    
        this.removeTimedTask  = function (data) {
            self.log('removeTimedTask');
            if (data.frequence && data.task) {
                delete timedTasks[data.frequence][data.task];
            }
        }
    
        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"]';
    
        this.closePanel = function(value) {
            self.log(value);
    
            switch (value) {
                case 'closePanelTools':
                    $(toolsPanel).fadeOut();
                    $(previewPanel).fadeIn();
                case 'closePanelFrame':
                    $(devicePanel).fadeOut();
                    $(stagePanel).fadeIn();
            }
            }
    */
    //------------------------
    // CSS METHODS
    //------------------------
    /*     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;
        } */
    /*     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
            }
        }
    
            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;
        } */
    var getClickPosition = function (e) {
        var parentPosition = getPosition(e.currentTarget);
        var xPosition = e.clientX - parentPosition.x;
        var yPosition = e.clientY - parentPosition.y;
    };

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

    /*     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});
        }
    
        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'};
        } */
    /*     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);
        }
            */
    /* 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
    //------------------------
    /*  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') +  '.');});
        } */
    /*     this.setAction = function (data) {
            $(data.object).off().on(self.touch, function () {
                var.touchTime = self.getTimestamp();
                self.do(data.action, data.value);
            });
        } */
    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
                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 });
                    });
            }
        });
    };

    /*
        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);
            }
        }
    
        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});
            }
        }
    
        this.TweenMaxDuration = function (duration) {
            if (duration != undefined) {return Number(duration)/1000} else {return 0.667;}
        }
        */
    // CSS CLASSES
    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);
    };

    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 + '}');
    };

    this.findPlugin = function (name) {
        if (self.json.plugins) {
            for (var plugin of self.json.plugins) {
                if (plugin.name == name) return plugin;
            }
        } else {
            return false;
        }
    };

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

    /*    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);
        } */
    /*     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);
            }
        } */
    /* 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);
    }
    */
    /* 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}
            ]
        });
    } */
    /* 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`
    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
    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
    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);
    };
    */
    this.hash = function (path, args) {
        self.log('hash');
        var path = self.replaceProperties(path, args);
        self.log(path);
        window.location.hash = path;
    };

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

    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);
        } */
    };

    /*     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
    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)
                }
            }
            }
        }
    }
    function uniqueValue(obj, key, value) {
        let found = new Set()
        findValues(object, key, found)
        return found.size === 1 && found.has(value);
    } */
    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;
    };

    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
    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
    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
    this.valueInJson = function (obj, string) {
        //return Boolean(JSON.stringify(obj).indexOf('"' + string + '":') >= 0)
        return Boolean(JSON.stringify(obj).match(new RegExp('\"?' + string + '\"?'))); // JSON5 compatible
    };

    /* 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;
    } */
    /*     this.openPageFromUrl = function (data) {
            self.log('openPageFromUrl');
            self.hashChangeEvent();
            self.do(data);
        }; */
    /*     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);
            }
        } */
    /* 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
    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);

    };

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

    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 = {};

    this.fileType = function (fileName) {
        return /(?:\.([^.]+))?$/.exec(fileName)[1];
    };

    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);
    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');
        }


    };

    /*   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);
            });
        }  */
    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);
            }
        });
    };


    /*     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
    this.findUrl = function (array, url) {
        for (var item of array) {
            if (item.url == url)
                return true;
        }
    };

    /*     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');
            }
        } */
    /*     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});
        }
            */
    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://fonts.googleapis.com", 'link', 'preconnect'));
        //promises.push(loadPlugin("https://fonts.gstatic.com", 'link', 'preconnect'));
        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 });
            //promises.push(loadPlugin(url));
        });


        //self.log(filesToLoad);
        Promise.all(promises).then(function () {

            self.start();

        }).catch(function (script) {
            self.log('Failed to load');
            self.log(script);
        });

    };

    this.modulesList = function (modulesObj) {
        /*         self.log('modulesList');
                self.log('modulesObj');
                self.log(modulesObj);
            */
        var modulesList = [];
        if (modulesObj) {
            for (key in modulesObj) {
                var mod = self.replaceProperties(modulesObj[key]);
                modulesList = modulesList.concat(mod);
            }
        }
        return self.replaceProperties(modulesObj);
    };

    this.loadModules = function () {
        //self.log('loadModules');
        if (self.json.setup.modules) {
            var modules = self.replaceProperties(self.json.setup.modules); // replace {page:id}


            /* self.log('modules');
            self.log(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 = {};

    this.addModule = function (params) {

        if (!self.modules[params.name]) {

            var promises = [];
            //var ideModule = {name: 'ide', url: 'https://jsonic.io/app/modules/ide.json'};
            promises.push(self.loadModule(params));

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

                if (!self.moduleExecuted[params.name]) {
                    //self.log('all modules loaded');
                    var module = self.modules[params.name];

                    if (module.html) {
                        self.moduleExecuted[params.name] = true;
                        self.html(module.html);
                        // TO DO: add also on, funcions ...
                    }
                }
                if (params.success)
                    self.do(params.success);
                //self.do(params.success);
            }).catch(function (file) {
                self.log('Failed to load');
                self.log(file);
                if (params.error)
                    self.do(params.error);
                //self.do(params.error);
            });
        } else {
            if (params.success)
                self.do(params.success);
            //self.do(params.success);
        }
    };

    this.loadModuleGlobal = function () {
        self.log('loadModuleGlobal');

        var promises = [];

        var modules = []; // we need it to keep the order of the modules given in the config
        for (let key in self.json.setup.modules) {
            var mod = self.replaceProperties(self.json.setup.modules[key]);
            modules = modules.concat(self.cloneObject(mod));
        }
        /* self.log('modules');
        self.log(modules); */
        for (let mod of modules)
            promises.push(self.loadModule(mod));
        //var pageModule = {name: 'global', url: '/app/modules/global.json'};
        //var ideModule = {name: 'ide', url: '/app/modules/ide.json'};
        //promises.push(self.loadModule(ideModule)); // if admin
        Promise.all(promises).then(function () {
            //self.log('page module loaded');
            //if (self.json.modules) {
            //self.json.setup.modules = self.cloneObject(self.json.modules); // required modules
            //self.json.setup.modules.app.push(pageModule); // page module
            //self.json.setup.modules.app.push(ideModule); // ide module if admin
            self.loadModules();
            //}
        }).catch(function (file) {
            self.log('Failed to load');
            self.log(file);
        });
    };

    /*     this.extendJsonFromModules = function () {
            self.log('extendJsonFromModules');
            for (var mod in self.modules) {
                //self.extendJsonFromElement(self.modules[mod.name]);
                self.extendJson(json, self.modules[mod]);
            }
        } */
    /* DA SISTEMARE - TEMPORANEO */
    this.code = function (params, selectorParams) {
        /* self.log("params");
        self.log(params);
        self.log("selectorParams");
        self.log(selectorParams); */

        let container = self.selector(params.selector || params.container) || self.selector(selectorParams);
        //var container = self.selector(params) || self.selector(selectorParams);
        self.log("this.code");
        self.log("container");
        self.log(container);

        var editorContainer = container.slice(1);
        //if (editorContainer.startsWith('#')) {editorContainer.slice(1);} // remove #
        var editor = ace.edit(editorContainer);

        var value = params.value; // else value = params;
        if (!params.do || (params.do == 'set')) {

            //var langTools = ace.require('ace/ext/language_tools');
            if (params.theme) editor.setTheme(params.theme); // "ace/theme/monokai"
            var editorMode = params.mode || "ace/mode/json";
            editor.getSession().setMode(editorMode);

            var editorStyle = self.extend({
                background: "rgba(255,255,255,0)"
            }, params.style);

            //$(container).css(editorStyle); //editor.container.style
            /* self.css({
                style: editorStyle
            }, {
                container: container
            }); */ // Eventualmente
            var codeOptions = self.extend({
                fontSize: "11pt",
                selectionStyle: "line",
                highlightActiveLine: false,
                wrap: true,
                showLineNumbers: false,
                showGutter: false,
                fixedWidthGutter: false,
                readOnly: true,
                //enableBasicAutocompletion: false,
                //enableSnippets: false,
                //enableLiveAutocompletion: false,
            }, params.options);

            // https://github.com/ajaxorg/ace/wiki/Configuring-Ace
            editor.setOptions(codeOptions);

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

                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;
            // $(container).css({
            //     height: Number(21 * codeRows) + 'px'
            // });
            /* self.css({
                style: {
                    height: Number(21*codeRows)+"px"
                }
            }, {
                container: container
            }); */ // Eventualmente
            editor.setValue(codeString, -1);
        } else if (params.do == 'get') {
            return editor.getValue();
        } else {
            self.log('"code" function requires value param');
        }

    };

    this.init = function (appString, modulesString) {
        console.log('init');
        // app(String: app url) (Object = json app)
        // modules(String): modules "ui,daisyui,svg"
        /* json = {  // the jsonic app tree
            blocks: {},
            parts: {},
            css: {},
            do: {},
            on: {},
            texts: {},
            data: {},
            var: {}
        }; */
        //self.app = self.getParams(); // from url
        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);

            /* // TO DO: remove the requirement of resources.json
            if (!self.json.setup || !self.json.setup.modules) {
                self.json.setup = {
                    "modules": [
                        {
                            "name": "resources",
                            "url": "https://jsonic.io/app/modules/lib/resources.json"
                        }
                    ]
                }
            } */
            if (appString.css) self.defineCss(appString.css); // can be removed?

        }

        self.loadModuleGlobal();
    };

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

    this.htmlToJson = function (text) {
        let xmlDoc = new DOMParser().parseFromString(text, "text/xml");
        return JSON.stringify(toJsonNode(xmlDoc), null, "\t");
    };

    this.elementToJson = function (selector) {
        let el = document.querySelector(selector);
        return self.htmlToJson(el.getHTML());
    };

    function clearSelection() {
        if (window.getSelection) { window.getSelection().removeAllRanges(); }
        else if (document.selection) { document.selection.empty(); }
    }

    this.importHtml = function (htmlText) {
        // use modal and monaco js
        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.innerHTML = '';
            document.body.appendChild(textarea);
            //document.execCommand("copy");
            textarea.addEventListener('click', function () {
                textarea.remove();
                navigator.clipboard.writeText(textarea.value).then(() => console.log('copied!'));
            });
            //console.log(JSON.parse(jsonDoc));
        }
    };

    // https://github.com/ktskts/html2json/blob/master/src/html2json.js
    /*
        var Util = {
            div : null,
            attrs: "id,name,style,class,value,src,href,width,height,title,type".split(","),
            getJson: function(childNodes, attrs, length) {
                var result = [];
                for ( var i = 0, len = childNodes.length; i < len; i++) {
                    var item = childNodes[i];
                    if (item.nodeType == 3) { // textual content in an element or attribute
                        result.push({
                            text : item.nodeValue
                        })
                    } else if (item.nodeType == 1) { // element
                        var obj = {
                            //tag : item.nodeName.toLowerCase(),
                            attr : {}
                        };
                        var flag = false;
                        //for ie6
                        if (attrs) {
                            for (var j = 0; j < length; j++){
                                if (attrs[j] == 'style'){
                                    var sStyle = item.getAttribute('style').cssText;
                                    if(sStyle){
                                        obj.attr["style"] = sStyle;
                                        flag = true;
                                    }
                                } else {
                                    var attrNode = item.attributes[attrs[j]];
                                    if(attrNode && attrNode.nodeType === 2){
                                        var value = attrNode.value;
                                        if(value && value != 'null'){
                                            obj.attr[attrs[j]] = value;
                                            flag = true;
                                        }
                                    }
                                }
                            }
                        } else {
                            if (item.attributes.length) {
                                for ( var n = 0, l = item.attributes.length; n < l; n++) {
                                    var value = item.attributes[n].value;
                                    if(value){
                                        flag = true;
                                        obj.attr[item.attributes[n].name] = value;
                                    }
                                }
                            }
                        }
    
                        let nodeOut = {};
                        let nodeTag = item.nodeName.toLowerCase();
                        let nodeChild = Util.getJson(item.childNodes, attrs, length);
                        nodeOut[nodeTag] = {};
                        
    
                        self.log( 'item.childNodes');
                        self.log( item.childNodes);
                        self.log( 'nodeChild');
                        self.log( nodeChild);
                        //self.log( item.textContent);
        
    
                        nodeOut[nodeTag].html = nodeChild;
                        if (flag) nodeOut[nodeTag].attr = obj.attr;
                        result.push(nodeOut);
                
                    } else {
                        // do nothing
                    }
                }
                if (result.length == 1) return result[0]
                else if (result.length > 1) return result;
            }
        }
    
        this.html2json = function(text) {
            if (!Util.div) Util.div = document.createElement('div');
            Util.div.innerHTML = text;
            return  {html: Util.getJson(Util.div.childNodes)};
        };
        */
    /* var jsonicOptions = self.extend({
    }, options); */
    if (typeof options == 'object') {
    } else if (options)
        self.init(arguments[0], arguments[1]);


} // of jsonicObject

//var jsonicApp = jsonApp;
var jsonApp = new jsonAppObj();
var js = jsonApp;
var jsonic = jsonApp;

//window.jsonic = new jsonicObject();

/* var receiveMessage = function (process) {
    if (ThunkableWebviewerExtension)
        ThunkableWebviewerExtension.receiveMessage(process);
}

var postMessage = function (message) {
    if (ThunkableWebviewerExtension)
        ThunkableWebviewerExtension.postMessage(message);
} */

/* "on": {
    "init": [
        {
            "thunkable": {
                "postMessage": "Hi Rick!"
            }
        },
        {
            "thunkable": {
                "receiveMessage": {
                    "alert": "message: {result}"
                }
            }
        }
    ]
}
 */
