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