334 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			334 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* eslint-env browser */
 | |
| /* eslint-disable no-alert, no-console */
 | |
| 
 | |
| /**
 | |
|  * @file apx.js (previously apxnew.js)
 | |
|  * @description Modern, class-based implementation to manage data and interactions with an apxtri instance from a webpage.
 | |
|  * @version 2.1
 | |
|  * @author support@ndda.fr
 | |
|  */
 | |
| 
 | |
| // Establish the global namespace
 | |
| window.apx = window.apx || {};
 | |
| 
 | |
| /**
 | |
|  * @class ApxManager
 | |
|  * Manages the core application state and lifecycle.
 | |
|  */
 | |
| class ApxManager {
 | |
|     constructor() {
 | |
|         this.data = {};
 | |
|         this.pageContext = { search: {}, hash: {} };
 | |
|         this.afterUpdateCallbacks = [];
 | |
|         this.wcoProxies = {};
 | |
|         // Capture the observer flag immediately from the initial global config.
 | |
|         this.useWcoObserver = window.apxtri?.wcoobserver || false;
 | |
|     }
 | |
| 
 | |
|     ready(callback) {
 | |
|         if (typeof callback !== 'function') {
 | |
|             alert("Apx.ready(callback) requires a valid function.");
 | |
|             return;
 | |
|         }
 | |
|         if (document.readyState !== 'loading') {
 | |
|             callback();
 | |
|         } else {
 | |
|             document.addEventListener('DOMContentLoaded', callback);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     registerUpdateCallback(callback) {
 | |
|         this.afterUpdateCallbacks.push(callback);
 | |
|     }
 | |
| 
 | |
|     lazyLoad() {
 | |
|         const { xalias, xuuid, xtrkversion, xlang } = this.data.headers;
 | |
|         const consentCookie = localStorage.getItem('consentcookie');
 | |
| 
 | |
|         document.querySelectorAll('img[data-lazysrc]').forEach(img => {
 | |
|             let src = img.dataset.lazysrc;
 | |
|             if (img.dataset.trksrckey) {
 | |
|                 src = `/trk/${src}?alias=${xalias}&uuid=${xuuid}&srckey=${img.dataset.trksrckey}&version=${xtrkversion}&consentcookie=${consentCookie}&lg=${xlang}`;
 | |
|             }
 | |
|             img.setAttribute('src', src);
 | |
|             img.removeAttribute('data-lazysrc');
 | |
|         });
 | |
| 
 | |
|         document.querySelectorAll('[data-lazybgsrc]').forEach(el => {
 | |
|             el.style.backgroundImage = `url(${el.dataset.lazybgsrc})`;
 | |
|             el.removeAttribute('data-lazybgsrc');
 | |
|         });
 | |
| 
 | |
|         document.querySelectorAll('a[data-trksrckey]').forEach(a => {
 | |
|             let urlDest = a.getAttribute('href');
 | |
|             if (!urlDest.startsWith('http') && !urlDest.startsWith('/')) {
 | |
|                 urlDest = `/${urlDest}`;
 | |
|             }
 | |
|             const trackedHref = `/trk/redirect?alias=${xalias}&uuid=${xuuid}&srckey=${a.dataset.trksrckey}&version=${xtrkversion}&consentcookie=${consentCookie}&lg=${xlang}&url=${encodeURIComponent(urlDest)}`;
 | |
|             a.setAttribute('href', trackedHref);
 | |
|             a.removeAttribute('data-trksrckey');
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     notify(selector, responseData, clearBefore = false) {
 | |
|         const el = document.querySelector(selector);
 | |
|         if (!el) {
 | |
|             console.warn(`Notification selector not found: ${selector}`);
 | |
|             return;
 | |
|         }
 | |
|         if (clearBefore) {
 | |
|             el.innerHTML = '';
 | |
|         }
 | |
| 
 | |
|         const messages = responseData.multimsg || [responseData];
 | |
|         messages.forEach(info => {
 | |
|             const template = this.data.ref?.[info.ref]?.[info.msg];
 | |
|             if (!template) {
 | |
|                 console.warn(`Notification template not found for ref: ${info.ref}, msg: ${info.msg}`);
 | |
|                 return;
 | |
|             }
 | |
|             el.innerHTML += Mustache.render(template, info.data);
 | |
|         });
 | |
| 
 | |
|         if (responseData.status === 200) {
 | |
|             el.classList.remove('text-red');
 | |
|             el.classList.add('text-green');
 | |
|         } else {
 | |
|             el.classList.add('text-red');
 | |
|             el.classList.remove('text-green');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     listenWcoData() {
 | |
|         if (!this.data.wco || Object.keys(this.data.wco).length === 0) return;
 | |
| 
 | |
|         const updateElement = (element, value) => {
 | |
|             if (value.innerHTML !== undefined) element.innerHTML = value.innerHTML;
 | |
|             if (value.textContent !== undefined) element.textContent = value.textContent;
 | |
|             ['src', 'alt', 'placeholder', 'class', 'href'].forEach(attr => {
 | |
|                 if (value[attr] !== undefined) element.setAttribute(attr, value[attr]);
 | |
|             });
 | |
|         };
 | |
| 
 | |
|         for (const prop in this.data.wco) {
 | |
|             if (Object.hasOwnProperty.call(this.data.wco, prop) && !this.wcoProxies[prop]) {
 | |
|                 const elements = document.querySelectorAll(`[data-wco='${prop}']`);
 | |
|                 elements.forEach(el => updateElement(el, this.data.wco[prop]));
 | |
| 
 | |
|                 this.wcoProxies[prop] = new Proxy(this.data.wco[prop], {
 | |
|                     set: (target, key, value) => {
 | |
|                         target[key] = value;
 | |
|                         elements.forEach(el => updateElement(el, target));
 | |
|                         return true;
 | |
|                     },
 | |
|                 });
 | |
|             }
 | |
|         }
 | |
|         this.wco = new Proxy(this.data.wco, {
 | |
|             set: (target, prop, value) => {
 | |
|                 target[prop] = value;
 | |
|                 this.listenWcoData();
 | |
|                 return true;
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     startWcoObserver() {
 | |
|         console.log('[APX] Starting WCO observer and triggering initial load...');
 | |
|         
 | |
|         const processElement = (element) => {
 | |
|             if (element.nodeType !== 1 || !element.hasAttribute('wco-name') || !element.id) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const wcoName = element.getAttribute('wco-name');
 | |
|             const wcoModule = window.apx?.[wcoName];
 | |
| 
 | |
|             if (wcoModule && typeof wcoModule.loadwco === 'function') {
 | |
|                 console.log(`[APX] Observer is processing component: ${wcoName} on #${element.id}`);
 | |
|                 const ctx = {};
 | |
|                 for (const attr of element.attributes) {
 | |
|                     if (attr.name.startsWith('wco-')) {
 | |
|                         ctx[attr.name.slice(4)] = attr.value;
 | |
|                     }
 | |
|                 }
 | |
|                 wcoModule.loadwco(element.id, ctx);
 | |
|             } else {
 | |
|                 console.warn(`[APX] WCO handler not found for component: apx.${wcoName}`);
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         const observer = new MutationObserver((mutations) => {
 | |
|             mutations.forEach((mutation) => {
 | |
|                 if (mutation.type === 'childList') {
 | |
|                     mutation.addedNodes.forEach(processElement);
 | |
|                 }
 | |
|                 if (mutation.type === 'attributes' && mutation.attributeName.startsWith('wco-')) {
 | |
|                     processElement(mutation.target);
 | |
|                 }
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         observer.observe(document.body, {
 | |
|             childList: true,
 | |
|             subtree: true,
 | |
|             attributes: true,
 | |
|         });
 | |
| 
 | |
|         // This is the key part from your original logic:
 | |
|         // Manually trigger the observer for components already in the DOM.
 | |
|         document.querySelectorAll('div[wco-name]').forEach((element) => {
 | |
|             const wcoName = element.getAttribute('wco-name');
 | |
|             console.log(`[APX] Manually triggering initial load for: ${wcoName}`);
 | |
|             element.setAttribute('wco-name', wcoName);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     saveState() {
 | |
|         localStorage.setItem(this.data.headers.xapp, JSON.stringify(this.data));
 | |
|     }
 | |
| 
 | |
|     parseUrl() {
 | |
|         const parse = (str) => str.slice(1).split('&').reduce((acc, kv) => {
 | |
|             if (kv) {
 | |
|                 const [key, value] = kv.split('=');
 | |
|                 acc[key] = decodeURIComponent(value);
 | |
|             }
 | |
|             return acc;
 | |
|         }, {});
 | |
| 
 | |
|         this.pageContext.search = parse(window.location.search);
 | |
|         this.pageContext.hash = parse(window.location.hash);
 | |
|     }
 | |
| 
 | |
|     loadState() {
 | |
|         if (typeof apxtri === 'undefined') {
 | |
|             throw new Error('Global `apxtri` configuration object is missing.');
 | |
|         }
 | |
| 
 | |
|         const storedState = localStorage.getItem(apxtri.headers.xapp);
 | |
|         if (storedState) {
 | |
|             this.data = JSON.parse(storedState);
 | |
|             // Invalidate cache if key configuration has changed
 | |
|             if (
 | |
|                 this.data.headers.xtribe !== apxtri.headers.xtribe ||
 | |
|                 this.data.headers.xlang !== apxtri.headers.xlang ||
 | |
|                 this.data.headers.xtrkversion !== apxtri.headers.xtrkversion
 | |
|             ) {
 | |
|                 localStorage.removeItem(apxtri.headers.xapp);
 | |
|                 // Create a deep copy to prevent modifying the global apxtri object
 | |
|                 this.data = JSON.parse(JSON.stringify(apxtri));
 | |
|             }
 | |
|         } else {
 | |
|             // Create a deep copy to prevent modifying the global apxtri object
 | |
|             this.data = JSON.parse(JSON.stringify(apxtri));
 | |
|         }
 | |
|         // Always update with current page info from the original config
 | |
|         this.data.pagename = apxtri.pagename;
 | |
|         if (apxtri.pageauth) this.data.pageauth = apxtri.pageauth;
 | |
|     }
 | |
| 
 | |
|     handleAuthRedirect() {
 | |
|         const { xhash, xdays, xprofils, xtribe, url } = this.pageContext.hash;
 | |
|         if (xhash && xdays && xprofils && xtribe && dayjs(xdays).isValid() && dayjs(xdays).diff(dayjs(), 'hours') < 25) {
 | |
|             const headerKeys = ['xalias', 'xhash', 'xdays', 'xprofils', 'xtribe', 'xlang'];
 | |
|             const newHeaders = {};
 | |
|             let isValid = true;
 | |
| 
 | |
|             headerKeys.forEach(key => {
 | |
|                 if (this.pageContext.hash[key]) {
 | |
|                     newHeaders[key] = (key === 'xprofils') ? this.pageContext.hash[key].split(',') : this.pageContext.hash[key];
 | |
|                 } else {
 | |
|                     isValid = false;
 | |
|                 }
 | |
|             });
 | |
| 
 | |
|             if (isValid) {
 | |
|                 this.data.headers = { ...this.data.headers, ...newHeaders };
 | |
|                 this.saveState();
 | |
|                 if (url) {
 | |
|                     window.location.href = url;
 | |
|                     return true;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     checkAccess() {
 | |
|         const { allowedprofils, pagename, pageauth, headers } = this.data;
 | |
|         if (allowedprofils && !allowedprofils.includes('anonymous') && pagename !== pageauth) {
 | |
|             const hasAccess = allowedprofils.some(p => headers.xprofils?.includes(p));
 | |
| 
 | |
|             if (!hasAccess) {
 | |
|                 alert(this.data.ref?.Middlewares?.notallowtoaccess || 'Access denied.');
 | |
|                 return false;
 | |
|             }
 | |
|             if (dayjs().valueOf() - headers.xdays > 86400000) {
 | |
|                 const redirectUrl = `/${pageauth}_${headers.xlang}.html#url=${pagename}_${headers.xlang}.html`;
 | |
|                 document.location.href = redirectUrl;
 | |
|                 return false;
 | |
|             }
 | |
|         }
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     async fetchAndUpdateDataModel() {
 | |
|         this.data.version = 0;
 | |
| 
 | |
|         const { xalias, xtribe, xapp } = this.data.headers;
 | |
|         const ano = (xalias === 'anonymous') ? 'anonymous' : '';
 | |
|         const url = `/api/apxtri/wwws/updatelocaldb${ano}/${xtribe}/${xapp}/${this.data.pagename}/${this.data.version}`;
 | |
| 
 | |
|         try {
 | |
|             const response = await axios.get(url, { headers: this.data.headers, timeout: 2000 });
 | |
| 
 | |
|             if (response.data.msg === 'datamodelupdate') {
 | |
|                 Object.keys(response.data.data).forEach(key => {
 | |
|                     if (key !== 'headers') {
 | |
|                         this.data[key] = response.data.data[key];
 | |
|                     }
 | |
|                 });
 | |
|                 this.saveState();
 | |
|                 console.log('Local data model updated.');
 | |
|             } else if (response.data.msg === 'forbidenaccess') {
 | |
|                 alert(this.data.ref?.Middlewares?.notallowtoaccess || 'Access denied by API.');
 | |
|                 return false;
 | |
|             }
 | |
|         } catch (error) {
 | |
|             console.error('API is unavailable. Retrying in 30 seconds.', error);
 | |
|             setTimeout(() => this.init(), 30000);
 | |
|             return false;
 | |
|         }
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     async init() {
 | |
|         this.loadState();
 | |
|         this.parseUrl();
 | |
| 
 | |
|         if (this.handleAuthRedirect()) return;
 | |
|         if (!this.checkAccess()) return;
 | |
| 
 | |
|         if (await this.fetchAndUpdateDataModel()) {
 | |
|             this.listenWcoData();
 | |
|             
 | |
|             // This is the correct place to check and start the observer.
 | |
|             if (window.apxtri?.wcoobserver) {
 | |
|                 this.startWcoObserver();
 | |
|             } else {
 | |
|                 console.log('[APX] wcoobserver is not enabled.');
 | |
|             }
 | |
| 
 | |
|             this.afterUpdateCallbacks.forEach(cb => cb());
 | |
|             this.lazyLoad();
 | |
|             this.saveState();
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Instantiate the manager and attach it to the global namespace
 | |
| apx.main = new ApxManager();
 | |
| 
 | |
| // Start the application lifecycle
 | |
| apx.main.ready(() => apx.main.init()); |