/*eslint no-undef:0*/
/*eslint-env browser*/
"use strict";
var apx = apx || {};
/**************************************************************************
* apx.js manage data to interact with an apxtri instance from a webpage *
* component can be add with name-wco
*************************************************************************
* This code is not minify and target to be understood by a maximum of
* curious people to audit and give any feedback to support@ndda.fr
*
* To audit it in a browser like chrome:
* - open web developpement (F12)
* - Menu Sources: apx.js , see below for more information
* - : apxid.js, this is the authentification module that works as a microservice
*
*
* Main principle:
* - Usage of localStorage to store any usefull data and save it between session
* - All data and template can be download from an apxtri
* get /api/apxtri/wwws/updatelocaldb{ano}/{tribe}/{xapp}/{pagename}/{version}
* ano = empty if authenticated anonymous if not
* tribe = the tribe where tha xapp is strore
* xapp = the folder name in /tribe/xapp/
* pagename = the name of the page without _xx (language)
* version = 0 or the version currently store (version is to manage cach)
*
* ------------------
* ? State management
* ------------------
* html page must have apxtri with at least headers key
*
* ++++++++
* ? apx.ready(callback) await DOM is ready equivalent of jquery Document.ready()
* +++++++++
* ? apx.readyafterupdate(callback) allow to add function callback that will be load after apx.ready to add a function to execute apx.readyafterupdate(functname) without () after functname
* +++++++++
* ? apx.listendatawco(newpropertie) allow to store data in apx.data.wco and to listen any change to update HTML DOM content
test
"} and run apx.listendatawco(propertie) * +++++++++ * ? apx.notification(selector, data) insert in tag selector an apx return call into data{status:xxx,ref:"Ref",msg:"key",data:{}}, it use apx.data.ref[data.ref].msg as template and render it with data * +++++++++ * ? apx.save() save apx.data as {xapp} value in localStorage to be able for the same domain to share data * +++++++++ * ? apx.update() : * Run at least once after loading a pagename, what it does: * - url/page.html?k1=v1&k2=v2#h1=v4&h2=v5 => store in apx.pagecontext = { search: {k1:v1,k2:v2}, hash: {h1:v4,h2:v5} }; * - updatelocaldb from api to localstorage name xapp (options / ref / schema / tpl /tpldata) store in {tribe}/wwws/{xapp}.json with key {pagename} * - run apx.listendatawco() to allow any apx.data.wco variable to update DOM * - run any function that were store in apx.afterupdate. * - run apx.lazyload() to load image in background (this is to decrease time loading for search engine) * ++++++++++ * */ apx.ready = (callback) => { if (!callback) { alert( "You have an unknown callback apx.ready(callback), you need to order your code callback = ()=>{} then apx.ready(callback), boring but js rules ;-)" ); } if (document.readyState != "loading") callback(); // modern browsers else if (document.addEventListener) document.addEventListener("DOMContentLoaded", callback); // IE <= 8 else document.attachEvent("onreadystatechange", function () { if (document.readyState == "complete") callback(); }); }; apx.readyafterupdate = (callback) => { if (!apx.afterupdate) apx.afterupdate = []; apx.afterupdate.push(callback); }; apx.lazyload = () => { document.querySelectorAll("img[data-lazysrc]").forEach((e) => { // in src/page.html: src contain original img and data-lazysrc='true' // in dist/page.html src is removed img src is in data-lazysrc=newimage.webp or svg let src = e.getAttribute("src") ? e.getAttribute("src") : e.getAttribute("data-lazysrc"); if (e.getAttribute("data-trksrckey")) { src = `/trk/${src}?alias=${apx.data.headers.xalias}&uuid=${ apx.data.headers.xuuid }&srckey=${e.getAttribute("data-trksrckey")}&version=${ apx.data.headers.xtrkversion }&consentcookie=${localStorage.getItem("consentcookie")}&lg=${ apx.data.headers.xlang }`; } e.setAttribute("src", src); console.log("lazyload track:", src); e.removeAttribute("data-lazysrc"); }); document.querySelectorAll("[data-lazybgsrc]").forEach((e) => { e.style.backgroundImage = `url(${e.getAttribute("src")})`; e.removeAttribute("data-lazybgsrc"); }); document.querySelectorAll("a[data-trksrckey]").forEach((e) => { let urldestin = e.getAttribute("href"); if ( urldestin.substring(0, 4) != "http" && urldestin.substring(0, 1) != "/" ) { urldestin = "/" + urldestin; } const hreftrack = `/trk/redirect?alias=${apx.data.headers.xalias}&uuid=${ apx.data.headers.xuuid }&srckey=${e.getAttribute("data-trksrckey")}&version=${ apx.data.headers.xtrkversion }&consentcookie=${localStorage.getItem("consentcookie")}&lg=${ apx.data.headers.xlang }&url=${urldestin}`; console.log("href track:", hreftrack); e.setAttribute("href", hreftrack); e.removeAttribute("data-trksrckey"); }); }; apx.notification = (selector, data, clearbefore) => { /** * @selector a text to use querySelector() in document * @data apxtri return from any request {status:200,ref:"",msg:"key", data } * if {status,multimsg:[{ref,msg,data}]} then a multi feedback are in * @clearbefore boolean if true then it remove the previous message * @return update the dom selctor with the relevant render message in the relevant language */ console.log("notification of ", data); const el = document.querySelector(selector); if (!el) { console.log( `WARNING !!! check apx.notification selector:${selector} does not exist in this page` ); return false; } if (clearbefore) el.innerHTML = ""; const multimsg = data.multimsg ? data.multimsg : [data]; multimsg.forEach((info) => { if (!apx.data.ref[info.ref]) { console.log( `check apx.data.ref, ${info.ref} does not exist in this page, add it ` ); return false; } else if (!apx.data.ref[info.ref][info.msg]) { console.log( `check apx.data.ref.${info.ref} does not contain ${info.msg} update /schema/lg or /model/lg` ); return false; } el.innerHTML += " " + Mustache.render(apx.data.ref[info.ref][info.msg], info.data); if (data.status == 200) { el.classList.remove("text-red"); el.classList.add("text-green"); } else { el.classList.add("text-red"); el.classList.remove("text-green"); } }); }; apx.listendatawco = (newpropertie) => { // listen any change in apx.wco.newpropertie and update interface with this new value // < data-wco="propertie of apx.wco"> is updated with content text, html any attribute in this new value // to init run apx.listendatawco() this is done by apx.update() // to dynamicaly add a new propertie to listen just run apx.listendatawco(propertietoadd); // Then manage your data by modifying apx.wco and it will be update anywhere it use in the webpage //example: //Blabla
// apx.wco.claim={html:"newblabla"} console.log("From apx.data.wco:", apx.data.wco); if (!apx.wco) apx.wco = {}; console.log( "wco dynamic into the webpage", apx.wco, "no propertie to add:", !newpropertie ); if (!apx.data.wco || Object.keys(apx.data.wco).length == 0) return false; newpropertie = !newpropertie ? Object.keys(apx.data.wco) : [newpropertie]; console.log("listen apx.data.wco properties:", newpropertie); newpropertie.forEach((p) => { const actionprop = (val, elt) => { if (val.innerHTML) elt.innerHTML = val.innerHTML; if (val.textContent) elt.textContent = val.textContent; for (const h in ["innerHTML", "textContent"]) { if (val[h]) elt[h] = val[h]; } for (const a in ["src", "alt", "placeholder", "class", "href"]) { if (val[a]) elt.setAttribute(a, val[a]); } }; const elements = document.querySelectorAll(`[data-wco='${p}']`); elements.forEach((e) => actionprop(apx.data.wco[p], e)); //console.log(p, Object.hasOwnProperty(apx.wco)); if (Object.hasOwnProperty(apx.wco)) { Object.defineProperty(apx.wco, p, { set: (newv) => { this[p] = newv; elements.forEach((e) => actionprop(newv, e)); }, }); } }); }; apx.wcoobserver = () => { /** * wco web component observer if apxtri.wcoobserver==true, * Observe existing or creation of any element in DOM with * if create or if any wco-YYY value change it runs apx[wconame].loadwco(id,ctx) where ctx={YYY:aa} * Example: * * if innerHTML this OR if any wco-YYYY change => * run apx.monComposant.loadwco('monComponent', * {'wco-name': 'monComposant', * 'wco-screen': 'dashboard', * 'wco-theme': 'dark', * 'wco-ref': '123'}); * Used with wco component manage in apx to communicate between autonomous component to reload content if contexte change. * Typical example of a wco menu that will load another wco content if not exist and will change a wco-screen to change the content of the wco content */ console.log("listen wcoobserver"); const processElement = (element) => { if (element.nodeType === 1 && element.tagName === "DIV") { if (element.id && element.hasAttribute("wco-name")) { const ctx = {}; for (const attr of element.attributes) { if (attr.name.startsWith("wco-")) { ctx[attr.name.slice(4)] = attr.value; } } const wcoName = element.getAttribute("wco-name"); if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") { apx[wcoName].loadwco(element.id, ctx); } else { console.log(`ERROR: apx.${wcoName}.loadwco() not found`); } } } // Process children recursively if (element.children) { Array.from(element.children).forEach(processElement); } }; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { processElement(node); }); } if ( mutation.type === "attributes" && mutation.attributeName.startsWith("wco-") ) { const element = mutation.target; if (element.id && element.hasAttribute("wco-name")) { const ctx = {}; for (const attr of element.attributes) { if (attr.name.startsWith("wco-")) { ctx[attr.name.slice(4)] = attr.value; } } const wcoName = element.getAttribute("wco-name"); if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") { apx[wcoName].loadwco(element.id, ctx); } } } }); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, // <-- active la détection de changements d'attributs }); // Pour les éléments déjà présents au chargement document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("div[wco-name]").forEach((element) => { if (element.id) { const ctx = {}; for (const attr of element.attributes) { if (attr.name.startsWith("wco-")) { ctx[attr.name.slice(4)] = attr.value; } } const wcoName = element.getAttribute("wco-name"); console.log(`load observer of wco for ${wcoName} in id=${element.id}`); if (apx[wcoName] && typeof apx[wcoName].loadwco === "function") { apx[wcoName].loadwco(element.id, ctx); } } }); }); //load existing wco-name in the html page to initiate the wco process // it read and write the wco-name that will trig the observer as a change document.querySelectorAll("div[wco-name]").forEach((e) => { const wconame = e.getAttribute("wco-name"); console.log("load wco-name:", wconame); e.setAttribute("wco-name", wconame); }); }; // State management apx.save = () => { localStorage.setItem(apx.data.headers.xapp, JSON.stringify(apx.data)); }; apx.update = async () => { if (!apxtri) { console.log( 'Please add to the html page header, this line const apxtri = { headers: { xtrkversion: 1, xtribe: "smatchit", xapp: "pwa", xlang: "fr", xalias: "anonymous", xhash: "anonymous", xdays: 0} ,pagename:"apxid"} ' ); return; } //if (apxtri.forcereload){localStorage.setItem("forcereload",true)}; if (document.querySelector("html").getAttribute("lang")) { apxtri.headers.xlang = document.querySelector("html").getAttribute("lang"); } //alert(localStorage.getItem(apxtri.headers.xapp)) if (localStorage.getItem(apxtri.headers.xapp)) { apx.data = JSON.parse(localStorage.getItem(apxtri.headers.xapp)); //update with current pagename and eventualy pageauth apx.data.pagename = apxtri.pagename; if (apxtri.pageauth) apx.data.pageauth = apxtri.pageauth; // check localstorage in line with current webpage if ( apx.data.headers.xtribe != apxtri.headers.xtribe || apx.data.headers.xlang != apxtri.headers.xlang || apx.data.headers.xtrkversion != apxtri.headers.xtrkversion ) { // if an app change of tribe localStorage.removeItem(apxtri.headers.xapp); delete apx.data; } } if (!apx.data) { console.log("init or reinit apx.data"); apx.data = apxtri; } apx.pagecontext = { search: {}, hash: {} }; if (window.location.hash != "") { window.location.hash .slice(1) .split("&") .forEach((kv) => { const keyval = kv.split("="); apx.pagecontext.hash[keyval[0]] = keyval[1]; }); } if (window.location.search != "") { window.location.search .slice(1) .split("&") .forEach((kv) => { const keyval = kv.split("="); apx.pagecontext.hash[keyval[0]] = keyval[1]; }); } console.log("apx.pagecontext:", apx.pagecontext); // Set authenticate parameter if in pagecontext and redirect to the requested url console.log( apx.pagecontext.hash.xdays, apx.pagecontext.hash.xprofils, apx.pagecontext.hash.xtribe, dayjs(apx.pagecontext.hash.xdays), dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25, apx.pagecontext.hash.xhash ); if ( apx.pagecontext.hash.xhash && apx.pagecontext.hash.xdays && apx.pagecontext.hash.xprofils && apx.pagecontext.hash.xtribe && dayjs(apx.pagecontext.hash.xdays) && dayjs(apx.pagecontext.hash.xdays).diff(dayjs(), "hours") < 25 ) { //Means this page is called from an external auth app let headervalid = true; const headerkey = [ "xalias", "xhash", "xdays", "xprofils", "xtribe", "xlang", ]; headerkey.forEach((h) => { if (apx.pagecontext.hash[h]) { apx.data.headers[h] = (h==="xprofils")? apx.pagecontext.hash[h].split(","):apx.pagecontext.hash[h]; } else { headervalid = false; } }); console.log(headervalid, apx.data.headers); if (headervalid) { apx.save(); if (apx.pagecontext.hash.url) { window.location.href = apx.pagecontext.hash.url; } } else { console.log("Your try to access a page failled with ", apx.pagecontext); } } if ( apx.data.allowedprofils && !apx.data.allowedprofils.includes("anonymous") && apx.data.pagename !== apx.data.pageauth ) { const profilintersect = apx.data.allowedprofils.filter((x) => apx.data.headers.xprofils.includes(x) ); console.log("profils authorized:", profilintersect); if (profilintersect.length == 0) { alert(apx.data.ref.Middlewares.notallowtoaccess); return false; } if (dayjs().valueOf() - apx.data.headers.xdays > 86400000) { // need to refresh authentification if possible by opening the pageauth with url context // the pageauth redirect to this current page after authentification, if not then wait credential document.location.href = `/${apx.data.pageauth}_${apx.data.headers.xlang}.html#url=${apx.data.pagename}_${apx.data.headers.xlang}.html`; } } console.log("authorized to access"); /* à voir si utile redirect to authentification page pageauth with a redirection if authentify to the pagename (check if /src/ then add it) window.location.href = `${apxtri.pageauth}_${ apxtri.headers.xlang }.html?url=${window.location.href.includes("/src/") ? "/src/" : ""}${ apxtri.pagename }_${apxtri.headers.xlang}.html`; */ //////////////////////////////////////////// apx.data.version = 0; //this force an update to be removed in production /////////////////////////////////////////// const ano = apx.data.headers.xalias == "anonymous" ? "anonymous" : ""; const initdb = `/api/apxtri/wwws/updatelocaldb${ano}/${apx.data.headers.xtribe}/${apx.data.headers.xapp}/${apx.data.pagename}/${apx.data.version}`; let initset = {}; try { initset = await axios.get(initdb, { headers: apx.data.headers, timeout: 2000, }); } catch (err) { console.log(err); initset = { data: { msg: "unavailableAPI" } }; } console.log("recupe inidb for ", initdb, initset); if (initset.data.msg == "forbidenaccess") { alert(apx.data.ref.Middlewares.notallowtoaccess); return false; } if (initset.data.msg == "unavailableAPI") { console.log("Your api endpoint is down check your hosted server"); //try again in 30 seconds setTimeout(apx.update, 30000); } if (initset.data.msg == "datamodelupdate") { // mise à jour local /*if (initset.data.data.wco) { console.log("WARNING!!, local apxtri.wco was erase by updatelocaldb.wco"); }*/ Object.keys(initset.data.data).forEach((k) => { if (k != "headers") { apx.data[k] = initset.data.data[k]; } }); /* if (apx.data.confpage.wco && !apx.data.wco){ console.log("update apx.data.wco with localdb cause does not exist") apx.data.wco=apx.data.confpage.wco; } */ console.log("local update done"); apx.save(); } apx.listendatawco(); // listen any data-wco tag and update it when apxdatawco propertie change if (apxtri.wcoobserver) apx.wcoobserver(); if (apx.afterupdate) apx.afterupdate.forEach((cb) => cb()); //run all function store in apx.afterupdate in order apx.lazyload(); //reload image or any media that takes time to load to improve engine search apx.save(); //store in local the modification }; apx.ready(apx.update); //2nd param optional=> true mean does not wait same if apx.lock is set