Files
objects/wco/apx/apx.js
devpotatoes bc76840707 Update apx.
2025-07-16 12:36:04 +02:00

623 lines
23 KiB
JavaScript

/*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
*
<script>
const apxtri={
version:0, // 0 mean it is a first visit if you force to 0 then it will reload all app data
pagename:"pagename", // without _lg.html ex:pageadmin
pageauth:"apxid" local page with authentification without _lg,
headers:{
xalias:"anonymous",
xhash:"anonymous",
xtribe:"tribeId",
xapp:"app name",
xlang:"en",
xdays:0,
xuuid:"0",
xtrkversion:1}
}
wco:{}
//specific init data for this webpage
} ;
</script>
* ++++++++
* ? 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 <tag data-wco="propertie"></tag> if apx.data.wco.propertie value change then it change every where data-wco=propertie exist innerHTML, textContent, class, ...value
* to add a new propertie to listen just add it apx.data.wco.propertie={innerHTML:"<p>test</p>"} 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:
// <img data-wco="logodetoto" src="urltoimage" "alt">
// apx.wco.logodetoto={src:"newurltoimage",alt:"newalt"} then it will change every where data-wco=logodetoto
//
// <p data-wco="claim">Blabla</p>
// 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 <div wco-name="wconame" id="uniqueid" wco-YYY="aa"></div>
* if create or if any wco-YYY value change it runs apx[wconame].loadwco(id,ctx) where ctx={YYY:aa}
* Example:
* <div id="monComponent" wco-name="monComposant" wco-screen="dashboard" wco-theme="dark" wco-ref="123"></div>
* 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
apx.indexedDB = apx.indexedDB || {};
apx.indexedDB.set = async (db, storeName, keyPath, key, value) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (window.threadsObj) {
for (const threadId of Object.keys(window.threadsObj)) {
if (!db.objectStoreNames.contains(threadId)) {
db.createObjectStore(threadId, { keyPath: keyPath });
};
};
};
};
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const putRequest = store.put({ timestamp: key, value: value });
putRequest.onsuccess = () => resolve();
putRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.get = async (db, storeName, keyPath, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (window.threadsObj) {
for (const threadId of Object.keys(window.threadsObj)) {
if (!db.objectStoreNames.contains(threadId)) {
db.createObjectStore(threadId, { keyPath: keyPath });
};
};
};
};
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve(null);
};
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const getRequest = store.get(key);
getRequest.onsuccess = () => {
resolve(getRequest.result ? getRequest.result.value : null);
};
getRequest.onerror = () => resolve(null);
};
request.onerror = (error) => reject(error);
});
};
apx.indexedDB.del = async (db, storeName, keyPath, key) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(db, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (window.threadsObj) {
for (const threadId of Object.keys(window.threadsObj)) {
if (!db.objectStoreNames.contains(threadId)) {
db.createObjectStore(threadId, { keyPath: keyPath });
};
};
};
};
request.onsuccess = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(storeName)) {
return resolve();
};
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const deleteRequest = store.delete(key);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = (error) => reject(error);
};
request.onerror = (error) => reject(error);
});
};